diff --git a/src/extension/noConfigDebugInit.ts b/src/extension/noConfigDebugInit.ts index e623e951..27e22c53 100644 --- a/src/extension/noConfigDebugInit.ts +++ b/src/extension/noConfigDebugInit.ts @@ -4,10 +4,16 @@ import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; -import * as os from 'os'; -import { DebugSessionOptions, Disposable, GlobalEnvironmentVariableCollection, l10n, RelativePattern } from 'vscode'; +import { + DebugSessionOptions, + Disposable, + GlobalEnvironmentVariableCollection, + l10n, + RelativePattern, + workspace, +} from 'vscode'; import { createFileSystemWatcher, debugStartDebugging } from './utils'; -import { traceError, traceLog, traceVerbose } from './common/log/logging'; +import { traceError, traceVerbose } from './common/log/logging'; /** * Registers the configuration-less debugging setup for the extension. @@ -30,21 +36,30 @@ export async function registerNoConfigDebug( const collection = envVarCollection; // create a temp directory for the noConfigDebugAdapterEndpoints - // file path format: tempDir/noConfigDebugAdapterEndpoints-/debuggerAdapterEndpoint.txt - const randomSuffix = crypto.randomBytes(10).toString('hex'); - const tempDirName = `noConfigDebugAdapterEndpoints-${randomSuffix}`; - let tempDirPath = path.join(os.tmpdir(), tempDirName); - try { - traceLog('Attempting to use temp directory for noConfigDebugAdapterEndpoints, dir name:', tempDirName); - await fs.promises.mkdir(tempDirPath, { recursive: true }); - } catch (error) { - // Handle the error when accessing the temp directory - traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); - // Make new temp directory in extension root dird - tempDirPath = path.join(extPath, '.temp'); - await fs.promises.mkdir(tempDirPath, { recursive: true }); + // file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-stableWorkspaceHash.txt + const workspaceUri = workspace.workspaceFolders?.[0]?.uri; + if (!workspaceUri) { + traceError('No workspace folder found'); + return Promise.resolve(new Disposable(() => {})); + } + + // create a stable hash for the workspace folder, reduce terminal variable churn + const hash = crypto.createHash('sha256'); + hash.update(workspaceUri.toString()); + const stableWorkspaceHash = hash.digest('hex').slice(0, 16); + + const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints'); + const tempFilePath = path.join(tempDirPath, `endpoint-${stableWorkspaceHash}.txt`); + + // create the temp directory if it doesn't exist + if (!fs.existsSync(tempDirPath)) { + fs.mkdirSync(tempDirPath, { recursive: true }); + } else { + // remove endpoint file in the temp directory if it exists + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } } - const tempFilePath = path.join(tempDirPath, 'debuggerAdapterEndpoint.txt'); // Add env var for PYDEVD_DISABLE_FILE_VALIDATION to disable extra output in terminal when starting the debug session. collection.replace('PYDEVD_DISABLE_FILE_VALIDATION', '1'); diff --git a/src/test/unittest/noConfigDebugInit.unit.test.ts b/src/test/unittest/noConfigDebugInit.unit.test.ts index 5be9edca..47f3a9fb 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 } from 'vscode'; +import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, workspace } from 'vscode'; import * as utils from '../../extension/utils'; import { assert } from 'console'; import * as fs from 'fs'; @@ -21,7 +21,7 @@ suite('setup for no-config debug scenario', function () { let bundledDebugPath: string; let DEBUGPY_ADAPTER_ENDPOINTS = 'DEBUGPY_ADAPTER_ENDPOINTS'; let BUNDLED_DEBUGPY_PATH = 'BUNDLED_DEBUGPY_PATH'; - let tempDirPath: string; + let workspaceUriStub: sinon.SinonStub; const testDataDir = path.join(__dirname, 'testData'); const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt'); @@ -29,10 +29,7 @@ suite('setup for no-config debug scenario', function () { try { context = TypeMoq.Mock.ofType(); - const randomSuffix = '1234567899'; - const tempDirName = `noConfigDebugAdapterEndpoints-${randomSuffix}`; - tempDirPath = path.join(os.tmpdir(), tempDirName); - context.setup((c) => (c as any).extensionPath).returns(() => 'fake/extension/path'); + context.setup((c) => (c as any).extensionPath).returns(() => os.tmpdir()); context.setup((c) => c.subscriptions).returns(() => []); noConfigScriptsDir = path.join(context.object.extensionPath, 'bundled/scripts/noConfigScripts'); bundledDebugPath = path.join(context.object.extensionPath, 'bundled/libs/debugpy'); @@ -41,12 +38,15 @@ suite('setup for no-config debug scenario', function () { 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()) }]); } catch (error) { console.error('Error in setup:', error); } }); teardown(() => { sinon.restore(); + workspaceUriStub.restore(); }); test('should add environment variables for DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH', async () => { @@ -59,7 +59,7 @@ 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('noConfigDebugAdapterEndpoints-1234567899')); + assert(value.includes('endpoint-')); } else if (key === BUNDLED_DEBUGPY_PATH) { assert(value === bundledDebugPath); } else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') { @@ -99,7 +99,7 @@ suite('setup for no-config debug scenario', function () { // Assert sinon.assert.calledOnce(createFileSystemWatcherFunct); - const expectedPattern = new RelativePattern(tempDirPath, '**/*'); + const expectedPattern = new RelativePattern(path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'), '**/*'); sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern); }); @@ -163,6 +163,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 () => { + // Arrange + const environmentVariableCollectionMock = TypeMoq.Mock.ofType(); + context.setup((c) => c.environmentVariableCollection).returns(() => environmentVariableCollectionMock.object); + + const fsExistsSyncStub = sinon.stub(fs, 'existsSync').returns(true); + 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); + + // Cleanup + fsExistsSyncStub.restore(); + fsUnlinkSyncStub.restore(); + }); }); function setupFileSystemWatchers(): sinon.SinonStub {