Skip to content

Commit

Permalink
Add support for mapping of local and remote paths in remote debugging (
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored and Aman Agarwal committed Aug 30, 2018
1 parent 816f3ea commit b3776eb
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 59 deletions.
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,31 @@
},
"default": []
},
"pathMappings": {
"type": "array",
"label": "Additional path mappings.",
"items": {
"type": "object",
"label": "Path mapping",
"required": [
"localRoot",
"remoteRoot"
],
"properties": {
"localRoot": {
"type": "string",
"label": "Local source root.",
"default": ""
},
"remoteRoot": {
"type": "string",
"label": "Remote source root.",
"default": ""
}
}
},
"default": []
},
"logToFile": {
"type": "boolean",
"description": "Enable logging of debugger events to a log file.",
Expand Down
10 changes: 5 additions & 5 deletions src/client/common/net/socket/socketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ export class SocketServer extends EventEmitter implements ISocketServer {
this.socketServer = undefined;
}

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

const port = typeof options.port === 'number' ? options.port! : 0;
const host = typeof options.host === 'string' ? options.host! : 'localhost';
this.socketServer!.listen({ port, host }, () => {
def.resolve(this.socketServer!.address().port);
});

this.socketServer!.on('error', ex => {
console.error('Error in Socket Server', ex);
const msg = `Failed to start the socket server. (Error: ${ex.message})`;

def.reject(msg);
});
this.socketServer!.listen({ port, host }, () => {
def.resolve(this.socketServer!.address().port);
});

return def.promise;
}

Expand Down
2 changes: 2 additions & 0 deletions src/client/debugger/Common/Contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface AttachRequestArguments extends DebugProtocol.AttachRequestArgum
host?: string;
secret?: string;
logToFile?: boolean;
pathMappings?: { localRoot: string; remoteRoot: string }[];
debugOptions?: DebugOptions[];
}

export interface IDebugServer {
Expand Down
15 changes: 8 additions & 7 deletions src/client/debugger/DebugServers/RemoteDebugServerv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

'use strict';

import { connect, Socket } from 'net';
import { Socket } from 'net';
import { DebugSession } from 'vscode-debugadapter';
import { AttachRequestArguments, IDebugServer, IPythonProcess } from '../Common/Contracts';
import { BaseDebugServer } from './BaseDebugServer';
Expand Down Expand Up @@ -31,18 +31,19 @@ export class RemoteDebugServerV2 extends BaseDebugServer {
}
try {
let connected = false;
const socket = connect(options, () => {
connected = true;
this.socket = socket;
this.clientSocket.resolve(socket);
resolve(options);
});
const socket = new Socket();
socket.on('error', ex => {
if (connected) {
return;
}
reject(ex);
});
socket.connect(options, () => {
connected = true;
this.socket = socket;
this.clientSocket.resolve(socket);
resolve(options);
});
} catch (ex) {
reject(ex);
}
Expand Down
14 changes: 12 additions & 2 deletions src/client/debugger/configProviders/pythonV2Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide

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

// Add PTVSD specific flags.
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
// We'll need paths to be fixed only in the case where local and remote hosts are the same
// I.e. only if hostName === 'localhost' or '127.0.0.1' or ''
const isLocalHost = !debugConfiguration.host || debugConfiguration.host === 'localhost' || debugConfiguration.host === '127.0.0.1';
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows && isLocalHost) {
debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase);
}

if (!debugConfiguration.pathMappings) {
debugConfiguration.pathMappings = [];
}
debugConfiguration.pathMappings!.push({
localRoot: debugConfiguration.localRoot,
remoteRoot: debugConfiguration.remoteRoot
});
}
}
10 changes: 8 additions & 2 deletions src/test/autocomplete/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@ const fileEncodingUsed = path.join(autoCompPath, 'five.py');
const fileSuppress = path.join(autoCompPath, 'suppress.py');

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

suiteSetup(async () => {
suiteSetup(async function () {
// Attempt to fix #1301
// tslint:disable-next-line:no-invalid-this
this.timeout(60000);
await initialize();
initializeDI();
isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri) === 2;
Expand Down
66 changes: 45 additions & 21 deletions src/test/debugger/attach.ptvsd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,33 @@
import { ChildProcess, spawn } from 'child_process';
import * as getFreePort from 'get-port';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import { DebugConfiguration, Uri } from 'vscode';
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
import '../../client/common/extensions';
import { IS_WINDOWS } from '../../client/common/platform/constants';
import { IPlatformService } from '../../client/common/platform/types';
import { PythonV2DebugConfigurationProvider } from '../../client/debugger';
import { PTVSD_PATH } from '../../client/debugger/Common/constants';
import { DebugOptions } from '../../client/debugger/Common/Contracts';
import { AttachRequestArguments, DebugOptions } from '../../client/debugger/Common/Contracts';
import { IServiceContainer } from '../../client/ioc/types';
import { sleep } from '../common';
import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
import { initialize, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';
import { continueDebugging, createDebugAdapter } from './utils';

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

suite('Attach Debugger - Experimental', () => {
let debugClient: DebugClient;
let procToKill: ChildProcess;
let proc: ChildProcess;
suiteSetup(initialize);

setup(async function () {
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
this.skip();
}
this.timeout(30000);
const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd');
debugClient = await createDebugAdapter(coverageDirectory);
});
Expand All @@ -37,27 +44,23 @@ suite('Attach Debugger - Experimental', () => {
try {
await debugClient.stop().catch(() => { });
} catch (ex) { }
if (procToKill) {
if (proc) {
try {
procToKill.kill();
proc.kill();
} catch { }
}
});
test('Confirm we are able to attach to a running program', async function () {
this.timeout(20000);
// Lets skip this test on AppVeyor (very flaky on AppVeyor).
if (IS_APPVEYOR) {
return;
}

async function testAttachingToRemoteProcess(localRoot: string, remoteRoot: string, isLocalHostWindows: boolean) {
const localHostPathSeparator = isLocalHostWindows ? '\\' : '/';
const port = await getFreePort({ host: 'localhost', port: 3000 });
const customEnv = { ...process.env };
const env = { ...process.env };

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

// Send initialize, attach
const initializePromise = debugClient.initializeRequest({
Expand All @@ -69,15 +72,25 @@ suite('Attach Debugger - Experimental', () => {
supportsVariableType: true,
supportsVariablePaging: true
});
const attachPromise = debugClient.attachRequest({
localRoot: path.dirname(fileToDebug),
remoteRoot: path.dirname(fileToDebug),
const options: AttachRequestArguments & DebugConfiguration = {
name: 'attach',
request: 'attach',
localRoot,
remoteRoot,
type: 'pythonExperimental',
port: port,
host: 'localhost',
logToFile: false,
logToFile: true,
debugOptions: [DebugOptions.RedirectOutput]
});
};
const platformService = TypeMoq.Mock.ofType<IPlatformService>();
platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows);
const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object);
const configProvider = new PythonV2DebugConfigurationProvider(serviceContainer.object);

await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options);
const attachPromise = debugClient.attachRequest(options);

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

const breakpointLocation = { path: fileToDebug, column: 1, line: 12 };
// Don't use path utils, as we're building the paths manually (mimic windows paths on unix test servers and vice versa).
const localFileName = `${localRoot}${localHostPathSeparator}${path.basename(fileToDebug)}`;
const breakpointLocation = { path: localFileName, column: 1, line: 12 };
const breakpointPromise = debugClient.setBreakpointsRequest({
lines: [breakpointLocation.line],
breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }],
Expand All @@ -111,5 +126,14 @@ suite('Attach Debugger - Experimental', () => {
debugClient.waitForEvent('exited'),
debugClient.waitForEvent('terminated')
]);
}
test('Confirm we are able to attach to a running program', async () => {
await testAttachingToRemoteProcess(path.dirname(fileToDebug), path.dirname(fileToDebug), IS_WINDOWS);
});
test('Confirm local and remote paths are translated', async () => {
// If tests are running on windows, then treat debug client as a unix client and remote process as current OS.
const isLocalHostWindows = !IS_WINDOWS;
const localWorkspace = isLocalHostWindows ? 'C:\\Project\\src' : '/home/user/Desktop/project/src';
await testAttachingToRemoteProcess(localWorkspace, path.dirname(fileToDebug), isLocalHostWindows);
});
});
34 changes: 16 additions & 18 deletions src/test/debugger/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@
import { expect } from 'chai';
import { ChildProcess, spawn } from 'child_process';
import * as getFreePort from 'get-port';
import { connect, Socket } from 'net';
import { Socket } from 'net';
import * as path from 'path';
import { PassThrough } from 'stream';
import { Message } from 'vscode-debugadapter/lib/messages';
import { DebugProtocol } from 'vscode-debugprotocol';
import { EXTENSION_ROOT_DIR } from '../../client/common/constants';
import { sleep } from '../../client/common/core.utils';
import { createDeferred } from '../../client/common/helpers';
import { PTVSD_PATH } from '../../client/debugger/Common/constants';
import { ProtocolParser } from '../../client/debugger/Common/protocolParser';
import { ProtocolMessageWriter } from '../../client/debugger/Common/protocolWriter';
import { PythonDebugger } from '../../client/debugger/mainV2';
import { sleep } from '../common';
import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize';

class Request extends Message implements DebugProtocol.InitializeRequest {
Expand All @@ -29,13 +31,16 @@ class Request extends Message implements DebugProtocol.InitializeRequest {
}
}

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

suite('Debugging - Capabilities', () => {
let disposables: { dispose?: Function; destroy?: Function }[];
let proc: ChildProcess;
setup(async function () {
if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) {
this.skip();
}
this.timeout(30000);
disposables = [];
});
teardown(() => {
Expand Down Expand Up @@ -72,24 +77,17 @@ suite('Debugging - Capabilities', () => {
const expectedResponse = await expectedResponsePromise;

const host = 'localhost';
const port = await getFreePort({ host });
const port = await getFreePort({ host, port: 3000 });
const env = { ...process.env };
env.PYTHONPATH = PTVSD_PATH;
proc = spawn('python', ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', 'someFile.py'], { cwd: __dirname, env });
// Wait for the socket server to start.
// Keep trying till we timeout.
let socket: Socket | undefined;
for (let index = 0; index < 1000; index += 1) {
try {
const connected = createDeferred();
socket = connect({ port, host }, () => connected.resolve(socket));
socket.on('error', connected.reject.bind(connected));
await connected.promise;
break;
} catch {
await sleep(500);
}
}
proc = spawn('python', ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug], { cwd: path.dirname(fileToDebug), env });
await sleep(3000);

const connected = createDeferred();
const socket = new Socket();
socket.on('error', connected.reject.bind(connected));
socket.connect({ port, host }, () => connected.resolve(socket));
await connected.promise;
const protocolParser = new ProtocolParser();
protocolParser.connect(socket!);
disposables.push(protocolParser);
Expand Down
14 changes: 10 additions & 4 deletions src/test/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ export async function initializeTest(): Promise<any> {
// Dispose any cached python settings (used only in test env).
PythonSettings.dispose();
}

export async function closeActiveWindows(): Promise<void> {
return new Promise<void>((resolve, reject) => vscode.commands.executeCommand('workbench.action.closeAllEditors')
// tslint:disable-next-line:no-unnecessary-callback-wrapper
.then(() => resolve(), reject));
return new Promise<void>((resolve, reject) => {
vscode.commands.executeCommand('workbench.action.closeAllEditors')
// tslint:disable-next-line:no-unnecessary-callback-wrapper
.then(() => resolve(), reject);
// Attempt to fix #1301.
// Lets not waste too much time.
setTimeout(() => {
reject(new Error('Command \'workbench.action.closeAllEditors\' timedout'));
}, 15000);
});
}

function getPythonPath(): string {
Expand Down

0 comments on commit b3776eb

Please sign in to comment.