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
Show file tree
Hide file tree
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
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 @@ -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 {
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