Skip to content

Commit 3e40ce2

Browse files
authored
Add support for remote debugging with experimental debugger (#1230)
* Fixes #1229 * Fixes #1265
1 parent 60ff772 commit 3e40ce2

File tree

13 files changed

+677
-78
lines changed

13 files changed

+677
-78
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"coverage": true
1515
},
1616
"typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
17-
"tslint.enable": true,
17+
"tslint.enable": true, // We will run our own linting in gulp (& git commit hooks), else tslint extension just complains about unmodified files
1818
"python.linting.enabled": false,
1919
"python.unitTest.promptToConfigure": false,
2020
"python.workspaceSymbols.enabled": false,

news/1 Enhancements/1229.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add prelimnary support for remote debugging using the experimental debugger.
2+
Attach to a Python program started using the command `python -m ptvsd --server --port 9091 --file pythonFile.py`

package.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,19 @@
874874
"Pyramid"
875875
]
876876
}
877+
},
878+
{
879+
"label": "Python Experimental: Attach",
880+
"description": "%python.snippet.launch.attach.description%",
881+
"body": {
882+
"name": "Attach (Remote Debug)",
883+
"type": "pythonExperimental",
884+
"request": "attach",
885+
"localRoot": "^\"\\${workspaceFolder}\"",
886+
"remoteRoot": "^\"\\${workspaceFolder}\"",
887+
"port": 3000,
888+
"host": "localhost"
889+
}
877890
}
878891
],
879892
"configurationAttributes": {
@@ -963,6 +976,53 @@
963976
"default": false
964977
}
965978
}
979+
},
980+
"attach": {
981+
"required": [
982+
"port",
983+
"remoteRoot"
984+
],
985+
"properties": {
986+
"localRoot": {
987+
"type": "string",
988+
"description": "Local source root that corrresponds to the 'remoteRoot'.",
989+
"default": "${workspaceFolder}"
990+
},
991+
"remoteRoot": {
992+
"type": "string",
993+
"description": "The source root of the remote host.",
994+
"default": ""
995+
},
996+
"port": {
997+
"type": "number",
998+
"description": "Debug port to attach",
999+
"default": 0
1000+
},
1001+
"host": {
1002+
"type": "string",
1003+
"description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).",
1004+
"default": "localhost"
1005+
},
1006+
"debugOptions": {
1007+
"type": "array",
1008+
"description": "Advanced options, view read me for further details.",
1009+
"items": {
1010+
"type": "string",
1011+
"enum": [
1012+
"RedirectOutput",
1013+
"DebugStdLib",
1014+
"Django",
1015+
"Jinja"
1016+
]
1017+
},
1018+
"default": []
1019+
},
1020+
"logToFile": {
1021+
"type": "boolean",
1022+
"description": "Enable logging of debugger events to a log file.",
1023+
"default": false
1024+
}
1025+
}
9661026
}
9671027
},
9681028
"initialConfigurations": [
@@ -973,6 +1033,15 @@
9731033
"program": "${file}",
9741034
"console": "integratedTerminal"
9751035
},
1036+
{
1037+
"name": "Python Experimental: Attach",
1038+
"type": "pythonExperimental",
1039+
"request": "pythonExperimental",
1040+
"localRoot": "${workspaceFolder}",
1041+
"remoteRoot": "${workspaceFolder}",
1042+
"port": 3000,
1043+
"host": "localhost"
1044+
},
9761045
{
9771046
"name": "Python Experimental: Django",
9781047
"type": "pythonExperimental",

src/client/debugger/Common/Contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum
7272
}
7373

7474
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
75+
type?: DebuggerType;
7576
/** An absolute path to local directory with source. */
7677
localRoot: string;
7778
remoteRoot: string;

src/client/debugger/DebugClients/RemoteDebugClient.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import { DebugSession } from 'vscode-debugadapter';
22
import { AttachRequestArguments, IPythonProcess } from '../Common/Contracts';
33
import { BaseDebugServer } from '../DebugServers/BaseDebugServer';
44
import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer';
5+
import { RemoteDebugServerV2 } from '../DebugServers/RemoteDebugServerv2';
56
import { DebugClient, DebugType } from './DebugClient';
67

78
export class RemoteDebugClient extends DebugClient<AttachRequestArguments> {
8-
private pythonProcess: IPythonProcess;
9+
private pythonProcess?: IPythonProcess;
910
private debugServer?: BaseDebugServer;
1011
// tslint:disable-next-line:no-any
11-
constructor(args: any, debugSession: DebugSession) {
12+
constructor(args: AttachRequestArguments, debugSession: DebugSession) {
1213
super(args, debugSession);
1314
}
1415

1516
public CreateDebugServer(pythonProcess?: IPythonProcess): BaseDebugServer {
16-
this.pythonProcess = pythonProcess!;
17-
this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args);
17+
if (this.args.type === 'pythonExperimental') {
18+
// tslint:disable-next-line:no-any
19+
this.debugServer = new RemoteDebugServerV2(this.debugSession, undefined as any, this.args);
20+
} else {
21+
this.pythonProcess = pythonProcess!;
22+
this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args);
23+
}
1824
return this.debugServer!;
1925
}
2026
public get DebugType(): DebugType {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { connect, Socket } from 'net';
7+
import { DebugSession } from 'vscode-debugadapter';
8+
import { AttachRequestArguments, IDebugServer, IPythonProcess } from '../Common/Contracts';
9+
import { BaseDebugServer } from './BaseDebugServer';
10+
11+
export class RemoteDebugServerV2 extends BaseDebugServer {
12+
private args: AttachRequestArguments;
13+
private socket?: Socket;
14+
constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) {
15+
super(debugSession, pythonProcess);
16+
this.args = args;
17+
}
18+
19+
public Stop() {
20+
if (this.socket) {
21+
this.socket.destroy();
22+
}
23+
}
24+
public Start(): Promise<IDebugServer> {
25+
return new Promise<IDebugServer>((resolve, reject) => {
26+
const port = this.args.port!;
27+
const options = { port };
28+
if (typeof this.args.host === 'string' && this.args.host.length > 0) {
29+
// tslint:disable-next-line:no-any
30+
(<any>options).host = this.args.host;
31+
}
32+
try {
33+
let connected = false;
34+
const socket = connect(options, () => {
35+
connected = true;
36+
this.socket = socket;
37+
this.clientSocket.resolve(socket);
38+
resolve(options);
39+
});
40+
socket.on('error', ex => {
41+
if (connected) {
42+
return;
43+
}
44+
reject(ex);
45+
});
46+
} catch (ex) {
47+
reject(ex);
48+
}
49+
});
50+
}
51+
}

src/client/debugger/configProviders/baseProvider.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,55 @@ import { PythonLanguage } from '../../common/constants';
1111
import { IFileSystem, IPlatformService } from '../../common/platform/types';
1212
import { IConfigurationService } from '../../common/types';
1313
import { IServiceContainer } from '../../ioc/types';
14-
import { DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts';
14+
import { AttachRequestArguments, DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts';
1515

1616
// tslint:disable:no-invalid-template-strings
1717

18-
export type PythonDebugConfiguration = DebugConfiguration & LaunchRequestArguments;
18+
export type PythonLaunchDebugConfiguration = DebugConfiguration & LaunchRequestArguments;
19+
export type PythonAttachDebugConfiguration = DebugConfiguration & AttachRequestArguments;
1920

2021
@injectable()
2122
export abstract class BaseConfigurationProvider implements DebugConfigurationProvider {
2223
constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { }
2324
public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult<DebugConfiguration> {
24-
const config = debugConfiguration as PythonDebugConfiguration;
25-
const numberOfSettings = Object.keys(config);
26-
const workspaceFolder = this.getWorkspaceFolder(folder, config);
25+
const workspaceFolder = this.getWorkspaceFolder(folder);
2726

28-
if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) {
29-
const defaultProgram = this.getProgram(config);
27+
if (debugConfiguration.request === 'attach') {
28+
this.provideAttachDefaults(workspaceFolder, debugConfiguration as PythonAttachDebugConfiguration);
29+
} else {
30+
const config = debugConfiguration as PythonLaunchDebugConfiguration;
31+
const numberOfSettings = Object.keys(config);
3032

31-
config.name = 'Launch';
32-
config.type = this.debugType;
33-
config.request = 'launch';
34-
config.program = defaultProgram ? defaultProgram : '';
35-
config.env = {};
36-
}
33+
if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) {
34+
const defaultProgram = this.getProgram();
35+
36+
config.name = 'Launch';
37+
config.type = this.debugType;
38+
config.request = 'launch';
39+
config.program = defaultProgram ? defaultProgram : '';
40+
config.env = {};
41+
}
3742

38-
this.provideDefaults(workspaceFolder, config);
39-
return config;
43+
this.provideLaunchDefaults(workspaceFolder, config);
44+
}
45+
return debugConfiguration;
46+
}
47+
protected provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): void {
48+
if (!Array.isArray(debugConfiguration.debugOptions)) {
49+
debugConfiguration.debugOptions = [];
50+
}
51+
// Always redirect output.
52+
if (debugConfiguration.debugOptions.indexOf(DebugOptions.RedirectOutput) === -1) {
53+
debugConfiguration.debugOptions.push(DebugOptions.RedirectOutput);
54+
}
55+
if (!debugConfiguration.host) {
56+
debugConfiguration.host = 'localhost';
57+
}
58+
if (!debugConfiguration.localRoot && workspaceFolder) {
59+
debugConfiguration.localRoot = workspaceFolder.fsPath;
60+
}
4061
}
41-
protected provideDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void {
62+
protected provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void {
4263
this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration);
4364
if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) {
4465
debugConfiguration.cwd = workspaceFolder.fsPath;
@@ -75,11 +96,11 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro
7596
}
7697
}
7798
}
78-
private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonDebugConfiguration): Uri | undefined {
99+
private getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined {
79100
if (folder) {
80101
return folder.uri;
81102
}
82-
const program = this.getProgram(config);
103+
const program = this.getProgram();
83104
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
84105
if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) {
85106
return program ? Uri.file(path.dirname(program)) : undefined;
@@ -94,14 +115,14 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro
94115
}
95116
}
96117
}
97-
private getProgram(config: PythonDebugConfiguration): string | undefined {
118+
private getProgram(): string | undefined {
98119
const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager);
99120
const editor = documentManager.activeTextEditor;
100121
if (editor && editor.document.languageId === PythonLanguage.language) {
101122
return editor.document.fileName;
102123
}
103124
}
104-
private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void {
125+
private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void {
105126
if (!debugConfiguration) {
106127
return;
107128
}

src/client/debugger/configProviders/pythonV2Provider.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { Uri } from 'vscode';
88
import { IPlatformService } from '../../common/platform/types';
99
import { IServiceContainer } from '../../ioc/types';
1010
import { DebugOptions } from '../Common/Contracts';
11-
import { BaseConfigurationProvider, PythonDebugConfiguration } from './baseProvider';
11+
import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider';
1212

1313
@injectable()
1414
export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider {
1515
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
1616
super('pythonExperimental', serviceContainer);
1717
}
18-
protected provideDefaults(workspaceFolder: Uri, debugConfiguration: PythonDebugConfiguration): void {
19-
super.provideDefaults(workspaceFolder, debugConfiguration);
18+
protected provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): void {
19+
super.provideLaunchDefaults(workspaceFolder, debugConfiguration);
2020

2121
debugConfiguration.stopOnEntry = false;
2222
debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : [];
@@ -30,4 +30,14 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide
3030
debugConfiguration.debugOptions.push(DebugOptions.Jinja);
3131
}
3232
}
33+
protected provideAttachDefaults(workspaceFolder: Uri, debugConfiguration: PythonAttachDebugConfiguration): void {
34+
super.provideAttachDefaults(workspaceFolder, debugConfiguration);
35+
36+
debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : [];
37+
38+
// Add PTVSD specific flags.
39+
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
40+
debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase);
41+
}
42+
}
3343
}

0 commit comments

Comments
 (0)