diff --git a/bundled/scripts/noConfigScripts/debugpy b/bundled/scripts/noConfigScripts/debugpy index def62ec5..038c3f35 100755 --- a/bundled/scripts/noConfigScripts/debugpy +++ b/bundled/scripts/noConfigScripts/debugpy @@ -1,4 +1,5 @@ #! /bin/bash # Bash script -export DEBUGPY_ADAPTER_ENDPOINTS=$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS -python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $@ +# 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 2450cf6a..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% +:: 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 624f7202..3c30a5fc 100755 --- a/bundled/scripts/noConfigScripts/debugpy.fish +++ b/bundled/scripts/noConfigScripts/debugpy.fish @@ -1,3 +1,4 @@ # Fish script -set -x DEBUGPY_ADAPTER_ENDPOINTS $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +# 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 4b2ff85a..1a09c142 100755 --- a/bundled/scripts/noConfigScripts/debugpy.ps1 +++ b/bundled/scripts/noConfigScripts/debugpy.ps1 @@ -1,5 +1,9 @@ # PowerShell script -$env:DEBUGPY_ADAPTER_ENDPOINTS = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS +# 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) { 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..2372828e 100644 --- a/src/extension/noConfigDebugInit.ts +++ b/src/extension/noConfigDebugInit.ts @@ -8,9 +8,9 @@ import { DebugSessionOptions, Disposable, GlobalEnvironmentVariableCollection, + env, l10n, RelativePattern, - workspace, } from 'vscode'; import { createFileSystemWatcher, debugStartDebugging } from './utils'; import { traceError, traceVerbose } from './common/log/logging'; @@ -39,31 +39,25 @@ export async function registerNoConfigDebug( const collection = envVarCollection; // create a temp directory for the noConfigDebugAdapterEndpoints - // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-stableWorkspaceHash.txt - 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, reduce terminal variable churn + // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-windowHash-* const hash = crypto.createHash('sha256'); - hash.update(workspaceString.toString()); - const stableWorkspaceHash = hash.digest('hex').slice(0, 16); + hash.update(env.sessionId); + const windowHash = hash.digest('hex').slice(0, 16); - const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); - const tempFilePath = path.join(tempDirPath, `endpoint-${stableWorkspaceHash}.txt`); + const endpointFolderPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); + const endpointPrefix = `endpoint-${windowHash}-`; - // create the temp directory if it doesn't exist - if (!fs.existsSync(tempDirPath)) { - fs.mkdirSync(tempDirPath, { recursive: true }); + // create the directory if it doesn't exist + 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 for this window hash + const entries = fs.readdirSync(endpointFolderPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.startsWith(endpointPrefix)) { + const entryPath = path.join(endpointFolderPath, entry.name.toString()); + fs.unlinkSync(entryPath); + } } } // clear the env var collection to remove any existing env vars @@ -73,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', tempFilePath); + // 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' ? ';' : ':'; @@ -92,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(tempDirPath, '**/*.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 3cf6e401..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, 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,6 +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 windowHash: string; const testDataDir = path.join(__dirname, 'testData'); const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt'); @@ -40,6 +41,12 @@ suite('setup for no-config debug scenario', function () { randomBytesStub.callsFake((_size: number) => Buffer.from('1234567899', 'hex')); workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(os.tmpdir()) }]); + + // 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); } @@ -60,6 +67,7 @@ suite('setup for no-config debug scenario', function () { .callback((key, value) => { if (key === DEBUGPY_ADAPTER_ENDPOINTS) { assert(value.includes('endpoint-')); + assert(value.includes(windowHash)); } else if (key === BUNDLED_DEBUGPY_PATH) { assert(value === bundledDebugPath); } else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') { @@ -195,7 +203,7 @@ suite('setup for no-config debug scenario', function () { sinon.assert.calledOnce(createFileSystemWatcherFunct); const expectedPattern = new RelativePattern( path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'), - '**/*.txt', + `endpoint-${windowHash}-*`, ); sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern); }); @@ -261,26 +269,29 @@ 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 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 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, - sinon.match((value: any) => value.includes('endpoint-')), - ); + // Assert - only files matching this window hash should be deleted + sinon.assert.called(fsReaddirSyncStub); sinon.assert.calledOnce(fsUnlinkSyncStub); // Cleanup fsExistsSyncStub.restore(); + fsReaddirSyncStub.restore(); fsUnlinkSyncStub.restore(); }); });