From c6dacbc2a2d84880fe6bc7177756b4a1127e9c03 Mon Sep 17 00:00:00 2001 From: Zentrik Date: Tue, 6 Jan 2026 23:08:04 +0000 Subject: [PATCH 1/3] no config debugging: Allow concurrent debug sessions Closes https://github.com/microsoft/vscode-python-debugger/issues/613 and driveby fix of https://github.com/microsoft/vscode-python-debugger/issues/820. AI generated and untested but I've reviewed and the diff looks sensible to me (though it does seem to do a lot of mocking in tests). --- bundled/scripts/noConfigScripts/debugpy | 6 ++- bundled/scripts/noConfigScripts/debugpy.bat | 2 +- bundled/scripts/noConfigScripts/debugpy.fish | 4 +- bundled/scripts/noConfigScripts/debugpy.ps1 | 5 ++- src/extension/noConfigDebugInit.ts | 26 ++++++----- .../unittest/noConfigDebugInit.unit.test.ts | 44 ++++++++++++++----- 6 files changed, 61 insertions(+), 26 deletions(-) diff --git a/bundled/scripts/noConfigScripts/debugpy b/bundled/scripts/noConfigScripts/debugpy index def62ec5..7ac3e6f2 100755 --- a/bundled/scripts/noConfigScripts/debugpy +++ b/bundled/scripts/noConfigScripts/debugpy @@ -1,4 +1,6 @@ #! /bin/bash # Bash script -export DEBUGPY_ADAPTER_ENDPOINTS=$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS -python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $@ +endpoint_dir="$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS" +endpoint_file="$(mktemp -p "$endpoint_dir" endpoint-XXXXXX.txt)" +export DEBUGPY_ADAPTER_ENDPOINTS="$endpoint_file" +python3 "$BUNDLED_DEBUGPY_PATH" --listen 0 --wait-for-client "$@" diff --git a/bundled/scripts/noConfigScripts/debugpy.bat b/bundled/scripts/noConfigScripts/debugpy.bat index 2450cf6a..9058cf02 100755 --- a/bundled/scripts/noConfigScripts/debugpy.bat +++ b/bundled/scripts/noConfigScripts/debugpy.bat @@ -1,4 +1,4 @@ @echo off :: Bat script -set DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS% +set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%\endpoint-%RANDOM%%RANDOM%.txt" python %BUNDLED_DEBUGPY_PATH% --listen 0 --wait-for-client %* diff --git a/bundled/scripts/noConfigScripts/debugpy.fish b/bundled/scripts/noConfigScripts/debugpy.fish index 624f7202..0b5cb5de 100755 --- a/bundled/scripts/noConfigScripts/debugpy.fish +++ b/bundled/scripts/noConfigScripts/debugpy.fish @@ -1,3 +1,5 @@ # Fish script -set -x DEBUGPY_ADAPTER_ENDPOINTS $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +set endpoint_dir $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +set endpoint_file (mktemp -p $endpoint_dir endpoint-XXXXXX.txt) +set -x DEBUGPY_ADAPTER_ENDPOINTS $endpoint_file python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $argv diff --git a/bundled/scripts/noConfigScripts/debugpy.ps1 b/bundled/scripts/noConfigScripts/debugpy.ps1 index 4b2ff85a..f703f4b4 100755 --- a/bundled/scripts/noConfigScripts/debugpy.ps1 +++ b/bundled/scripts/noConfigScripts/debugpy.ps1 @@ -1,5 +1,8 @@ # PowerShell script -$env:DEBUGPY_ADAPTER_ENDPOINTS = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +$endpointFolder = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +$endpointFile = Join-Path $endpointFolder ("endpoint-{0}.txt" -f ([System.Guid]::NewGuid().ToString('N').Substring(0, 8))) +$env:DEBUGPY_ADAPTER_ENDPOINTS = $endpointFile + $os = [System.Environment]::OSVersion.Platform if ($os -eq [System.PlatformID]::Win32NT) { python $env:BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $args diff --git a/src/extension/noConfigDebugInit.ts b/src/extension/noConfigDebugInit.ts index 23ed3790..15908059 100644 --- a/src/extension/noConfigDebugInit.ts +++ b/src/extension/noConfigDebugInit.ts @@ -8,6 +8,7 @@ import { DebugSessionOptions, Disposable, GlobalEnvironmentVariableCollection, + env, l10n, RelativePattern, workspace, @@ -39,7 +40,7 @@ export async function registerNoConfigDebug( const collection = envVarCollection; // create a temp directory for the noConfigDebugAdapterEndpoints - // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-stableWorkspaceHash.txt + // folder path format: extPath/.noConfigDebugAdapterEndpoints/ let workspaceString = workspace.workspaceFile?.fsPath; if (!workspaceString) { workspaceString = workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';'); @@ -49,21 +50,26 @@ export async function registerNoConfigDebug( return Promise.resolve(new Disposable(() => {})); } - // create a stable hash for the workspace folder, reduce terminal variable churn + // create a stable hash for the workspace folder and VS Code window, reduce terminal variable churn const hash = crypto.createHash('sha256'); hash.update(workspaceString.toString()); + hash.update(env.sessionId); const stableWorkspaceHash = hash.digest('hex').slice(0, 16); const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); - const tempFilePath = path.join(tempDirPath, `endpoint-${stableWorkspaceHash}.txt`); + const endpointFolderPath = path.join(tempDirPath, stableWorkspaceHash); // create the temp directory if it doesn't exist - if (!fs.existsSync(tempDirPath)) { - fs.mkdirSync(tempDirPath, { recursive: true }); + if (!fs.existsSync(endpointFolderPath)) { + fs.mkdirSync(endpointFolderPath, { recursive: true }); } else { - // remove endpoint file in the temp directory if it exists - if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); + // clean out any existing endpoint files in the folder + const entries = fs.readdirSync(endpointFolderPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile()) { + const entryPath = path.join(endpointFolderPath, entry.name.toString()); + fs.unlinkSync(entryPath); + } } } // clear the env var collection to remove any existing env vars @@ -73,7 +79,7 @@ export async function registerNoConfigDebug( collection.replace('PYDEVD_DISABLE_FILE_VALIDATION', '1'); // Add env vars for VSCODE_DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH - collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', tempFilePath); + collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', endpointFolderPath); const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); const pathSeparator = process.platform === 'win32' ? ';' : ':'; @@ -93,7 +99,7 @@ export async function registerNoConfigDebug( ); // create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written - const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(tempDirPath, '**/*.txt')); + const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, '**/*.txt')); const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => { sendTelemetryEvent(EventName.DEBUG_SESSION_START, undefined, { trigger: 'noConfig' as TriggerType, diff --git a/src/test/unittest/noConfigDebugInit.unit.test.ts b/src/test/unittest/noConfigDebugInit.unit.test.ts index 3cf6e401..369111e7 100644 --- a/src/test/unittest/noConfigDebugInit.unit.test.ts +++ b/src/test/unittest/noConfigDebugInit.unit.test.ts @@ -6,7 +6,7 @@ import { IExtensionContext } from '../../extension/common/types'; import { registerNoConfigDebug as registerNoConfigDebug } from '../../extension/noConfigDebugInit'; import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; -import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, workspace } from 'vscode'; +import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, env, workspace } from 'vscode'; import * as utils from '../../extension/utils'; import { assert } from 'console'; import * as fs from 'fs'; @@ -22,6 +22,9 @@ suite('setup for no-config debug scenario', function () { let DEBUGPY_ADAPTER_ENDPOINTS = 'DEBUGPY_ADAPTER_ENDPOINTS'; let BUNDLED_DEBUGPY_PATH = 'BUNDLED_DEBUGPY_PATH'; let workspaceUriStub: sinon.SinonStub; + let sessionIdStub: sinon.SinonStub; + let stableWorkspaceHash: string; + let workspacePath: string; const testDataDir = path.join(__dirname, 'testData'); const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt'); @@ -34,12 +37,20 @@ suite('setup for no-config debug scenario', function () { noConfigScriptsDir = path.join(context.object.extensionPath, 'bundled/scripts/noConfigScripts'); bundledDebugPath = path.join(context.object.extensionPath, 'bundled/libs/debugpy'); + sessionIdStub = sinon.stub(env, 'sessionId').value('test-session'); + workspacePath = os.tmpdir(); + // Stub crypto.randomBytes with proper typing let randomBytesStub = sinon.stub(crypto, 'randomBytes'); // Provide a valid Buffer object randomBytesStub.callsFake((_size: number) => Buffer.from('1234567899', 'hex')); - workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(os.tmpdir()) }]); + workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(workspacePath) }]); + + const hash = crypto.createHash('sha256'); + hash.update(workspacePath.toString()); + hash.update('test-session'); + stableWorkspaceHash = hash.digest('hex').slice(0, 16); } catch (error) { console.error('Error in setup:', error); } @@ -47,6 +58,7 @@ suite('setup for no-config debug scenario', function () { teardown(() => { sinon.restore(); workspaceUriStub.restore(); + sessionIdStub.restore(); }); test('should add environment variables for DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH', async () => { @@ -59,7 +71,10 @@ suite('setup for no-config debug scenario', function () { .setup((x) => x.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .callback((key, value) => { if (key === DEBUGPY_ADAPTER_ENDPOINTS) { - assert(value.includes('endpoint-')); + assert( + value === + path.join(context.object.extensionPath, '.noConfigDebugAdapterEndpoints', stableWorkspaceHash), + ); } else if (key === BUNDLED_DEBUGPY_PATH) { assert(value === bundledDebugPath); } else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') { @@ -194,7 +209,7 @@ suite('setup for no-config debug scenario', function () { // Assert sinon.assert.calledOnce(createFileSystemWatcherFunct); const expectedPattern = new RelativePattern( - path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'), + path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash), '**/*.txt', ); sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern); @@ -261,26 +276,33 @@ suite('setup for no-config debug scenario', function () { sinon.assert.calledWith(debugStub, undefined, expectedConfig, optionsExpected); }); - test('should check if tempFilePath exists when debuggerAdapterEndpointFolder exists', async () => { + test('should clear existing endpoint files when debuggerAdapterEndpointFolder exists', async () => { // Arrange const environmentVariableCollectionMock = TypeMoq.Mock.ofType(); context.setup((c) => c.environmentVariableCollection).returns(() => environmentVariableCollectionMock.object); - const fsExistsSyncStub = sinon.stub(fs, 'existsSync').returns(true); + const endpointFolderPath = path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash); + const fsExistsSyncStub = sinon.stub(fs, 'existsSync').callsFake((p) => p === endpointFolderPath); + const fakeDirent = { isFile: () => true, name: Buffer.from('old.txt') } as unknown as fs.Dirent; + const fsReaddirSyncStub = sinon + .stub(fs, 'readdirSync') + .callsFake((dirPath: fs.PathLike, options?: any) => { + assert(dirPath === endpointFolderPath); + assert(options?.withFileTypes === true); + return [fakeDirent] as unknown as fs.Dirent[]; + }); const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync'); // Act await registerNoConfigDebug(context.object.environmentVariableCollection, context.object.extensionPath); // Assert - sinon.assert.calledWith( - fsExistsSyncStub, - sinon.match((value: any) => value.includes('endpoint-')), - ); - sinon.assert.calledOnce(fsUnlinkSyncStub); + sinon.assert.calledWith(fsExistsSyncStub, endpointFolderPath); + sinon.assert.calledWith(fsUnlinkSyncStub, path.join(endpointFolderPath, 'old.txt')); // Cleanup fsExistsSyncStub.restore(); + fsReaddirSyncStub.restore(); fsUnlinkSyncStub.restore(); }); }); From 891f02783ad45286fa06da84090b8971047dd6a8 Mon Sep 17 00:00:00 2001 From: Zentrik Date: Thu, 8 Jan 2026 23:30:40 +0000 Subject: [PATCH 2/3] Format --- .../unittest/noConfigDebugInit.unit.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/test/unittest/noConfigDebugInit.unit.test.ts b/src/test/unittest/noConfigDebugInit.unit.test.ts index 369111e7..a8357c51 100644 --- a/src/test/unittest/noConfigDebugInit.unit.test.ts +++ b/src/test/unittest/noConfigDebugInit.unit.test.ts @@ -73,7 +73,11 @@ suite('setup for no-config debug scenario', function () { if (key === DEBUGPY_ADAPTER_ENDPOINTS) { assert( value === - path.join(context.object.extensionPath, '.noConfigDebugAdapterEndpoints', stableWorkspaceHash), + path.join( + context.object.extensionPath, + '.noConfigDebugAdapterEndpoints', + stableWorkspaceHash, + ), ); } else if (key === BUNDLED_DEBUGPY_PATH) { assert(value === bundledDebugPath); @@ -284,13 +288,11 @@ suite('setup for no-config debug scenario', function () { const endpointFolderPath = path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash); const fsExistsSyncStub = sinon.stub(fs, 'existsSync').callsFake((p) => p === endpointFolderPath); const fakeDirent = { isFile: () => true, name: Buffer.from('old.txt') } as unknown as fs.Dirent; - const fsReaddirSyncStub = sinon - .stub(fs, 'readdirSync') - .callsFake((dirPath: fs.PathLike, options?: any) => { - assert(dirPath === endpointFolderPath); - assert(options?.withFileTypes === true); - return [fakeDirent] as unknown as fs.Dirent[]; - }); + const fsReaddirSyncStub = sinon.stub(fs, 'readdirSync').callsFake((dirPath: fs.PathLike, options?: any) => { + assert(dirPath === endpointFolderPath); + assert(options?.withFileTypes === true); + return [fakeDirent] as unknown as fs.Dirent[]; + }); const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync'); // Act From 1ebf774472b8dd798dbb91fdb09a61c6251fbd09 Mon Sep 17 00:00:00 2001 From: Zentrik Date: Sat, 10 Jan 2026 19:26:07 +0000 Subject: [PATCH 3/3] Switch to endpoint files of the form `endpoint-{window_id}-{random_str}.txt` This fixes the issue with never cleaning up the created folders in the previous implementation --- bundled/scripts/noConfigScripts/debugpy | 5 +- bundled/scripts/noConfigScripts/debugpy.bat | 3 +- bundled/scripts/noConfigScripts/debugpy.fish | 5 +- bundled/scripts/noConfigScripts/debugpy.ps1 | 7 ++- src/extension/noConfigDebugInit.ts | 33 ++++------- .../unittest/noConfigDebugInit.unit.test.ts | 57 +++++++------------ 6 files changed, 43 insertions(+), 67 deletions(-) diff --git a/bundled/scripts/noConfigScripts/debugpy b/bundled/scripts/noConfigScripts/debugpy index 7ac3e6f2..038c3f35 100755 --- a/bundled/scripts/noConfigScripts/debugpy +++ b/bundled/scripts/noConfigScripts/debugpy @@ -1,6 +1,5 @@ #! /bin/bash # Bash script -endpoint_dir="$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS" -endpoint_file="$(mktemp -p "$endpoint_dir" endpoint-XXXXXX.txt)" -export DEBUGPY_ADAPTER_ENDPOINTS="$endpoint_file" +# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; mktemp creates the file atomically to prevent races +export DEBUGPY_ADAPTER_ENDPOINTS=$(mktemp "${VSCODE_DEBUGPY_ADAPTER_ENDPOINTS}XXXXXX.txt") python3 "$BUNDLED_DEBUGPY_PATH" --listen 0 --wait-for-client "$@" diff --git a/bundled/scripts/noConfigScripts/debugpy.bat b/bundled/scripts/noConfigScripts/debugpy.bat index 9058cf02..bc927bc1 100755 --- a/bundled/scripts/noConfigScripts/debugpy.bat +++ b/bundled/scripts/noConfigScripts/debugpy.bat @@ -1,4 +1,5 @@ @echo off :: Bat script -set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%\endpoint-%RANDOM%%RANDOM%.txt" +:: VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; append random suffix to create unique file +set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%%RANDOM%%RANDOM%.txt" python %BUNDLED_DEBUGPY_PATH% --listen 0 --wait-for-client %* diff --git a/bundled/scripts/noConfigScripts/debugpy.fish b/bundled/scripts/noConfigScripts/debugpy.fish index 0b5cb5de..3c30a5fc 100755 --- a/bundled/scripts/noConfigScripts/debugpy.fish +++ b/bundled/scripts/noConfigScripts/debugpy.fish @@ -1,5 +1,4 @@ # Fish script -set endpoint_dir $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS -set endpoint_file (mktemp -p $endpoint_dir endpoint-XXXXXX.txt) -set -x DEBUGPY_ADAPTER_ENDPOINTS $endpoint_file +# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; mktemp creates the file atomically to prevent races +set -x DEBUGPY_ADAPTER_ENDPOINTS (mktemp "$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS"XXXXXX.txt) python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $argv diff --git a/bundled/scripts/noConfigScripts/debugpy.ps1 b/bundled/scripts/noConfigScripts/debugpy.ps1 index f703f4b4..1a09c142 100755 --- a/bundled/scripts/noConfigScripts/debugpy.ps1 +++ b/bundled/scripts/noConfigScripts/debugpy.ps1 @@ -1,7 +1,8 @@ # PowerShell script -$endpointFolder = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS -$endpointFile = Join-Path $endpointFolder ("endpoint-{0}.txt" -f ([System.Guid]::NewGuid().ToString('N').Substring(0, 8))) -$env:DEBUGPY_ADAPTER_ENDPOINTS = $endpointFile +# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; append random suffix to create unique file +$endpointPrefix = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +$randomString = [System.Guid]::NewGuid().ToString('N').Substring(0, 8) +$env:DEBUGPY_ADAPTER_ENDPOINTS = "${endpointPrefix}${randomString}.txt" $os = [System.Environment]::OSVersion.Platform if ($os -eq [System.PlatformID]::Win32NT) { diff --git a/src/extension/noConfigDebugInit.ts b/src/extension/noConfigDebugInit.ts index 15908059..2372828e 100644 --- a/src/extension/noConfigDebugInit.ts +++ b/src/extension/noConfigDebugInit.ts @@ -11,7 +11,6 @@ import { env, l10n, RelativePattern, - workspace, } from 'vscode'; import { createFileSystemWatcher, debugStartDebugging } from './utils'; import { traceError, traceVerbose } from './common/log/logging'; @@ -40,33 +39,22 @@ export async function registerNoConfigDebug( const collection = envVarCollection; // create a temp directory for the noConfigDebugAdapterEndpoints - // folder path format: extPath/.noConfigDebugAdapterEndpoints/ - let workspaceString = workspace.workspaceFile?.fsPath; - if (!workspaceString) { - workspaceString = workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';'); - } - if (!workspaceString) { - traceError('No workspace folder found'); - return Promise.resolve(new Disposable(() => {})); - } - - // create a stable hash for the workspace folder and VS Code window, reduce terminal variable churn + // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-windowHash-* const hash = crypto.createHash('sha256'); - hash.update(workspaceString.toString()); hash.update(env.sessionId); - const stableWorkspaceHash = hash.digest('hex').slice(0, 16); + const windowHash = hash.digest('hex').slice(0, 16); - const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); - const endpointFolderPath = path.join(tempDirPath, stableWorkspaceHash); + const endpointFolderPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); + const endpointPrefix = `endpoint-${windowHash}-`; - // create the temp directory if it doesn't exist + // create the directory if it doesn't exist if (!fs.existsSync(endpointFolderPath)) { fs.mkdirSync(endpointFolderPath, { recursive: true }); } else { - // clean out any existing endpoint files in the folder + // clean out any existing endpoint files for this window hash const entries = fs.readdirSync(endpointFolderPath, { withFileTypes: true }); for (const entry of entries) { - if (entry.isFile()) { + if (entry.isFile() && entry.name.startsWith(endpointPrefix)) { const entryPath = path.join(endpointFolderPath, entry.name.toString()); fs.unlinkSync(entryPath); } @@ -79,7 +67,8 @@ export async function registerNoConfigDebug( collection.replace('PYDEVD_DISABLE_FILE_VALIDATION', '1'); // Add env vars for VSCODE_DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH - collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', endpointFolderPath); + // VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is the prefix for endpoint files - scripts append something unique to avoid collisions + collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', path.join(endpointFolderPath, endpointPrefix)); const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts'); const pathSeparator = process.platform === 'win32' ? ';' : ':'; @@ -98,8 +87,8 @@ export async function registerNoConfigDebug( 'Enables use of [no-config debugging](https://github.com/microsoft/vscode-python-debugger/wiki/No%E2%80%90Config-Debugging), `debugpy `, in the terminal.', ); - // create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written - const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, '**/*.txt')); + // create file system watcher for endpoint files matching this window's prefix + const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, `${endpointPrefix}*`)); const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => { sendTelemetryEvent(EventName.DEBUG_SESSION_START, undefined, { trigger: 'noConfig' as TriggerType, diff --git a/src/test/unittest/noConfigDebugInit.unit.test.ts b/src/test/unittest/noConfigDebugInit.unit.test.ts index a8357c51..f7731e8a 100644 --- a/src/test/unittest/noConfigDebugInit.unit.test.ts +++ b/src/test/unittest/noConfigDebugInit.unit.test.ts @@ -6,7 +6,7 @@ import { IExtensionContext } from '../../extension/common/types'; import { registerNoConfigDebug as registerNoConfigDebug } from '../../extension/noConfigDebugInit'; import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; -import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, env, workspace } from 'vscode'; +import { DebugConfiguration, DebugSessionOptions, env, RelativePattern, Uri, workspace } from 'vscode'; import * as utils from '../../extension/utils'; import { assert } from 'console'; import * as fs from 'fs'; @@ -22,9 +22,7 @@ suite('setup for no-config debug scenario', function () { let DEBUGPY_ADAPTER_ENDPOINTS = 'DEBUGPY_ADAPTER_ENDPOINTS'; let BUNDLED_DEBUGPY_PATH = 'BUNDLED_DEBUGPY_PATH'; let workspaceUriStub: sinon.SinonStub; - let sessionIdStub: sinon.SinonStub; - let stableWorkspaceHash: string; - let workspacePath: string; + let windowHash: string; const testDataDir = path.join(__dirname, 'testData'); const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt'); @@ -37,20 +35,18 @@ suite('setup for no-config debug scenario', function () { noConfigScriptsDir = path.join(context.object.extensionPath, 'bundled/scripts/noConfigScripts'); bundledDebugPath = path.join(context.object.extensionPath, 'bundled/libs/debugpy'); - sessionIdStub = sinon.stub(env, 'sessionId').value('test-session'); - workspacePath = os.tmpdir(); - // Stub crypto.randomBytes with proper typing let randomBytesStub = sinon.stub(crypto, 'randomBytes'); // Provide a valid Buffer object randomBytesStub.callsFake((_size: number) => Buffer.from('1234567899', 'hex')); - workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(workspacePath) }]); + workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(os.tmpdir()) }]); - const hash = crypto.createHash('sha256'); - hash.update(workspacePath.toString()); - hash.update('test-session'); - stableWorkspaceHash = hash.digest('hex').slice(0, 16); + // Stub env.sessionId to get a stable window hash + sinon.stub(env, 'sessionId').value('test-session-id'); + const hashObj = crypto.createHash('sha256'); + hashObj.update('test-session-id'); + windowHash = hashObj.digest('hex').substring(0, 16); } catch (error) { console.error('Error in setup:', error); } @@ -58,7 +54,6 @@ suite('setup for no-config debug scenario', function () { teardown(() => { sinon.restore(); workspaceUriStub.restore(); - sessionIdStub.restore(); }); test('should add environment variables for DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH', async () => { @@ -71,14 +66,8 @@ suite('setup for no-config debug scenario', function () { .setup((x) => x.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .callback((key, value) => { if (key === DEBUGPY_ADAPTER_ENDPOINTS) { - assert( - value === - path.join( - context.object.extensionPath, - '.noConfigDebugAdapterEndpoints', - stableWorkspaceHash, - ), - ); + assert(value.includes('endpoint-')); + assert(value.includes(windowHash)); } else if (key === BUNDLED_DEBUGPY_PATH) { assert(value === bundledDebugPath); } else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') { @@ -213,8 +202,8 @@ suite('setup for no-config debug scenario', function () { // Assert sinon.assert.calledOnce(createFileSystemWatcherFunct); const expectedPattern = new RelativePattern( - path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash), - '**/*.txt', + path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'), + `endpoint-${windowHash}-*`, ); sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern); }); @@ -280,27 +269,25 @@ suite('setup for no-config debug scenario', function () { sinon.assert.calledWith(debugStub, undefined, expectedConfig, optionsExpected); }); - test('should clear existing endpoint files when debuggerAdapterEndpointFolder exists', async () => { + test('should clean up existing endpoint files for this window hash when debuggerAdapterEndpointFolder exists', async () => { // Arrange const environmentVariableCollectionMock = TypeMoq.Mock.ofType(); context.setup((c) => c.environmentVariableCollection).returns(() => environmentVariableCollectionMock.object); - const endpointFolderPath = path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash); - const fsExistsSyncStub = sinon.stub(fs, 'existsSync').callsFake((p) => p === endpointFolderPath); - const fakeDirent = { isFile: () => true, name: Buffer.from('old.txt') } as unknown as fs.Dirent; - const fsReaddirSyncStub = sinon.stub(fs, 'readdirSync').callsFake((dirPath: fs.PathLike, options?: any) => { - assert(dirPath === endpointFolderPath); - assert(options?.withFileTypes === true); - return [fakeDirent] as unknown as fs.Dirent[]; - }); + const fsExistsSyncStub = sinon.stub(fs, 'existsSync').returns(true); + const fsReaddirSyncStub = sinon.stub(fs, 'readdirSync').returns([ + { name: `endpoint-${windowHash}-abc123.txt`, isFile: () => true }, + { name: `endpoint-otherhash-def456.txt`, isFile: () => true }, + { name: 'somedir', isFile: () => false }, + ] as any); const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync'); // Act await registerNoConfigDebug(context.object.environmentVariableCollection, context.object.extensionPath); - // Assert - sinon.assert.calledWith(fsExistsSyncStub, endpointFolderPath); - sinon.assert.calledWith(fsUnlinkSyncStub, path.join(endpointFolderPath, 'old.txt')); + // Assert - only files matching this window hash should be deleted + sinon.assert.called(fsReaddirSyncStub); + sinon.assert.calledOnce(fsUnlinkSyncStub); // Cleanup fsExistsSyncStub.restore();