Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions bundled/scripts/noConfigScripts/debugpy
Original file line number Diff line number Diff line change
@@ -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 "$@"
3 changes: 2 additions & 1 deletion bundled/scripts/noConfigScripts/debugpy.bat
Original file line number Diff line number Diff line change
@@ -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 %*
3 changes: 2 additions & 1 deletion bundled/scripts/noConfigScripts/debugpy.fish
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion bundled/scripts/noConfigScripts/debugpy.ps1
Original file line number Diff line number Diff line change
@@ -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
Expand Down
45 changes: 20 additions & 25 deletions src/extension/noConfigDebugInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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' ? ';' : ':';
Expand All @@ -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 <script.py>`, 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,
Expand Down
27 changes: 19 additions & 8 deletions src/test/unittest/noConfigDebugInit.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand All @@ -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);
}
Expand All @@ -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') {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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<any>();
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();
});
});
Expand Down