From b3776eb68ee1671bab93de7ebe2cf960b2c9d571 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Apr 2018 19:11:26 -0700 Subject: [PATCH] Add support for mapping of local and remote paths in remote debugging (#1300) Fixes #1289 --- package.json | 25 +++++++ src/client/common/net/socket/socketServer.ts | 10 +-- src/client/debugger/Common/Contracts.ts | 2 + .../DebugServers/RemoteDebugServerv2.ts | 15 +++-- .../configProviders/pythonV2Provider.ts | 14 +++- src/test/autocomplete/base.test.ts | 10 ++- src/test/debugger/attach.ptvsd.test.ts | 66 +++++++++++++------ src/test/debugger/capabilities.test.ts | 34 +++++----- src/test/initialize.ts | 14 ++-- 9 files changed, 131 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 13d8ab4506b0..26d728dd09ba 100644 --- a/package.json +++ b/package.json @@ -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.", diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts index 24e7b2713740..74099fd7cffe 100644 --- a/src/client/common/net/socket/socketServer.ts +++ b/src/client/common/net/socket/socketServer.ts @@ -28,22 +28,22 @@ export class SocketServer extends EventEmitter implements ISocketServer { this.socketServer = undefined; } - public Start(options: { port?: number, host?: string } = {}): Promise { + public Start(options: { port?: number; host?: string } = {}): Promise { const def = createDeferred(); 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; } diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 30011ef88e6a..85bc8d7f4a2c 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -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 { diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts index be400c12c5ea..ff6ff06f9a40 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -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); } diff --git a/src/client/debugger/configProviders/pythonV2Provider.ts b/src/client/debugger/configProviders/pythonV2Provider.ts index d356901f45ab..4c6b74d7faac 100644 --- a/src/client/debugger/configProviders/pythonV2Provider.ts +++ b/src/client/debugger/configProviders/pythonV2Provider.ts @@ -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).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).isWindows && isLocalHost) { debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase); } + + if (!debugConfiguration.pathMappings) { + debugConfiguration.pathMappings = []; + } + debugConfiguration.pathMappings!.push({ + localRoot: debugConfiguration.localRoot, + remoteRoot: debugConfiguration.remoteRoot + }); } } diff --git a/src/test/autocomplete/base.test.ts b/src/test/autocomplete/base.test.ts index 5219e4ababd9..f3289e359dfd 100644 --- a/src/test/autocomplete/base.test.ts +++ b/src/test/autocomplete/base.test.ts @@ -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; diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index 76ccd4944034..87d7e77de17d 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -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(); + platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows); + const serviceContainer = TypeMoq.Mock.ofType(); + 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); }); }); diff --git a/src/test/debugger/capabilities.test.ts b/src/test/debugger/capabilities.test.ts index 0bc4005ee512..5f7f6f53c159 100644 --- a/src/test/debugger/capabilities.test.ts +++ b/src/test/debugger/capabilities.test.ts @@ -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,6 +31,8 @@ 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; @@ -36,6 +40,7 @@ suite('Debugging - Capabilities', () => { 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); diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 5914d38185e9..0a428faa4dbd 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -42,11 +42,17 @@ export async function initializeTest(): Promise { // Dispose any cached python settings (used only in test env). PythonSettings.dispose(); } - export async function closeActiveWindows(): Promise { - return new Promise((resolve, reject) => vscode.commands.executeCommand('workbench.action.closeAllEditors') - // tslint:disable-next-line:no-unnecessary-callback-wrapper - .then(() => resolve(), reject)); + return new Promise((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 {