Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mapping of local and remote paths in remote debugging #1300

Merged
merged 10 commits into from
Apr 6, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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.",
10 changes: 5 additions & 5 deletions src/client/common/net/socket/socketServer.ts
Original file line number Diff line number Diff line change
@@ -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;
}

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

export interface IDebugServer {
15 changes: 8 additions & 7 deletions src/client/debugger/DebugServers/RemoteDebugServerv2.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -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);
}
14 changes: 12 additions & 2 deletions src/client/debugger/configProviders/pythonV2Provider.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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;
66 changes: 45 additions & 21 deletions src/test/debugger/attach.ptvsd.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
@@ -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({
@@ -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,
@@ -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 }],
@@ -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
@@ -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 {
@@ -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(() => {
@@ -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);
14 changes: 10 additions & 4 deletions src/test/initialize.ts
Original file line number Diff line number Diff line change
@@ -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 {