diff --git a/src/extension/common/stringUtils.ts b/src/extension/common/stringUtils.ts index d5ce9742..fd9fcc6c 100644 --- a/src/extension/common/stringUtils.ts +++ b/src/extension/common/stringUtils.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { getOSType, OSType } from './platform'; + /** * Replaces all instances of a substring with a new substring. */ @@ -58,5 +60,12 @@ export function fileToCommandArgumentForPythonExt(source: string): string { if (!source) { return source; } - return toCommandArgumentForPythonExt(source).replace(/\\/g, '/'); + + let result = toCommandArgumentForPythonExt(source); + + if (getOSType() !== OSType.Windows) { + result = result.replace(/\\/g, '/'); + } + + return result; } diff --git a/src/extension/debugger/adapter/factory.ts b/src/extension/debugger/adapter/factory.ts index 5befc986..2a8090ed 100644 --- a/src/extension/debugger/adapter/factory.ts +++ b/src/extension/debugger/adapter/factory.ts @@ -80,6 +80,13 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac let executable = command.shift() ?? 'python'; + // Always ensure interpreter/command is quoted if necessary. Previously this was + // only done in the debugAdapterPath branch which meant that in the common case + // (using the built‑in adapter path) an interpreter path containing spaces would + // be passed unquoted, resulting in a fork/spawn failure on Windows. See bug + // report for details. + executable = fileToCommandArgumentForPythonExt(executable); + // "logToFile" is not handled directly by the adapter - instead, we need to pass // the corresponding CLI switch when spawning it. const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : []; @@ -87,7 +94,6 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac if (configuration.debugAdapterPath !== undefined) { const args = command.concat([configuration.debugAdapterPath, ...logArgs]); traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); - executable = fileToCommandArgumentForPythonExt(executable); return new DebugAdapterExecutable(executable, args); } diff --git a/src/test/unittest/adapter/factory.unit.test.ts b/src/test/unittest/adapter/factory.unit.test.ts index 029ac48a..9c3a9e91 100644 --- a/src/test/unittest/adapter/factory.unit.test.ts +++ b/src/test/unittest/adapter/factory.unit.test.ts @@ -304,7 +304,23 @@ suite('Debugging - Adapter Factory', () => { assert.deepStrictEqual(descriptor, debugExecutable); }); - test('Add quotes to interpreter path with spaces', async () => { + test('Add quotes to interpreter path with spaces (default adapter path)', async () => { + const session = createSession({}); + const interpreterPathSpaces = 'path/to/python interpreter with spaces'; + const interpreterPathSpacesQuoted = `"${interpreterPathSpaces}"`; + const debugExecutable = new DebugAdapterExecutable(interpreterPathSpacesQuoted, [debugAdapterPath]); + + getInterpreterDetailsStub.resolves({ path: [interpreterPathSpaces] }); + const interpreterSpacePath: PythonEnvironment = createInterpreter(interpreterPathSpaces, '3.7.4-test'); + // Add architecture for completeness. + (interpreterSpacePath as any).architecture = Architecture.Unknown; + resolveEnvironmentStub.withArgs(interpreterPathSpaces).resolves(interpreterSpacePath); + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Add quotes to interpreter path with spaces when debugAdapterPath is specified', async () => { const customAdapterPath = 'custom/debug/adapter/customAdapterPath'; const session = createSession({ debugAdapterPath: customAdapterPath }); const interpreterPathSpaces = 'path/to/python interpreter with spaces'; diff --git a/src/test/unittest/adapter/remoteLaunchers.unit.test.ts b/src/test/unittest/adapter/remoteLaunchers.unit.test.ts index f681ee08..17e3ff17 100644 --- a/src/test/unittest/adapter/remoteLaunchers.unit.test.ts +++ b/src/test/unittest/adapter/remoteLaunchers.unit.test.ts @@ -8,18 +8,23 @@ import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../../extension/common/constants'; import '../../../extension/common/promiseUtils'; import * as launchers from '../../../extension/debugger/adapter/remoteLaunchers'; +import { getOSType, OSType } from '../../../extension/common/platform'; + +function osExpectedPath(windowsPath: string, unixPath: string): string { + return getOSType() === OSType.Windows ? windowsPath : unixPath; +} suite('External debugpy Debugger Launcher', () => { [ { testName: 'When path to debugpy does not contains spaces', path: path.join('path', 'to', 'debugpy'), - expectedPath: 'path/to/debugpy', + expectedPath: osExpectedPath('path\\to\\debugpy', 'path/to/debugpy'), }, { testName: 'When path to debugpy contains spaces', path: path.join('path', 'to', 'debugpy', 'with spaces'), - expectedPath: '"path/to/debugpy/with spaces"', + expectedPath: osExpectedPath('"path\\to\\debugpy\\with spaces"', '"path/to/debugpy/with spaces"'), }, ].forEach((testParams) => { suite(testParams.testName, async () => {