Skip to content

Commit 94cc8b1

Browse files
authored
Add support for mapping of local and remote paths in remote debugging (#1300)
Fixes #1289
1 parent db6030d commit 94cc8b1

File tree

9 files changed

+131
-59
lines changed

9 files changed

+131
-59
lines changed

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,31 @@
10171017
},
10181018
"default": []
10191019
},
1020+
"pathMappings": {
1021+
"type": "array",
1022+
"label": "Additional path mappings.",
1023+
"items": {
1024+
"type": "object",
1025+
"label": "Path mapping",
1026+
"required": [
1027+
"localRoot",
1028+
"remoteRoot"
1029+
],
1030+
"properties": {
1031+
"localRoot": {
1032+
"type": "string",
1033+
"label": "Local source root.",
1034+
"default": ""
1035+
},
1036+
"remoteRoot": {
1037+
"type": "string",
1038+
"label": "Remote source root.",
1039+
"default": ""
1040+
}
1041+
}
1042+
},
1043+
"default": []
1044+
},
10201045
"logToFile": {
10211046
"type": "boolean",
10221047
"description": "Enable logging of debugger events to a log file.",

src/client/common/net/socket/socketServer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ export class SocketServer extends EventEmitter implements ISocketServer {
2828
this.socketServer = undefined;
2929
}
3030

31-
public Start(options: { port?: number, host?: string } = {}): Promise<number> {
31+
public Start(options: { port?: number; host?: string } = {}): Promise<number> {
3232
const def = createDeferred<number>();
3333
this.socketServer = net.createServer(this.connectionListener.bind(this));
3434

3535
const port = typeof options.port === 'number' ? options.port! : 0;
3636
const host = typeof options.host === 'string' ? options.host! : 'localhost';
37-
this.socketServer!.listen({ port, host }, () => {
38-
def.resolve(this.socketServer!.address().port);
39-
});
40-
4137
this.socketServer!.on('error', ex => {
4238
console.error('Error in Socket Server', ex);
4339
const msg = `Failed to start the socket server. (Error: ${ex.message})`;
4440

4541
def.reject(msg);
4642
});
43+
this.socketServer!.listen({ port, host }, () => {
44+
def.resolve(this.socketServer!.address().port);
45+
});
46+
4747
return def.promise;
4848
}
4949

src/client/debugger/Common/Contracts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum
8080
host?: string;
8181
secret?: string;
8282
logToFile?: boolean;
83+
pathMappings?: { localRoot: string; remoteRoot: string }[];
84+
debugOptions?: DebugOptions[];
8385
}
8486

8587
export interface IDebugServer {

src/client/debugger/DebugServers/RemoteDebugServerv2.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
'use strict';
55

6-
import { connect, Socket } from 'net';
6+
import { Socket } from 'net';
77
import { DebugSession } from 'vscode-debugadapter';
88
import { AttachRequestArguments, IDebugServer, IPythonProcess } from '../Common/Contracts';
99
import { BaseDebugServer } from './BaseDebugServer';
@@ -31,18 +31,19 @@ export class RemoteDebugServerV2 extends BaseDebugServer {
3131
}
3232
try {
3333
let connected = false;
34-
const socket = connect(options, () => {
35-
connected = true;
36-
this.socket = socket;
37-
this.clientSocket.resolve(socket);
38-
resolve(options);
39-
});
34+
const socket = new Socket();
4035
socket.on('error', ex => {
4136
if (connected) {
4237
return;
4338
}
4439
reject(ex);
4540
});
41+
socket.connect(options, () => {
42+
connected = true;
43+
this.socket = socket;
44+
this.clientSocket.resolve(socket);
45+
resolve(options);
46+
});
4647
} catch (ex) {
4748
reject(ex);
4849
}

src/client/debugger/configProviders/pythonV2Provider.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide
3535

3636
debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : [];
3737

38-
// Add PTVSD specific flags.
39-
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
38+
// We'll need paths to be fixed only in the case where local and remote hosts are the same
39+
// I.e. only if hostName === 'localhost' or '127.0.0.1' or ''
40+
const isLocalHost = !debugConfiguration.host || debugConfiguration.host === 'localhost' || debugConfiguration.host === '127.0.0.1';
41+
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows && isLocalHost) {
4042
debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase);
4143
}
44+
45+
if (!debugConfiguration.pathMappings) {
46+
debugConfiguration.pathMappings = [];
47+
}
48+
debugConfiguration.pathMappings!.push({
49+
localRoot: debugConfiguration.localRoot,
50+
remoteRoot: debugConfiguration.remoteRoot
51+
});
4252
}
4353
}

src/test/autocomplete/base.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,17 @@ const fileEncodingUsed = path.join(autoCompPath, 'five.py');
2222
const fileSuppress = path.join(autoCompPath, 'suppress.py');
2323

2424
// tslint:disable-next-line:max-func-body-length
25-
suite('Autocomplete', () => {
25+
suite('Autocomplete', function () {
26+
// Attempt to fix #1301
27+
// tslint:disable-next-line:no-invalid-this
28+
this.timeout(60000);
2629
let isPython2: boolean;
2730
let ioc: UnitTestIocContainer;
2831

29-
suiteSetup(async () => {
32+
suiteSetup(async function () {
33+
// Attempt to fix #1301
34+
// tslint:disable-next-line:no-invalid-this
35+
this.timeout(60000);
3036
await initialize();
3137
initializeDI();
3238
isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2;

src/test/debugger/attach.ptvsd.test.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,33 @@
88
import { ChildProcess, spawn } from 'child_process';
99
import * as getFreePort from 'get-port';
1010
import * as path from 'path';
11+
import * as TypeMoq from 'typemoq';
12+
import { DebugConfiguration, Uri } from 'vscode';
1113
import { DebugClient } from 'vscode-debugadapter-testsupport';
1214
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
1315
import '../../client/common/extensions';
16+
import { IS_WINDOWS } from '../../client/common/platform/constants';
17+
import { IPlatformService } from '../../client/common/platform/types';
18+
import { PythonV2DebugConfigurationProvider } from '../../client/debugger';
1419
import { PTVSD_PATH } from '../../client/debugger/Common/constants';
15-
import { DebugOptions } from '../../client/debugger/Common/Contracts';
20+
import { AttachRequestArguments, DebugOptions } from '../../client/debugger/Common/Contracts';
21+
import { IServiceContainer } from '../../client/ioc/types';
1622
import { sleep } from '../common';
17-
import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
23+
import { initialize, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
1824
import { continueDebugging, createDebugAdapter } from './utils';
1925

2026
const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py');
2127

2228
suite('Attach Debugger - Experimental', () => {
2329
let debugClient: DebugClient;
24-
let procToKill: ChildProcess;
30+
let proc: ChildProcess;
2531
suiteSetup(initialize);
2632

2733
setup(async function () {
2834
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
2935
this.skip();
3036
}
37+
this.timeout(30000);
3138
const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd');
3239
debugClient = await createDebugAdapter(coverageDirectory);
3340
});
@@ -37,27 +44,23 @@ suite('Attach Debugger - Experimental', () => {
3744
try {
3845
await debugClient.stop().catch(() => { });
3946
} catch (ex) { }
40-
if (procToKill) {
47+
if (proc) {
4148
try {
42-
procToKill.kill();
49+
proc.kill();
4350
} catch { }
4451
}
4552
});
46-
test('Confirm we are able to attach to a running program', async function () {
47-
this.timeout(20000);
48-
// Lets skip this test on AppVeyor (very flaky on AppVeyor).
49-
if (IS_APPVEYOR) {
50-
return;
51-
}
52-
53+
async function testAttachingToRemoteProcess(localRoot: string, remoteRoot: string, isLocalHostWindows: boolean) {
54+
const localHostPathSeparator = isLocalHostWindows ? '\\' : '/';
5355
const port = await getFreePort({ host: 'localhost', port: 3000 });
54-
const customEnv = { ...process.env };
56+
const env = { ...process.env };
5557

5658
// Set the path for PTVSD to be picked up.
5759
// tslint:disable-next-line:no-string-literal
58-
customEnv['PYTHONPATH'] = PTVSD_PATH;
60+
env['PYTHONPATH'] = PTVSD_PATH;
5961
const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()];
60-
procToKill = spawn('python', pythonArgs, { env: customEnv, cwd: path.dirname(fileToDebug) });
62+
proc = spawn('python', pythonArgs, { env: env, cwd: path.dirname(fileToDebug) });
63+
await sleep(3000);
6164

6265
// Send initialize, attach
6366
const initializePromise = debugClient.initializeRequest({
@@ -69,15 +72,25 @@ suite('Attach Debugger - Experimental', () => {
6972
supportsVariableType: true,
7073
supportsVariablePaging: true
7174
});
72-
const attachPromise = debugClient.attachRequest({
73-
localRoot: path.dirname(fileToDebug),
74-
remoteRoot: path.dirname(fileToDebug),
75+
const options: AttachRequestArguments & DebugConfiguration = {
76+
name: 'attach',
77+
request: 'attach',
78+
localRoot,
79+
remoteRoot,
7580
type: 'pythonExperimental',
7681
port: port,
7782
host: 'localhost',
78-
logToFile: false,
83+
logToFile: true,
7984
debugOptions: [DebugOptions.RedirectOutput]
80-
});
85+
};
86+
const platformService = TypeMoq.Mock.ofType<IPlatformService>();
87+
platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows);
88+
const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
89+
serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object);
90+
const configProvider = new PythonV2DebugConfigurationProvider(serviceContainer.object);
91+
92+
await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options);
93+
const attachPromise = debugClient.attachRequest(options);
8194

8295
await Promise.all([
8396
initializePromise,
@@ -90,7 +103,9 @@ suite('Attach Debugger - Experimental', () => {
90103
const stdOutPromise = debugClient.assertOutput('stdout', 'this is stdout');
91104
const stdErrPromise = debugClient.assertOutput('stderr', 'this is stderr');
92105

93-
const breakpointLocation = { path: fileToDebug, column: 1, line: 12 };
106+
// Don't use path utils, as we're building the paths manually (mimic windows paths on unix test servers and vice versa).
107+
const localFileName = `${localRoot}${localHostPathSeparator}${path.basename(fileToDebug)}`;
108+
const breakpointLocation = { path: localFileName, column: 1, line: 12 };
94109
const breakpointPromise = debugClient.setBreakpointsRequest({
95110
lines: [breakpointLocation.line],
96111
breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }],
@@ -111,5 +126,14 @@ suite('Attach Debugger - Experimental', () => {
111126
debugClient.waitForEvent('exited'),
112127
debugClient.waitForEvent('terminated')
113128
]);
129+
}
130+
test('Confirm we are able to attach to a running program', async () => {
131+
await testAttachingToRemoteProcess(path.dirname(fileToDebug), path.dirname(fileToDebug), IS_WINDOWS);
132+
});
133+
test('Confirm local and remote paths are translated', async () => {
134+
// If tests are running on windows, then treat debug client as a unix client and remote process as current OS.
135+
const isLocalHostWindows = !IS_WINDOWS;
136+
const localWorkspace = isLocalHostWindows ? 'C:\\Project\\src' : '/home/user/Desktop/project/src';
137+
await testAttachingToRemoteProcess(localWorkspace, path.dirname(fileToDebug), isLocalHostWindows);
114138
});
115139
});

src/test/debugger/capabilities.test.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@
88
import { expect } from 'chai';
99
import { ChildProcess, spawn } from 'child_process';
1010
import * as getFreePort from 'get-port';
11-
import { connect, Socket } from 'net';
11+
import { Socket } from 'net';
12+
import * as path from 'path';
1213
import { PassThrough } from 'stream';
1314
import { Message } from 'vscode-debugadapter/lib/messages';
1415
import { DebugProtocol } from 'vscode-debugprotocol';
16+
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
17+
import { sleep } from '../../client/common/core.utils';
1518
import { createDeferred } from '../../client/common/helpers';
1619
import { PTVSD_PATH } from '../../client/debugger/Common/constants';
1720
import { ProtocolParser } from '../../client/debugger/Common/protocolParser';
1821
import { ProtocolMessageWriter } from '../../client/debugger/Common/protocolWriter';
1922
import { PythonDebugger } from '../../client/debugger/mainV2';
20-
import { sleep } from '../common';
2123
import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
2224

2325
class Request extends Message implements DebugProtocol.InitializeRequest {
@@ -29,13 +31,16 @@ class Request extends Message implements DebugProtocol.InitializeRequest {
2931
}
3032
}
3133

34+
const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py');
35+
3236
suite('Debugging - Capabilities', () => {
3337
let disposables: { dispose?: Function; destroy?: Function }[];
3438
let proc: ChildProcess;
3539
setup(async function () {
3640
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
3741
this.skip();
3842
}
43+
this.timeout(30000);
3944
disposables = [];
4045
});
4146
teardown(() => {
@@ -72,24 +77,17 @@ suite('Debugging - Capabilities', () => {
7277
const expectedResponse = await expectedResponsePromise;
7378

7479
const host = 'localhost';
75-
const port = await getFreePort({ host });
80+
const port = await getFreePort({ host, port: 3000 });
7681
const env = { ...process.env };
7782
env.PYTHONPATH = PTVSD_PATH;
78-
proc = spawn('python', ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', 'someFile.py'], { cwd: __dirname, env });
79-
// Wait for the socket server to start.
80-
// Keep trying till we timeout.
81-
let socket: Socket | undefined;
82-
for (let index = 0; index < 1000; index += 1) {
83-
try {
84-
const connected = createDeferred();
85-
socket = connect({ port, host }, () => connected.resolve(socket));
86-
socket.on('error', connected.reject.bind(connected));
87-
await connected.promise;
88-
break;
89-
} catch {
90-
await sleep(500);
91-
}
92-
}
83+
proc = spawn('python', ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug], { cwd: path.dirname(fileToDebug), env });
84+
await sleep(3000);
85+
86+
const connected = createDeferred();
87+
const socket = new Socket();
88+
socket.on('error', connected.reject.bind(connected));
89+
socket.connect({ port, host }, () => connected.resolve(socket));
90+
await connected.promise;
9391
const protocolParser = new ProtocolParser();
9492
protocolParser.connect(socket!);
9593
disposables.push(protocolParser);

src/test/initialize.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,17 @@ export async function initializeTest(): Promise<any> {
4242
// Dispose any cached python settings (used only in test env).
4343
PythonSettings.dispose();
4444
}
45-
4645
export async function closeActiveWindows(): Promise<void> {
47-
return new Promise<void>((resolve, reject) => vscode.commands.executeCommand('workbench.action.closeAllEditors')
48-
// tslint:disable-next-line:no-unnecessary-callback-wrapper
49-
.then(() => resolve(), reject));
46+
return new Promise<void>((resolve, reject) => {
47+
vscode.commands.executeCommand('workbench.action.closeAllEditors')
48+
// tslint:disable-next-line:no-unnecessary-callback-wrapper
49+
.then(() => resolve(), reject);
50+
// Attempt to fix #1301.
51+
// Lets not waste too much time.
52+
setTimeout(() => {
53+
reject(new Error('Command \'workbench.action.closeAllEditors\' timedout'));
54+
}, 15000);
55+
});
5056
}
5157

5258
function getPythonPath(): string {

0 commit comments

Comments
 (0)