Skip to content

Commit 1ebf774

Browse files
committed
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
1 parent db1f83c commit 1ebf774

File tree

6 files changed

+43
-67
lines changed

6 files changed

+43
-67
lines changed
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#! /bin/bash
22
# Bash script
3-
endpoint_dir="$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS"
4-
endpoint_file="$(mktemp -p "$endpoint_dir" endpoint-XXXXXX.txt)"
5-
export DEBUGPY_ADAPTER_ENDPOINTS="$endpoint_file"
3+
# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; mktemp creates the file atomically to prevent races
4+
export DEBUGPY_ADAPTER_ENDPOINTS=$(mktemp "${VSCODE_DEBUGPY_ADAPTER_ENDPOINTS}XXXXXX.txt")
65
python3 "$BUNDLED_DEBUGPY_PATH" --listen 0 --wait-for-client "$@"
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@echo off
22
:: Bat script
3-
set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%\endpoint-%RANDOM%%RANDOM%.txt"
3+
:: VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; append random suffix to create unique file
4+
set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%%RANDOM%%RANDOM%.txt"
45
python %BUNDLED_DEBUGPY_PATH% --listen 0 --wait-for-client %*
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Fish script
2-
set endpoint_dir $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
3-
set endpoint_file (mktemp -p $endpoint_dir endpoint-XXXXXX.txt)
4-
set -x DEBUGPY_ADAPTER_ENDPOINTS $endpoint_file
2+
# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; mktemp creates the file atomically to prevent races
3+
set -x DEBUGPY_ADAPTER_ENDPOINTS (mktemp "$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS"XXXXXX.txt)
54
python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $argv

bundled/scripts/noConfigScripts/debugpy.ps1

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# PowerShell script
2-
$endpointFolder = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
3-
$endpointFile = Join-Path $endpointFolder ("endpoint-{0}.txt" -f ([System.Guid]::NewGuid().ToString('N').Substring(0, 8)))
4-
$env:DEBUGPY_ADAPTER_ENDPOINTS = $endpointFile
2+
# VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is a prefix; append random suffix to create unique file
3+
$endpointPrefix = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
4+
$randomString = [System.Guid]::NewGuid().ToString('N').Substring(0, 8)
5+
$env:DEBUGPY_ADAPTER_ENDPOINTS = "${endpointPrefix}${randomString}.txt"
56

67
$os = [System.Environment]::OSVersion.Platform
78
if ($os -eq [System.PlatformID]::Win32NT) {

src/extension/noConfigDebugInit.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
env,
1212
l10n,
1313
RelativePattern,
14-
workspace,
1514
} from 'vscode';
1615
import { createFileSystemWatcher, debugStartDebugging } from './utils';
1716
import { traceError, traceVerbose } from './common/log/logging';
@@ -40,33 +39,22 @@ export async function registerNoConfigDebug(
4039
const collection = envVarCollection;
4140

4241
// create a temp directory for the noConfigDebugAdapterEndpoints
43-
// folder path format: extPath/.noConfigDebugAdapterEndpoints/<stableWorkspaceHash>
44-
let workspaceString = workspace.workspaceFile?.fsPath;
45-
if (!workspaceString) {
46-
workspaceString = workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';');
47-
}
48-
if (!workspaceString) {
49-
traceError('No workspace folder found');
50-
return Promise.resolve(new Disposable(() => {}));
51-
}
52-
53-
// create a stable hash for the workspace folder and VS Code window, reduce terminal variable churn
42+
// file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-windowHash-*
5443
const hash = crypto.createHash('sha256');
55-
hash.update(workspaceString.toString());
5644
hash.update(env.sessionId);
57-
const stableWorkspaceHash = hash.digest('hex').slice(0, 16);
45+
const windowHash = hash.digest('hex').slice(0, 16);
5846

59-
const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints');
60-
const endpointFolderPath = path.join(tempDirPath, stableWorkspaceHash);
47+
const endpointFolderPath = path.join(extPath, '.noConfigDebugAdapterEndpoints');
48+
const endpointPrefix = `endpoint-${windowHash}-`;
6149

62-
// create the temp directory if it doesn't exist
50+
// create the directory if it doesn't exist
6351
if (!fs.existsSync(endpointFolderPath)) {
6452
fs.mkdirSync(endpointFolderPath, { recursive: true });
6553
} else {
66-
// clean out any existing endpoint files in the folder
54+
// clean out any existing endpoint files for this window hash
6755
const entries = fs.readdirSync(endpointFolderPath, { withFileTypes: true });
6856
for (const entry of entries) {
69-
if (entry.isFile()) {
57+
if (entry.isFile() && entry.name.startsWith(endpointPrefix)) {
7058
const entryPath = path.join(endpointFolderPath, entry.name.toString());
7159
fs.unlinkSync(entryPath);
7260
}
@@ -79,7 +67,8 @@ export async function registerNoConfigDebug(
7967
collection.replace('PYDEVD_DISABLE_FILE_VALIDATION', '1');
8068

8169
// Add env vars for VSCODE_DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH
82-
collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', endpointFolderPath);
70+
// VSCODE_DEBUGPY_ADAPTER_ENDPOINTS is the prefix for endpoint files - scripts append something unique to avoid collisions
71+
collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', path.join(endpointFolderPath, endpointPrefix));
8372

8473
const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
8574
const pathSeparator = process.platform === 'win32' ? ';' : ':';
@@ -98,8 +87,8 @@ export async function registerNoConfigDebug(
9887
'Enables use of [no-config debugging](https://github.com/microsoft/vscode-python-debugger/wiki/No%E2%80%90Config-Debugging), `debugpy <script.py>`, in the terminal.',
9988
);
10089

101-
// create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written
102-
const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, '**/*.txt'));
90+
// create file system watcher for endpoint files matching this window's prefix
91+
const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, `${endpointPrefix}*`));
10392
const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => {
10493
sendTelemetryEvent(EventName.DEBUG_SESSION_START, undefined, {
10594
trigger: 'noConfig' as TriggerType,

src/test/unittest/noConfigDebugInit.unit.test.ts

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IExtensionContext } from '../../extension/common/types';
66
import { registerNoConfigDebug as registerNoConfigDebug } from '../../extension/noConfigDebugInit';
77
import * as TypeMoq from 'typemoq';
88
import * as sinon from 'sinon';
9-
import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, env, workspace } from 'vscode';
9+
import { DebugConfiguration, DebugSessionOptions, env, RelativePattern, Uri, workspace } from 'vscode';
1010
import * as utils from '../../extension/utils';
1111
import { assert } from 'console';
1212
import * as fs from 'fs';
@@ -22,9 +22,7 @@ suite('setup for no-config debug scenario', function () {
2222
let DEBUGPY_ADAPTER_ENDPOINTS = 'DEBUGPY_ADAPTER_ENDPOINTS';
2323
let BUNDLED_DEBUGPY_PATH = 'BUNDLED_DEBUGPY_PATH';
2424
let workspaceUriStub: sinon.SinonStub;
25-
let sessionIdStub: sinon.SinonStub;
26-
let stableWorkspaceHash: string;
27-
let workspacePath: string;
25+
let windowHash: string;
2826

2927
const testDataDir = path.join(__dirname, 'testData');
3028
const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt');
@@ -37,28 +35,25 @@ suite('setup for no-config debug scenario', function () {
3735
noConfigScriptsDir = path.join(context.object.extensionPath, 'bundled/scripts/noConfigScripts');
3836
bundledDebugPath = path.join(context.object.extensionPath, 'bundled/libs/debugpy');
3937

40-
sessionIdStub = sinon.stub(env, 'sessionId').value('test-session');
41-
workspacePath = os.tmpdir();
42-
4338
// Stub crypto.randomBytes with proper typing
4439
let randomBytesStub = sinon.stub(crypto, 'randomBytes');
4540
// Provide a valid Buffer object
4641
randomBytesStub.callsFake((_size: number) => Buffer.from('1234567899', 'hex'));
4742

48-
workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(workspacePath) }]);
43+
workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(os.tmpdir()) }]);
4944

50-
const hash = crypto.createHash('sha256');
51-
hash.update(workspacePath.toString());
52-
hash.update('test-session');
53-
stableWorkspaceHash = hash.digest('hex').slice(0, 16);
45+
// Stub env.sessionId to get a stable window hash
46+
sinon.stub(env, 'sessionId').value('test-session-id');
47+
const hashObj = crypto.createHash('sha256');
48+
hashObj.update('test-session-id');
49+
windowHash = hashObj.digest('hex').substring(0, 16);
5450
} catch (error) {
5551
console.error('Error in setup:', error);
5652
}
5753
});
5854
teardown(() => {
5955
sinon.restore();
6056
workspaceUriStub.restore();
61-
sessionIdStub.restore();
6257
});
6358

6459
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 () {
7166
.setup((x) => x.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
7267
.callback((key, value) => {
7368
if (key === DEBUGPY_ADAPTER_ENDPOINTS) {
74-
assert(
75-
value ===
76-
path.join(
77-
context.object.extensionPath,
78-
'.noConfigDebugAdapterEndpoints',
79-
stableWorkspaceHash,
80-
),
81-
);
69+
assert(value.includes('endpoint-'));
70+
assert(value.includes(windowHash));
8271
} else if (key === BUNDLED_DEBUGPY_PATH) {
8372
assert(value === bundledDebugPath);
8473
} else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') {
@@ -213,8 +202,8 @@ suite('setup for no-config debug scenario', function () {
213202
// Assert
214203
sinon.assert.calledOnce(createFileSystemWatcherFunct);
215204
const expectedPattern = new RelativePattern(
216-
path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash),
217-
'**/*.txt',
205+
path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'),
206+
`endpoint-${windowHash}-*`,
218207
);
219208
sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern);
220209
});
@@ -280,27 +269,25 @@ suite('setup for no-config debug scenario', function () {
280269
sinon.assert.calledWith(debugStub, undefined, expectedConfig, optionsExpected);
281270
});
282271

283-
test('should clear existing endpoint files when debuggerAdapterEndpointFolder exists', async () => {
272+
test('should clean up existing endpoint files for this window hash when debuggerAdapterEndpointFolder exists', async () => {
284273
// Arrange
285274
const environmentVariableCollectionMock = TypeMoq.Mock.ofType<any>();
286275
context.setup((c) => c.environmentVariableCollection).returns(() => environmentVariableCollectionMock.object);
287276

288-
const endpointFolderPath = path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash);
289-
const fsExistsSyncStub = sinon.stub(fs, 'existsSync').callsFake((p) => p === endpointFolderPath);
290-
const fakeDirent = { isFile: () => true, name: Buffer.from('old.txt') } as unknown as fs.Dirent<Buffer>;
291-
const fsReaddirSyncStub = sinon.stub(fs, 'readdirSync').callsFake((dirPath: fs.PathLike, options?: any) => {
292-
assert(dirPath === endpointFolderPath);
293-
assert(options?.withFileTypes === true);
294-
return [fakeDirent] as unknown as fs.Dirent<Buffer>[];
295-
});
277+
const fsExistsSyncStub = sinon.stub(fs, 'existsSync').returns(true);
278+
const fsReaddirSyncStub = sinon.stub(fs, 'readdirSync').returns([
279+
{ name: `endpoint-${windowHash}-abc123.txt`, isFile: () => true },
280+
{ name: `endpoint-otherhash-def456.txt`, isFile: () => true },
281+
{ name: 'somedir', isFile: () => false },
282+
] as any);
296283
const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync');
297284

298285
// Act
299286
await registerNoConfigDebug(context.object.environmentVariableCollection, context.object.extensionPath);
300287

301-
// Assert
302-
sinon.assert.calledWith(fsExistsSyncStub, endpointFolderPath);
303-
sinon.assert.calledWith(fsUnlinkSyncStub, path.join(endpointFolderPath, 'old.txt'));
288+
// Assert - only files matching this window hash should be deleted
289+
sinon.assert.called(fsReaddirSyncStub);
290+
sinon.assert.calledOnce(fsUnlinkSyncStub);
304291

305292
// Cleanup
306293
fsExistsSyncStub.restore();

0 commit comments

Comments
 (0)