Skip to content

Commit 0ab1384

Browse files
authored
Expose API to get command for launching debugger in remote debug mode (#3399)
* Add API * Add tests * Resolve code review comments
1 parent ae40119 commit 0ab1384

File tree

12 files changed

+140
-67
lines changed

12 files changed

+140
-67
lines changed

news/1 Enhancements/3121.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expose an API that can be used by other extensions to interact with the Python Extension.

src/client/api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,41 @@
33

44
'use strict';
55

6+
import { RemoteDebuggerLauncherScriptProvider } from './debugger/debugAdapter/DebugClients/launcherProvider';
7+
8+
/*
9+
* Do not introduce any breaking changes to this API.
10+
* This is the public API for other extensions to interact with this extension.
11+
*/
12+
613
export interface IExtensionApi {
14+
/**
15+
* Promise indicating whether all parts of the extension have completed loading or not.
16+
* @type {Promise<void>}
17+
* @memberof IExtensionApi
18+
*/
719
ready: Promise<void>;
20+
debug: {
21+
/**
22+
* Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging.
23+
* Users can append another array of strings of what they want to execute along with relevant arguments to Python.
24+
* E.g `['/Users/..../pythonVSCode/pythonFiles/experimental/ptvsd_launcher.py', '--host', 'localhost', '--port', '57039', '--wait']`
25+
* @param {string} host
26+
* @param {number} port
27+
* @param {boolean} [waitUntilDebuggerAttaches=true]
28+
* @returns {Promise<string[]>}
29+
*/
30+
getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>;
31+
};
32+
}
33+
34+
export function buildApi(ready: Promise<void>) {
35+
return {
36+
ready,
37+
debug: {
38+
async getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean = true): Promise<string[]> {
39+
return new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host, port, waitUntilDebuggerAttaches });
40+
}
41+
}
42+
};
843
}

src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DebugSession } from 'vscode-debugadapter';
22
import { AttachRequestArguments, LaunchRequestArguments } from '../../types';
3-
import { IDebugLauncherScriptProvider } from '../types';
3+
import { ILocalDebugLauncherScriptProvider } from '../types';
44
import { DebugClient } from './DebugClient';
55
import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider';
66
import { LocalDebugClient } from './LocalDebugClient';
@@ -9,7 +9,7 @@ import { NonDebugClientV2 } from './nonDebugClientV2';
99
import { RemoteDebugClient } from './RemoteDebugClient';
1010

1111
export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> {
12-
let launchScriptProvider: IDebugLauncherScriptProvider;
12+
let launchScriptProvider: ILocalDebugLauncherScriptProvider;
1313
let debugClientClass: typeof LocalDebugClient;
1414
if (launchRequestOptions.noDebug === true) {
1515
launchScriptProvider = new NoDebugLauncherScriptProvider();

src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,15 @@ import { CurrentProcess } from '../../../common/process/currentProcess';
88
import { noop } from '../../../common/utils/misc';
99
import { EnvironmentVariablesService } from '../../../common/variables/environment';
1010
import { IServiceContainer } from '../../../ioc/types';
11-
import { DebugOptions, LaunchRequestArguments } from '../../types';
11+
import { LaunchRequestArguments } from '../../types';
1212
import { IDebugServer } from '../Common/Contracts';
1313
import { IS_WINDOWS } from '../Common/Utils';
1414
import { BaseDebugServer } from '../DebugServers/BaseDebugServer';
1515
import { LocalDebugServerV2 } from '../DebugServers/LocalDebugServerV2';
16-
import { IDebugLauncherScriptProvider } from '../types';
16+
import { ILocalDebugLauncherScriptProvider } from '../types';
1717
import { DebugClient, DebugType } from './DebugClient';
1818
import { DebugClientHelper } from './helper';
1919

20-
const VALID_DEBUG_OPTIONS = [
21-
'RedirectOutput',
22-
'DebugStdLib',
23-
'StopOnEntry',
24-
'ShowReturnValue',
25-
'BreakOnSystemExitZero',
26-
'DjangoDebugging',
27-
'Django'];
28-
2920
enum DebugServerStatus {
3021
Unknown = 1,
3122
Running = 2,
@@ -44,7 +35,7 @@ export class LocalDebugClient extends DebugClient<LaunchRequestArguments> {
4435
}
4536
return DebugServerStatus.Unknown;
4637
}
47-
constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, protected launcherScriptProvider: IDebugLauncherScriptProvider) {
38+
constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, protected launcherScriptProvider: ILocalDebugLauncherScriptProvider) {
4839
super(args, debugSession);
4940
}
5041

@@ -143,18 +134,7 @@ export class LocalDebugClient extends DebugClient<LaunchRequestArguments> {
143134

144135
// tslint:disable-next-line:member-ordering
145136
protected buildDebugArguments(cwd: string, debugPort: number): string[] {
146-
const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath();
147-
const vsDebugOptions: string[] = [DebugOptions.RedirectOutput];
148-
if (Array.isArray(this.args.debugOptions)) {
149-
this.args.debugOptions.filter(opt => VALID_DEBUG_OPTIONS.indexOf(opt) >= 0)
150-
.forEach(item => vsDebugOptions.push(item));
151-
}
152-
const djangoIndex = vsDebugOptions.indexOf(DebugOptions.Django);
153-
// PTVSD expects the string `DjangoDebugging`
154-
if (djangoIndex >= 0) {
155-
vsDebugOptions[djangoIndex] = 'DjangoDebugging';
156-
}
157-
return [ptVSToolsFilePath, cwd, debugPort.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14', vsDebugOptions.join(',')];
137+
throw new Error('Not Implemented');
158138
}
159139
// tslint:disable-next-line:member-ordering
160140
protected buildStandardArguments() {

src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@
77

88
import * as path from 'path';
99
import { EXTENSION_ROOT_DIR } from '../../../common/constants';
10-
import { IDebugLauncherScriptProvider } from '../types';
10+
import { IDebugLauncherScriptProvider, IRemoteDebugLauncherScriptProvider, LocalDebugOptions, RemoteDebugOptions } from '../types';
1111

12-
export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider {
13-
public getLauncherFilePath(): string {
14-
return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py');
12+
const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py');
13+
export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider<LocalDebugOptions> {
14+
public getLauncherArgs(options: LocalDebugOptions): string[] {
15+
return [script, '--nodebug', '--client', '--host', options.host, '--port', options.port.toString()];
1516
}
1617
}
1718

18-
export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider {
19-
public getLauncherFilePath(): string {
20-
return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py');
19+
export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider<LocalDebugOptions> {
20+
public getLauncherArgs(options: LocalDebugOptions): string[] {
21+
return [script, '--client', '--host', options.host, '--port', options.port.toString()];
22+
}
23+
}
24+
25+
export class RemoteDebuggerLauncherScriptProvider implements IRemoteDebugLauncherScriptProvider {
26+
public getLauncherArgs(options: RemoteDebugOptions): string[] {
27+
const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait'] : [];
28+
return [script, '--host', options.host, '--port', options.port.toString()].concat(waitArgs);
2129
}
2230
}

src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,15 @@
55

66
import { DebugSession } from 'vscode-debugadapter';
77
import { LaunchRequestArguments } from '../../types';
8-
import { IDebugLauncherScriptProvider } from '../types';
8+
import { ILocalDebugLauncherScriptProvider } from '../types';
99
import { LocalDebugClient } from './LocalDebugClient';
1010

1111
export class LocalDebugClientV2 extends LocalDebugClient {
12-
constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) {
12+
constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) {
1313
super(args, debugSession, canLaunchTerminal, launcherScriptProvider);
1414
}
1515
protected buildDebugArguments(cwd: string, debugPort: number): string[] {
16-
const launcher = this.launcherScriptProvider.getLauncherFilePath();
17-
const additionalPtvsdArgs: string[] = [];
18-
if (this.args.noDebug) {
19-
additionalPtvsdArgs.push('--nodebug');
20-
}
21-
return [launcher, ...additionalPtvsdArgs, '--client', '--host', 'localhost', '--port', debugPort.toString()];
16+
return this.launcherScriptProvider.getLauncherArgs({ host: 'localhost', port: debugPort });
2217
}
2318
protected buildStandardArguments() {
2419
const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : [];

src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
import { ChildProcess } from 'child_process';
77
import { DebugSession } from 'vscode-debugadapter';
88
import { LaunchRequestArguments } from '../../types';
9-
import { IDebugLauncherScriptProvider } from '../types';
9+
import { ILocalDebugLauncherScriptProvider } from '../types';
1010
import { DebugType } from './DebugClient';
1111
import { LocalDebugClientV2 } from './localDebugClientV2';
1212

1313
export class NonDebugClientV2 extends LocalDebugClientV2 {
14-
constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) {
14+
constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) {
1515
super(args, debugSession, canLaunchTerminal, launcherScriptProvider);
1616
}
1717

src/client/debugger/debugAdapter/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,18 @@ import { Disposable } from 'vscode';
99
import { Logger } from 'vscode-debugadapter';
1010
import { Message } from 'vscode-debugadapter/lib/messages';
1111

12-
export interface IDebugLauncherScriptProvider {
13-
getLauncherFilePath(): string;
12+
export type LocalDebugOptions = { port: number; host: string };
13+
export type RemoteDebugOptions = LocalDebugOptions & { waitUntilDebuggerAttaches: boolean };
14+
15+
export interface IDebugLauncherScriptProvider<T> {
16+
getLauncherArgs(options: T): string[];
17+
}
18+
19+
export interface ILocalDebugLauncherScriptProvider extends IDebugLauncherScriptProvider<LocalDebugOptions> {
20+
getLauncherArgs(options: LocalDebugOptions): string[];
21+
}
22+
23+
export interface IRemoteDebugLauncherScriptProvider extends IDebugLauncherScriptProvider<RemoteDebugOptions> {
1424
}
1525

1626
export const IProtocolParser = Symbol('IProtocolParser');

src/client/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Container } from 'inversify';
1414
import { CodeActionKind, debug, DebugConfigurationProvider, Disposable, ExtensionContext, extensions, IndentAction, languages, Memento, OutputChannel, window } from 'vscode';
1515
import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry';
1616
import { IExtensionActivationService } from './activation/types';
17-
import { IExtensionApi } from './api';
17+
import { buildApi, IExtensionApi } from './api';
1818
import { registerTypes as appRegisterTypes } from './application/serviceRegistry';
1919
import { IApplicationDiagnostics } from './application/types';
2020
import { DebugService } from './common/application/debugService';
@@ -164,7 +164,7 @@ export async function activate(context: ExtensionContext): Promise<IExtensionApi
164164
durations.endActivateTime = stopWatch.elapsedTime;
165165
activationDeferred.resolve();
166166

167-
const api = { ready: activationDeferred.promise };
167+
const api = buildApi(activationDeferred.promise);
168168
// In test environment return the DI Container.
169169
if (isTestExecution()) {
170170
// tslint:disable-next-line:no-any
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { expect } from 'chai';
7+
import * as fs from 'fs-extra';
8+
import * as path from 'path';
9+
import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants';
10+
import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider, RemoteDebuggerLauncherScriptProvider } from '../../../../client/debugger/debugAdapter/DebugClients/launcherProvider';
11+
12+
const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py');
13+
14+
suite('Debugger - Launcher Script Provider', () => {
15+
test('Ensure launcher script exists', async () => {
16+
expect(await fs.pathExists(expectedPath)).to.be.deep.equal(true, 'Debugger launcher script does not exist');
17+
});
18+
test('Test debug launcher args', async () => {
19+
const args = new DebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 });
20+
const expectedArgs = [expectedPath, '--client', '--host', 'something', '--port', '1234'];
21+
expect(args).to.be.deep.equal(expectedArgs);
22+
});
23+
test('Test non-debug launcher args', async () => {
24+
const args = new NoDebugLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 });
25+
const expectedArgs = [expectedPath, '--nodebug', '--client', '--host', 'something', '--port', '1234'];
26+
expect(args).to.be.deep.equal(expectedArgs);
27+
});
28+
test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => {
29+
const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: false });
30+
const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234'];
31+
expect(args).to.be.deep.equal(expectedArgs);
32+
});
33+
test('Test remote debug launcher args (and wait for debugger to attach)', async () => {
34+
const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: true });
35+
const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234', '--wait'];
36+
expect(args).to.be.deep.equal(expectedArgs);
37+
});
38+
});

src/test/debugger/launcherScriptProvider.unit.test.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/test/extension.unit.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// tslint:disable:no-any
7+
8+
import { expect } from 'chai';
9+
import * as path from 'path';
10+
import { buildApi } from '../client/api';
11+
import { EXTENSION_ROOT_DIR } from '../client/common/constants';
12+
13+
const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd_launcher.py');
14+
15+
suite('Extension API Debugger', () => {
16+
test('Test debug launcher args (no-wait)', async () => {
17+
const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, false);
18+
const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234'];
19+
expect(args).to.be.deep.equal(expectedArgs);
20+
});
21+
test('Test debug launcher args (wait)', async () => {
22+
const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, true);
23+
const expectedArgs = [expectedPath, '--host', 'something', '--port', '1234', '--wait'];
24+
expect(args).to.be.deep.equal(expectedArgs);
25+
});
26+
});

0 commit comments

Comments
 (0)