Skip to content

Commit c6dacbc

Browse files
committed
no config debugging: Allow concurrent debug sessions
Closes #613 and driveby fix of #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).
1 parent d7b9643 commit c6dacbc

File tree

6 files changed

+61
-26
lines changed

6 files changed

+61
-26
lines changed
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#! /bin/bash
22
# Bash script
3-
export DEBUGPY_ADAPTER_ENDPOINTS=$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
4-
python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $@
3+
endpoint_dir="$VSCODE_DEBUGPY_ADAPTER_ENDPOINTS"
4+
endpoint_file="$(mktemp -p "$endpoint_dir" endpoint-XXXXXX.txt)"
5+
export DEBUGPY_ADAPTER_ENDPOINTS="$endpoint_file"
6+
python3 "$BUNDLED_DEBUGPY_PATH" --listen 0 --wait-for-client "$@"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
@echo off
22
:: Bat script
3-
set DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%
3+
set "DEBUGPY_ADAPTER_ENDPOINTS=%VSCODE_DEBUGPY_ADAPTER_ENDPOINTS%\endpoint-%RANDOM%%RANDOM%.txt"
44
python %BUNDLED_DEBUGPY_PATH% --listen 0 --wait-for-client %*
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# Fish script
2-
set -x DEBUGPY_ADAPTER_ENDPOINTS $VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
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
35
python3 $BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $argv

bundled/scripts/noConfigScripts/debugpy.ps1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# PowerShell script
2-
$env:DEBUGPY_ADAPTER_ENDPOINTS = $env:VSCODE_DEBUGPY_ADAPTER_ENDPOINTS
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
5+
36
$os = [System.Environment]::OSVersion.Platform
47
if ($os -eq [System.PlatformID]::Win32NT) {
58
python $env:BUNDLED_DEBUGPY_PATH --listen 0 --wait-for-client $args

src/extension/noConfigDebugInit.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DebugSessionOptions,
99
Disposable,
1010
GlobalEnvironmentVariableCollection,
11+
env,
1112
l10n,
1213
RelativePattern,
1314
workspace,
@@ -39,7 +40,7 @@ export async function registerNoConfigDebug(
3940
const collection = envVarCollection;
4041

4142
// create a temp directory for the noConfigDebugAdapterEndpoints
42-
// file path format: extPath/.noConfigDebugAdapterEndpoints/endpoint-stableWorkspaceHash.txt
43+
// folder path format: extPath/.noConfigDebugAdapterEndpoints/<stableWorkspaceHash>
4344
let workspaceString = workspace.workspaceFile?.fsPath;
4445
if (!workspaceString) {
4546
workspaceString = workspace.workspaceFolders?.map((e) => e.uri.fsPath).join(';');
@@ -49,21 +50,26 @@ export async function registerNoConfigDebug(
4950
return Promise.resolve(new Disposable(() => {}));
5051
}
5152

52-
// create a stable hash for the workspace folder, reduce terminal variable churn
53+
// create a stable hash for the workspace folder and VS Code window, reduce terminal variable churn
5354
const hash = crypto.createHash('sha256');
5455
hash.update(workspaceString.toString());
56+
hash.update(env.sessionId);
5557
const stableWorkspaceHash = hash.digest('hex').slice(0, 16);
5658

5759
const tempDirPath = path.join(extPath, '.noConfigDebugAdapterEndpoints');
58-
const tempFilePath = path.join(tempDirPath, `endpoint-${stableWorkspaceHash}.txt`);
60+
const endpointFolderPath = path.join(tempDirPath, stableWorkspaceHash);
5961

6062
// create the temp directory if it doesn't exist
61-
if (!fs.existsSync(tempDirPath)) {
62-
fs.mkdirSync(tempDirPath, { recursive: true });
63+
if (!fs.existsSync(endpointFolderPath)) {
64+
fs.mkdirSync(endpointFolderPath, { recursive: true });
6365
} else {
64-
// remove endpoint file in the temp directory if it exists
65-
if (fs.existsSync(tempFilePath)) {
66-
fs.unlinkSync(tempFilePath);
66+
// clean out any existing endpoint files in the folder
67+
const entries = fs.readdirSync(endpointFolderPath, { withFileTypes: true });
68+
for (const entry of entries) {
69+
if (entry.isFile()) {
70+
const entryPath = path.join(endpointFolderPath, entry.name.toString());
71+
fs.unlinkSync(entryPath);
72+
}
6773
}
6874
}
6975
// clear the env var collection to remove any existing env vars
@@ -73,7 +79,7 @@ export async function registerNoConfigDebug(
7379
collection.replace('PYDEVD_DISABLE_FILE_VALIDATION', '1');
7480

7581
// Add env vars for VSCODE_DEBUGPY_ADAPTER_ENDPOINTS, BUNDLED_DEBUGPY_PATH, and PATH
76-
collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', tempFilePath);
82+
collection.replace('VSCODE_DEBUGPY_ADAPTER_ENDPOINTS', endpointFolderPath);
7783

7884
const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
7985
const pathSeparator = process.platform === 'win32' ? ';' : ':';
@@ -93,7 +99,7 @@ export async function registerNoConfigDebug(
9399
);
94100

95101
// create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written
96-
const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(tempDirPath, '**/*.txt'));
102+
const fileSystemWatcher = createFileSystemWatcher(new RelativePattern(endpointFolderPath, '**/*.txt'));
97103
const fileCreationEvent = fileSystemWatcher.onDidCreate(async (uri) => {
98104
sendTelemetryEvent(EventName.DEBUG_SESSION_START, undefined, {
99105
trigger: 'noConfig' as TriggerType,

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

Lines changed: 33 additions & 11 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, workspace } from 'vscode';
9+
import { DebugConfiguration, DebugSessionOptions, RelativePattern, Uri, env, workspace } from 'vscode';
1010
import * as utils from '../../extension/utils';
1111
import { assert } from 'console';
1212
import * as fs from 'fs';
@@ -22,6 +22,9 @@ 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;
2528

2629
const testDataDir = path.join(__dirname, 'testData');
2730
const testFilePath = path.join(testDataDir, 'debuggerAdapterEndpoint.txt');
@@ -34,19 +37,28 @@ suite('setup for no-config debug scenario', function () {
3437
noConfigScriptsDir = path.join(context.object.extensionPath, 'bundled/scripts/noConfigScripts');
3538
bundledDebugPath = path.join(context.object.extensionPath, 'bundled/libs/debugpy');
3639

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

42-
workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(os.tmpdir()) }]);
48+
workspaceUriStub = sinon.stub(workspace, 'workspaceFolders').value([{ uri: Uri.parse(workspacePath) }]);
49+
50+
const hash = crypto.createHash('sha256');
51+
hash.update(workspacePath.toString());
52+
hash.update('test-session');
53+
stableWorkspaceHash = hash.digest('hex').slice(0, 16);
4354
} catch (error) {
4455
console.error('Error in setup:', error);
4556
}
4657
});
4758
teardown(() => {
4859
sinon.restore();
4960
workspaceUriStub.restore();
61+
sessionIdStub.restore();
5062
});
5163

5264
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 () {
5971
.setup((x) => x.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
6072
.callback((key, value) => {
6173
if (key === DEBUGPY_ADAPTER_ENDPOINTS) {
62-
assert(value.includes('endpoint-'));
74+
assert(
75+
value ===
76+
path.join(context.object.extensionPath, '.noConfigDebugAdapterEndpoints', stableWorkspaceHash),
77+
);
6378
} else if (key === BUNDLED_DEBUGPY_PATH) {
6479
assert(value === bundledDebugPath);
6580
} else if (key === 'PYDEVD_DISABLE_FILE_VALIDATION') {
@@ -194,7 +209,7 @@ suite('setup for no-config debug scenario', function () {
194209
// Assert
195210
sinon.assert.calledOnce(createFileSystemWatcherFunct);
196211
const expectedPattern = new RelativePattern(
197-
path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints'),
212+
path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash),
198213
'**/*.txt',
199214
);
200215
sinon.assert.calledWith(createFileSystemWatcherFunct, expectedPattern);
@@ -261,26 +276,33 @@ suite('setup for no-config debug scenario', function () {
261276
sinon.assert.calledWith(debugStub, undefined, expectedConfig, optionsExpected);
262277
});
263278

264-
test('should check if tempFilePath exists when debuggerAdapterEndpointFolder exists', async () => {
279+
test('should clear existing endpoint files when debuggerAdapterEndpointFolder exists', async () => {
265280
// Arrange
266281
const environmentVariableCollectionMock = TypeMoq.Mock.ofType<any>();
267282
context.setup((c) => c.environmentVariableCollection).returns(() => environmentVariableCollectionMock.object);
268283

269-
const fsExistsSyncStub = sinon.stub(fs, 'existsSync').returns(true);
284+
const endpointFolderPath = path.join(os.tmpdir(), '.noConfigDebugAdapterEndpoints', stableWorkspaceHash);
285+
const fsExistsSyncStub = sinon.stub(fs, 'existsSync').callsFake((p) => p === endpointFolderPath);
286+
const fakeDirent = { isFile: () => true, name: Buffer.from('old.txt') } as unknown as fs.Dirent<Buffer>;
287+
const fsReaddirSyncStub = sinon
288+
.stub(fs, 'readdirSync')
289+
.callsFake((dirPath: fs.PathLike, options?: any) => {
290+
assert(dirPath === endpointFolderPath);
291+
assert(options?.withFileTypes === true);
292+
return [fakeDirent] as unknown as fs.Dirent<Buffer>[];
293+
});
270294
const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync');
271295

272296
// Act
273297
await registerNoConfigDebug(context.object.environmentVariableCollection, context.object.extensionPath);
274298

275299
// Assert
276-
sinon.assert.calledWith(
277-
fsExistsSyncStub,
278-
sinon.match((value: any) => value.includes('endpoint-')),
279-
);
280-
sinon.assert.calledOnce(fsUnlinkSyncStub);
300+
sinon.assert.calledWith(fsExistsSyncStub, endpointFolderPath);
301+
sinon.assert.calledWith(fsUnlinkSyncStub, path.join(endpointFolderPath, 'old.txt'));
281302

282303
// Cleanup
283304
fsExistsSyncStub.restore();
305+
fsReaddirSyncStub.restore();
284306
fsUnlinkSyncStub.restore();
285307
});
286308
});

0 commit comments

Comments
 (0)