diff --git a/news/1 Enhancements/3121.md b/news/1 Enhancements/3121.md new file mode 100644 index 000000000000..93c4f87f71a4 --- /dev/null +++ b/news/1 Enhancements/3121.md @@ -0,0 +1 @@ +Expose an API that can be used by other extensions to interact with the Python Extension. diff --git a/src/client/api.ts b/src/client/api.ts index b729af647830..eb1f661ecb67 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -3,6 +3,41 @@ 'use strict'; +import { RemoteDebuggerLauncherScriptProvider } from './debugger/debugAdapter/DebugClients/launcherProvider'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. +*/ + export interface IExtensionApi { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise} + * @memberof IExtensionApi + */ ready: Promise; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/pythonFiles/experimental/ptvsd_launcher.py', '--host', 'localhost', '--port', '57039', '--wait']` + * @param {string} host + * @param {number} port + * @param {boolean} [waitUntilDebuggerAttaches=true] + * @returns {Promise} + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + }; +} + +export function buildApi(ready: Promise) { + return { + ready, + debug: { + async getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean = true): Promise { + return new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host, port, waitUntilDebuggerAttaches }); + } + } + }; } diff --git a/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts b/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts index afe2853c0daf..8a3f5dc665db 100644 --- a/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts +++ b/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts @@ -1,6 +1,6 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { IDebugLauncherScriptProvider } from '../types'; +import { ILocalDebugLauncherScriptProvider } from '../types'; import { DebugClient } from './DebugClient'; import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; import { LocalDebugClient } from './LocalDebugClient'; @@ -9,7 +9,7 @@ import { NonDebugClientV2 } from './nonDebugClientV2'; import { RemoteDebugClient } from './RemoteDebugClient'; export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> { - let launchScriptProvider: IDebugLauncherScriptProvider; + let launchScriptProvider: ILocalDebugLauncherScriptProvider; let debugClientClass: typeof LocalDebugClient; if (launchRequestOptions.noDebug === true) { launchScriptProvider = new NoDebugLauncherScriptProvider(); diff --git a/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts b/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts index 4c724647356f..c73ec730c36a 100644 --- a/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts @@ -8,24 +8,15 @@ import { CurrentProcess } from '../../../common/process/currentProcess'; import { noop } from '../../../common/utils/misc'; import { EnvironmentVariablesService } from '../../../common/variables/environment'; import { IServiceContainer } from '../../../ioc/types'; -import { DebugOptions, LaunchRequestArguments } from '../../types'; +import { LaunchRequestArguments } from '../../types'; import { IDebugServer } from '../Common/Contracts'; import { IS_WINDOWS } from '../Common/Utils'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { LocalDebugServerV2 } from '../DebugServers/LocalDebugServerV2'; -import { IDebugLauncherScriptProvider } from '../types'; +import { ILocalDebugLauncherScriptProvider } from '../types'; import { DebugClient, DebugType } from './DebugClient'; import { DebugClientHelper } from './helper'; -const VALID_DEBUG_OPTIONS = [ - 'RedirectOutput', - 'DebugStdLib', - 'StopOnEntry', - 'ShowReturnValue', - 'BreakOnSystemExitZero', - 'DjangoDebugging', - 'Django']; - enum DebugServerStatus { Unknown = 1, Running = 2, @@ -44,7 +35,7 @@ export class LocalDebugClient extends DebugClient { } return DebugServerStatus.Unknown; } - constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, protected launcherScriptProvider: IDebugLauncherScriptProvider) { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, protected launcherScriptProvider: ILocalDebugLauncherScriptProvider) { super(args, debugSession); } @@ -143,18 +134,7 @@ export class LocalDebugClient extends DebugClient { // tslint:disable-next-line:member-ordering protected buildDebugArguments(cwd: string, debugPort: number): string[] { - const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath(); - const vsDebugOptions: string[] = [DebugOptions.RedirectOutput]; - if (Array.isArray(this.args.debugOptions)) { - this.args.debugOptions.filter(opt => VALID_DEBUG_OPTIONS.indexOf(opt) >= 0) - .forEach(item => vsDebugOptions.push(item)); - } - const djangoIndex = vsDebugOptions.indexOf(DebugOptions.Django); - // PTVSD expects the string `DjangoDebugging` - if (djangoIndex >= 0) { - vsDebugOptions[djangoIndex] = 'DjangoDebugging'; - } - return [ptVSToolsFilePath, cwd, debugPort.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14', vsDebugOptions.join(',')]; + throw new Error('Not Implemented'); } // tslint:disable-next-line:member-ordering protected buildStandardArguments() { diff --git a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts b/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts index ef0a9ac58b99..f2e8bf629dd2 100644 --- a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts +++ b/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts @@ -7,16 +7,24 @@ import * as path from 'path'; import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { IDebugLauncherScriptProvider } from '../types'; +import { IDebugLauncherScriptProvider, IRemoteDebugLauncherScriptProvider, LocalDebugOptions, RemoteDebugOptions } from '../types'; -export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider { - public getLauncherFilePath(): string { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); +const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); +export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider { + public getLauncherArgs(options: LocalDebugOptions): string[] { + return [script, '--nodebug', '--client', '--host', options.host, '--port', options.port.toString()]; } } -export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider { - public getLauncherFilePath(): string { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); +export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider { + public getLauncherArgs(options: LocalDebugOptions): string[] { + return [script, '--client', '--host', options.host, '--port', options.port.toString()]; + } +} + +export class RemoteDebuggerLauncherScriptProvider implements IRemoteDebugLauncherScriptProvider { + public getLauncherArgs(options: RemoteDebugOptions): string[] { + const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait'] : []; + return [script, '--host', options.host, '--port', options.port.toString()].concat(waitArgs); } } diff --git a/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts b/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts index ac0f40ffa508..7b11acd60252 100644 --- a/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts +++ b/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts @@ -5,20 +5,15 @@ import { DebugSession } from 'vscode-debugadapter'; import { LaunchRequestArguments } from '../../types'; -import { IDebugLauncherScriptProvider } from '../types'; +import { ILocalDebugLauncherScriptProvider } from '../types'; import { LocalDebugClient } from './LocalDebugClient'; export class LocalDebugClientV2 extends LocalDebugClient { - constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) { super(args, debugSession, canLaunchTerminal, launcherScriptProvider); } protected buildDebugArguments(cwd: string, debugPort: number): string[] { - const launcher = this.launcherScriptProvider.getLauncherFilePath(); - const additionalPtvsdArgs: string[] = []; - if (this.args.noDebug) { - additionalPtvsdArgs.push('--nodebug'); - } - return [launcher, ...additionalPtvsdArgs, '--client', '--host', 'localhost', '--port', debugPort.toString()]; + return this.launcherScriptProvider.getLauncherArgs({ host: 'localhost', port: debugPort }); } protected buildStandardArguments() { const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; diff --git a/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts b/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts index 43d7ba677276..5ac1c05eb2f0 100644 --- a/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts +++ b/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts @@ -6,12 +6,12 @@ import { ChildProcess } from 'child_process'; import { DebugSession } from 'vscode-debugadapter'; import { LaunchRequestArguments } from '../../types'; -import { IDebugLauncherScriptProvider } from '../types'; +import { ILocalDebugLauncherScriptProvider } from '../types'; import { DebugType } from './DebugClient'; import { LocalDebugClientV2 } from './localDebugClientV2'; export class NonDebugClientV2 extends LocalDebugClientV2 { - constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) { super(args, debugSession, canLaunchTerminal, launcherScriptProvider); } diff --git a/src/client/debugger/debugAdapter/types.ts b/src/client/debugger/debugAdapter/types.ts index 4bb931ca8a91..3e6be34d84df 100644 --- a/src/client/debugger/debugAdapter/types.ts +++ b/src/client/debugger/debugAdapter/types.ts @@ -9,8 +9,18 @@ import { Disposable } from 'vscode'; import { Logger } from 'vscode-debugadapter'; import { Message } from 'vscode-debugadapter/lib/messages'; -export interface IDebugLauncherScriptProvider { - getLauncherFilePath(): string; +export type LocalDebugOptions = { port: number; host: string }; +export type RemoteDebugOptions = LocalDebugOptions & { waitUntilDebuggerAttaches: boolean }; + +export interface IDebugLauncherScriptProvider { + getLauncherArgs(options: T): string[]; +} + +export interface ILocalDebugLauncherScriptProvider extends IDebugLauncherScriptProvider { + getLauncherArgs(options: LocalDebugOptions): string[]; +} + +export interface IRemoteDebugLauncherScriptProvider extends IDebugLauncherScriptProvider { } export const IProtocolParser = Symbol('IProtocolParser'); diff --git a/src/client/extension.ts b/src/client/extension.ts index 88f7cd240083..a7d000666195 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -14,7 +14,7 @@ import { Container } from 'inversify'; import { CodeActionKind, debug, DebugConfigurationProvider, Disposable, ExtensionContext, extensions, IndentAction, languages, Memento, OutputChannel, window } from 'vscode'; import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; import { IExtensionActivationService } from './activation/types'; -import { IExtensionApi } from './api'; +import { buildApi, IExtensionApi } from './api'; import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; import { IApplicationDiagnostics } from './application/types'; import { DebugService } from './common/application/debugService'; @@ -164,7 +164,7 @@ export async function activate(context: ExtensionContext): Promise { + test('Ensure launcher script exists', async () => { + expect(await fs.pathExists(expectedPath)).to.be.deep.equal(true, 'Debugger launcher script does not exist'); + }); + test('Test debug launcher args', async () => { + const args = new DebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 }); + const expectedArgs = [expectedPath, '--client', '--host', 'something', '--port', '1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test non-debug launcher args', async () => { + const args = new NoDebugLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 }); + const expectedArgs = [expectedPath, '--nodebug', '--client', '--host', 'something', '--port', '1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => { + const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: false }); + const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test remote debug launcher args (and wait for debugger to attach)', async () => { + const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: true }); + const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234', '--wait']; + expect(args).to.be.deep.equal(expectedArgs); + }); +}); diff --git a/src/test/debugger/launcherScriptProvider.unit.test.ts b/src/test/debugger/launcherScriptProvider.unit.test.ts deleted file mode 100644 index 324964201264..000000000000 --- a/src/test/debugger/launcherScriptProvider.unit.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/debugAdapter/DebugClients/launcherProvider'; - -suite('Debugger - Launcher Script Provider', () => { - test('Ensure debugger gets the launcher from PythonTools directory', () => { - const launcherPath = new DebuggerLauncherScriptProvider().getLauncherFilePath(); - const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); - expect(launcherPath).to.be.equal(expectedPath); - }); - test('Ensure debugger gets the non debug launcher from PythonTools directory', () => { - const launcherPath = new NoDebugLauncherScriptProvider().getLauncherFilePath(); - const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); - expect(launcherPath).to.be.equal(expectedPath); - }); -}); diff --git a/src/test/extension.unit.test.ts b/src/test/extension.unit.test.ts new file mode 100644 index 000000000000..7cfdd4d7bdd6 --- /dev/null +++ b/src/test/extension.unit.test.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { buildApi } from '../client/api'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; + +const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); + +suite('Extension API Debugger', () => { + test('Test debug launcher args (no-wait)', async () => { + const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, false); + const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test debug launcher args (wait)', async () => { + const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, true); + const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234', '--wait']; + expect(args).to.be.deep.equal(expectedArgs); + }); +});