From 2fd50a73c760d3ccceecdb6d531d24949c4b1bc6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 Mar 2018 13:31:51 -0700 Subject: [PATCH 01/20] split compilation for faster compilation on windows --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e359168fd3a0..204fcc045003 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ "coverage": true }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "tslint.enable": true, + "tslint.enable": false, // We will run our own linting in gulp (& git commit hooks), else tslint extension just complains about unmodified files "python.linting.enabled": false, "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, From b9d6c0b2b26af15698cbcd0595b71b83b916c046 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 28 Mar 2018 13:22:07 -0700 Subject: [PATCH 02/20] :sparkles: experimental debugger attach --- package.json | 47 +++++ src/client/debugger/Common/Contracts.ts | 1 + .../DebugClients/RemoteDebugClient.ts | 14 +- .../DebugServers/RemoteDebugServerv2.ts | 44 ++++ .../debugger/configProviders/baseProvider.ts | 37 +++- .../configProviders/pythonV2Provider.ts | 16 +- src/client/debugger/mainV2.ts | 88 +++++--- .../configProvider/provider.attach.test.ts | 197 ++++++++++++++++++ 8 files changed, 400 insertions(+), 44 deletions(-) create mode 100644 src/client/debugger/DebugServers/RemoteDebugServerv2.ts create mode 100644 src/test/debugger/configProvider/provider.attach.test.ts diff --git a/package.json b/package.json index 81128aa16bb6..78b7681f2c2c 100644 --- a/package.json +++ b/package.json @@ -963,6 +963,53 @@ "default": false } } + }, + "attach": { + "required": [ + "port", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "description": "Local source root that corrresponds to the 'remoteRoot'.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "description": "The source root of the remote host.", + "default": "" + }, + "port": { + "type": "number", + "description": "Debug port to attach", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", + "default": "localhost" + }, + "debugOptions": { + "type": "array", + "description": "Advanced options, view read me for further details.", + "items": { + "type": "string", + "enum": [ + "RedirectOutput", + "DebugStdLib", + "Django", + "Jinja" + ] + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + } + } } }, "initialConfigurations": [ diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 7828d5685d7c..09dd275528a6 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -72,6 +72,7 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { + type?: DebuggerType; /** An absolute path to local directory with source. */ localRoot: string; remoteRoot: string; diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts index 45aec00388a4..a7bee8e9f578 100644 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ b/src/client/debugger/DebugClients/RemoteDebugClient.ts @@ -2,19 +2,25 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, IPythonProcess } from '../Common/Contracts'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer'; +import { RemoteDebugServerV2 } from '../DebugServers/RemoteDebugServerv2'; import { DebugClient, DebugType } from './DebugClient'; export class RemoteDebugClient extends DebugClient { - private pythonProcess: IPythonProcess; + private pythonProcess?: IPythonProcess; private debugServer?: BaseDebugServer; // tslint:disable-next-line:no-any - constructor(args: any, debugSession: DebugSession) { + constructor(args: AttachRequestArguments, debugSession: DebugSession) { super(args, debugSession); } public CreateDebugServer(pythonProcess?: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess!; - this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args); + if (this.args.type === 'pythonExperimental') { + // tslint:disable-next-line:no-any + this.debugServer = new RemoteDebugServerV2(this.debugSession, undefined as any, this.args); + } else { + this.pythonProcess = pythonProcess!; + this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess!, this.args); + } return this.debugServer!; } public get DebugType(): DebugType { diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts new file mode 100644 index 000000000000..21b6b119e57c --- /dev/null +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template + +import { DebugSession } from "vscode-debugadapter"; +import { IPythonProcess, IDebugServer, AttachRequestArguments } from "../Common/Contracts"; +import { connect, Socket } from "net"; +import { BaseDebugServer } from "./BaseDebugServer"; + +export class RemoteDebugServerV2 extends BaseDebugServer { + private args: AttachRequestArguments; + private socket?: Socket; + constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { + super(debugSession, pythonProcess); + this.args = args; + } + + public Stop() { + if (this.socket) { + this.socket.destroy(); + } + } + public Start(): Promise { + return new Promise((resolve, reject) => { + let portNumber = this.args.port; + let options = { port: portNumber! }; + if (typeof this.args.host === "string" && this.args.host.length > 0) { + (options).host = this.args.host; + } + try { + const socket = connect(options, () => { + this.socket = socket; + this.clientSocket.resolve(socket); + resolve(options); + }); + } catch (ex) { + reject(ex); + } + }); + } +} diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index f7786eca9b70..53329f185a18 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -11,17 +11,18 @@ import { PythonLanguage } from '../../common/constants'; import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { IConfigurationService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; -import { DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; +import { AttachRequestArguments, DebuggerType, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; // tslint:disable:no-invalid-template-strings -export type PythonDebugConfiguration = DebugConfiguration & LaunchRequestArguments; +export type PythonLaunchDebugConfiguration = DebugConfiguration & LaunchRequestArguments; +export type PythonAttachDebugConfiguration = DebugConfiguration & AttachRequestArguments; @injectable() export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - const config = debugConfiguration as PythonDebugConfiguration; + const config = debugConfiguration as PythonLaunchDebugConfiguration; const numberOfSettings = Object.keys(config); const workspaceFolder = this.getWorkspaceFolder(folder, config); @@ -35,10 +36,30 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro config.env = {}; } - this.provideDefaults(workspaceFolder, config); + if (config.request === 'attach') { + // tslint:disable-next-line:no-any + this.provideAttachDefaults(workspaceFolder, config as any as PythonAttachDebugConfiguration); + } else { + this.provideLaunchDefaults(workspaceFolder, config); + } return config; } - protected provideDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + protected provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): void { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + // Always redirect output. + if (debugConfiguration.debugOptions.indexOf(DebugOptions.RedirectOutput) === -1) { + debugConfiguration.debugOptions.push(DebugOptions.RedirectOutput); + } + if (!debugConfiguration.host) { + debugConfiguration.host = 'localhost'; + } + if (!debugConfiguration.localRoot && workspaceFolder) { + debugConfiguration.localRoot = workspaceFolder.fsPath; + } + } + protected provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void { this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; @@ -75,7 +96,7 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonDebugConfiguration): Uri | undefined { + private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonLaunchDebugConfiguration): Uri | undefined { if (folder) { return folder.uri; } @@ -94,14 +115,14 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getProgram(config: PythonDebugConfiguration): string | undefined { + private getProgram(config: PythonLaunchDebugConfiguration): string | undefined { const documentManager = this.serviceContainer.get(IDocumentManager); const editor = documentManager.activeTextEditor; if (editor && editor.document.languageId === PythonLanguage.language) { return editor.document.fileName; } } - private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonDebugConfiguration): void { + private resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): void { if (!debugConfiguration) { return; } diff --git a/src/client/debugger/configProviders/pythonV2Provider.ts b/src/client/debugger/configProviders/pythonV2Provider.ts index 0d95f539e80b..d356901f45ab 100644 --- a/src/client/debugger/configProviders/pythonV2Provider.ts +++ b/src/client/debugger/configProviders/pythonV2Provider.ts @@ -8,15 +8,15 @@ import { Uri } from 'vscode'; import { IPlatformService } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; import { DebugOptions } from '../Common/Contracts'; -import { BaseConfigurationProvider, PythonDebugConfiguration } from './baseProvider'; +import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; @injectable() export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('pythonExperimental', serviceContainer); } - protected provideDefaults(workspaceFolder: Uri, debugConfiguration: PythonDebugConfiguration): void { - super.provideDefaults(workspaceFolder, debugConfiguration); + protected provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): void { + super.provideLaunchDefaults(workspaceFolder, debugConfiguration); debugConfiguration.stopOnEntry = false; debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : []; @@ -30,4 +30,14 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide debugConfiguration.debugOptions.push(DebugOptions.Jinja); } } + protected provideAttachDefaults(workspaceFolder: Uri, debugConfiguration: PythonAttachDebugConfiguration): void { + super.provideAttachDefaults(workspaceFolder, debugConfiguration); + + debugConfiguration.debugOptions = Array.isArray(debugConfiguration.debugOptions) ? debugConfiguration.debugOptions : []; + + // Add PTVSD specific flags. + if (this.serviceContainer.get(IPlatformService).isWindows) { + debugConfiguration.debugOptions.push(DebugOptions.FixFilePathCase); + } + } } diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 53f4d60d17cd..f7f78c585732 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -25,7 +25,7 @@ import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; import { DebugClient } from './DebugClients/DebugClient'; -import { CreateLaunchDebugClient } from './DebugClients/DebugFactory'; +import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; import { BaseDebugServer } from './DebugServers/BaseDebugServer'; import { initializeIoc } from './serviceRegistry'; import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; @@ -61,6 +61,11 @@ export class PythonDebugger extends DebugSession { } super.shutdown(); } + public async createAttachDebugServer(attachRequest: DebugProtocol.AttachRequest) { + const launcher = CreateAttachDebugClient(attachRequest.arguments as AttachRequestArguments, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + await this.debugServer!.Start(); + } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { const body = response.body!; @@ -87,7 +92,7 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - this.sendResponse(response); + // We will let PTVSD send the response } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { this.launchPTVSD(args) @@ -173,7 +178,7 @@ class DebugManager implements Disposable { private hasShutdown: boolean = false; private debugSession?: PythonDebugger; private ptvsdProcessId?: number; - private killPTVSDProcess: boolean = false; + private launchOrAttach?: 'launch'|'attach'; private terminatedEventSent: boolean = false; private readonly initializeRequestDeferred: Deferred; private get initializeRequest(): Promise { @@ -263,7 +268,7 @@ class DebugManager implements Disposable { this.terminatedEventSent = true; } - if (this.killPTVSDProcess && this.ptvsdProcessId) { + if (this.launchOrAttach === 'launch' && this.ptvsdProcessId) { logger.verbose('killing process'); try { // 1. Wait for some time, its possible the program has run to completion. @@ -273,7 +278,6 @@ class DebugManager implements Disposable { await sleep(100); killProcessTree(this.ptvsdProcessId!); } catch { } - this.killPTVSDProcess = false; this.ptvsdProcessId = undefined; } @@ -309,18 +313,37 @@ class DebugManager implements Disposable { // Keep track of the initialize and launch requests, we'll need to re-send these to ptvsd, for bootstrapping. this.inputProtocolParser.once('request_initialize', this.onRequestInitialize); this.inputProtocolParser.once('request_launch', this.onRequestLaunch); + this.inputProtocolParser.once('request_attach', this.onRequestAttach); this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); + this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSDForLaunch); + } + /** + * Connect PTVSD socket to VS Code. + * @private + * @memberof DebugManager + */ + private connectVSCodeToPTVSDForAttach = async (attachRequest: DebugProtocol.AttachRequest) => { + await this.debugSession!.createAttachDebugServer(attachRequest); + + await this.connectVSCodeToPTVSD(attachRequest, 'attach'); } /** * Once PTVSD process has been started (done by DebugSession), we need to connect PTVSD socket to VS Code. + * @private + * @memberof DebugManager + */ + private connectVSCodeToPTVSDForLaunch = async () => { + await this.connectVSCodeToPTVSD(await this.launchRequest, 'launch'); + } + /** + * Connect PTVSD socket to VS Code. * This allows PTVSD to communicate directly with VS Code. * @private * @memberof DebugManager */ - private connectVSCodeToPTVSD = async () => { + private connectVSCodeToPTVSD = async (attachOrLaunchRequest: DebugProtocol.AttachRequest | DebugProtocol.LaunchRequest, requestType: 'attach' | 'launch') => { // By now we're connected to the client. this.ptvsdSocket = await this.debugSession!.debugServer!.client; @@ -330,43 +353,50 @@ class DebugManager implements Disposable { this.ptvsdSocket.on('error', this.shutdown); const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); debugSoketProtocolParser.connect(this.ptvsdSocket); + const initializedEventPromise = new Promise(resolve => debugSoketProtocolParser.once('event_initialized', resolve)); + const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${requestType}`, resolve)); - // Send PTVSD the launch request (PTVSD needs to do its own initialization using launch arguments). - // E.g. redirectOutput & fixFilePathCase found in launch request are used to initialize the debugger. - this.sendMessage(await this.launchRequest, this.ptvsdSocket); - await new Promise(resolve => debugSoketProtocolParser.once('response_launch', resolve)); + // Keep track of processid for killing it. + if (requestType === 'launch') { + debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { + this.ptvsdProcessId = proc.body.systemProcessId; + }); + } - // The PTVSD process has launched, now send the initialize request to it (required by PTVSD). + // Send the launch/attach request to PTVSD and wait for it to reply back. + this.sendMessage(attachOrLaunchRequest, this.ptvsdSocket); + await attachedOrLaunchedPromise; + + // Send the initialize request and wait for it to reply back with the initialized event this.sendMessage(await this.initializeRequest, this.ptvsdSocket); - // Keep track of processid for killing it. - debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { - this.ptvsdProcessId = proc.body.systemProcessId; - }); + const initializedEvent = await initializedEventPromise; - // Wait for PTVSD to reply back with initialized event. - debugSoketProtocolParser.once('event_initialized', (initialized: DebugProtocol.InitializedEvent) => { - // Get ready for PTVSD to communicate directly with VS Code. - (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); - this.debugSessionOutputStream.unpipe(this.outputStream); + // Get ready for PTVSD to communicate directly with VS Code. + (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); + this.debugSessionOutputStream.unpipe(this.outputStream); - this.inputStream.pipe(this.ptvsdSocket!); - this.ptvsdSocket!.pipe(this.throughOutputStream); - this.ptvsdSocket!.pipe(this.outputStream); + this.inputStream.pipe(this.ptvsdSocket!); + this.ptvsdSocket!.pipe(this.throughOutputStream); + this.ptvsdSocket!.pipe(this.outputStream); - // Forward the initialized event sent by PTVSD onto VSCode. - // This is what will cause PTVSD to start the actualy work. - this.sendMessage(initialized, this.outputStream); - }); + // Forward the initialized event sent by PTVSD onto VSCode. + // This is what will cause PTVSD to start the actualy work. + this.sendMessage(initializedEvent, this.outputStream); } private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { this.initializeRequestDeferred.resolve(request); } private onRequestLaunch = (request: DebugProtocol.LaunchRequest) => { - this.killPTVSDProcess = true; + this.launchOrAttach = 'launch'; this.loggingEnabled = (request.arguments as LaunchRequestArguments).logToFile === true; this.launchRequestDeferred.resolve(request); } + private onRequestAttach = (request: DebugProtocol.AttachRequest) => { + this.launchOrAttach = 'attach'; + this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; + this.connectVSCodeToPTVSDForAttach(request).ignoreErrors(); + } private onEventTerminated = async () => { logger.verbose('onEventTerminated'); this.terminatedEventSent = true; diff --git a/src/test/debugger/configProvider/provider.attach.test.ts b/src/test/debugger/configProvider/provider.attach.test.ts new file mode 100644 index 000000000000..596cc907c7dc --- /dev/null +++ b/src/test/debugger/configProvider/provider.attach.test.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { PythonLanguage } from '../../../client/common/constants'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; +import { DebugOptions } from '../../../client/debugger/Common/Contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +enum OS { + Windows, + Mac, + Linux +} +[ + { debugType: 'pythonExperimental', class: PythonV2DebugConfigurationProvider }, + { debugType: 'python', class: PythonDebugConfigurationProvider } +].forEach(provider => { + EnumEx.getNamesAndValues(OS).forEach(os => { + suite(`Debugging - Config Provider attach, ${provider.debugType}, OS = ${os.name}`, () => { + let serviceContainer: TypeMoq.IMock; + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + const debugOptionsAvailable = [DebugOptions.RedirectOutput]; + if (os.value === OS.Windows && provider.debugType === 'pythonExperimental') { + debugOptionsAvailable.push(DebugOptions.FixFilePathCase); + } + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + platformService.setup(p => p.isWindows).returns(() => os.value === OS.Windows); + platformService.setup(p => p.isMac).returns(() => os.value === OS.Mac); + platformService.setup(p => p.isLinux).returns(() => os.value === OS.Linux); + debugProvider = new provider.class(serviceContainer.object); + }); + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType(); + folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + const documentManager = TypeMoq.Mock.ofType(); + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.languageId).returns(() => languageId); + document.setup(d => d.fileName).returns(() => fileName); + textEditor.setup(t => t.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + } + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + } + function setupWorkspaces(folders: string[]) { + const workspaceService = TypeMoq.Mock.ofType(); + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PythonLanguage.language); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PythonLanguage.language); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + setupActiveEditor(undefined, PythonLanguage.language); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const activeFile = 'xyz.js'; + + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const activeFile = 'xyz.py'; + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const filePath = Uri.file(defaultWorkspace).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + expect(debugConfig!.localRoot!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Ensure \'localRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + test('Ensure \'remoteRoot\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test('Ensure \'port\' is left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test('Ensure \'debugOptions\' are left unaltered', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PythonLanguage.language); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(debugOptions); + }); + }); + }); +}); From 4a1dcb0d81062ce0b6cf7b386c081657b936a1ca Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 10:19:44 -0700 Subject: [PATCH 03/20] :hammer: refactor --- .../DebugServers/RemoteDebugServerv2.ts | 8 ++ src/client/debugger/mainV2.ts | 78 ++++++++++--------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts index 21b6b119e57c..5e52620fb57e 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -31,11 +31,19 @@ export class RemoteDebugServerV2 extends BaseDebugServer { (options).host = this.args.host; } try { + let connected = false; const socket = connect(options, () => { + connected = true; this.socket = socket; this.clientSocket.resolve(socket); resolve(options); }); + socket.on('error', ex => { + if (connected) { + return; + } + reject(ex); + }); } catch (ex) { reject(ex); } diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index f7f78c585732..295711ad6b5a 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -24,7 +24,6 @@ import { createDeferred, Deferred, isNotInstalledError } from '../common/helpers import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; -import { DebugClient } from './DebugClients/DebugClient'; import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; import { BaseDebugServer } from './DebugServers/BaseDebugServer'; import { initializeIoc } from './serviceRegistry'; @@ -44,7 +43,6 @@ const MIN_DEBUGGER_CONNECT_TIMEOUT = 5000; */ export class PythonDebugger extends DebugSession { public debugServer?: BaseDebugServer; - public debugClient?: DebugClient<{}>; public client = createDeferred(); private supportsRunInTerminalRequest: boolean = false; constructor(private readonly serviceContainer: IServiceContainer) { @@ -55,17 +53,8 @@ export class PythonDebugger extends DebugSession { this.debugServer.Stop(); this.debugServer = undefined; } - if (this.debugClient) { - this.debugClient.Stop(); - this.debugClient = undefined; - } super.shutdown(); } - public async createAttachDebugServer(attachRequest: DebugProtocol.AttachRequest) { - const launcher = CreateAttachDebugClient(attachRequest.arguments as AttachRequestArguments, this); - this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); - await this.debugServer!.Start(); - } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { const body = response.body!; @@ -92,14 +81,24 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - // We will let PTVSD send the response + const launcher = CreateAttachDebugClient(args as AttachRequestArguments, this); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + this.debugServer!.Start() + .then(() => this.sendResponse(response)) + .catch(ex => { + logger.error('Attach failed'); + logger.error(`${ex}, ${ex.name}, ${ex.message}, ${ex.stack}`); + const message = this.getUserFriendlyAttachErrorMessage(ex) || 'Attach Failed'; + this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); + }); + } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { this.launchPTVSD(args) .then(() => this.waitForPTVSDToConnect(args)) .then(() => this.sendResponse(response)) .catch(ex => { - const message = this.getErrorUserFriendlyMessage(args, ex) || 'Debug Error'; + const message = this.getUserFriendlyLaunchErrorMessage(args, ex) || 'Debug Error'; this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); }); } @@ -135,7 +134,7 @@ export class PythonDebugger extends DebugSession { const connectionTimeout = typeof (args as any).timeout === 'number' ? (args as any).timeout as number : DEBUGGER_CONNECT_TIMEOUT; return Math.max(connectionTimeout, MIN_DEBUGGER_CONNECT_TIMEOUT); } - private getErrorUserFriendlyMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { + private getUserFriendlyLaunchErrorMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { if (!error) { return; } @@ -146,6 +145,16 @@ export class PythonDebugger extends DebugSession { return errorMsg; } } + private getUserFriendlyAttachErrorMessage(error: any): string | undefined { + if (!error) { + return; + } + if (error.code === 'ECONNREFUSED' || error.errno === 'ECONNREFUSED') { + return `Failed to attach (${error.message})`; + } else { + return typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); + } + } } /** @@ -178,7 +187,7 @@ class DebugManager implements Disposable { private hasShutdown: boolean = false; private debugSession?: PythonDebugger; private ptvsdProcessId?: number; - private launchOrAttach?: 'launch'|'attach'; + private launchOrAttach?: 'launch' | 'attach'; private terminatedEventSent: boolean = false; private readonly initializeRequestDeferred: Deferred; private get initializeRequest(): Promise { @@ -189,6 +198,11 @@ class DebugManager implements Disposable { return this.launchRequestDeferred.promise; } + private readonly attachRequestDeferred: Deferred; + private get attachRequest(): Promise { + return this.attachRequestDeferred.promise; + } + private set loggingEnabled(value: boolean) { if (value) { logger.setup(LogLevel.Verbose, true); @@ -216,6 +230,7 @@ class DebugManager implements Disposable { this.initializeRequestDeferred = createDeferred(); this.launchRequestDeferred = createDeferred(); + this.attachRequestDeferred = createDeferred(); } public dispose() { this.shutdown().ignoreErrors(); @@ -317,25 +332,8 @@ class DebugManager implements Disposable { this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSDForLaunch); - } - /** - * Connect PTVSD socket to VS Code. - * @private - * @memberof DebugManager - */ - private connectVSCodeToPTVSDForAttach = async (attachRequest: DebugProtocol.AttachRequest) => { - await this.debugSession!.createAttachDebugServer(attachRequest); - - await this.connectVSCodeToPTVSD(attachRequest, 'attach'); - } - /** - * Once PTVSD process has been started (done by DebugSession), we need to connect PTVSD socket to VS Code. - * @private - * @memberof DebugManager - */ - private connectVSCodeToPTVSDForLaunch = async () => { - await this.connectVSCodeToPTVSD(await this.launchRequest, 'launch'); + this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); + this.outputProtocolParser.once('response_attach', this.connectVSCodeToPTVSD); } /** * Connect PTVSD socket to VS Code. @@ -343,7 +341,11 @@ class DebugManager implements Disposable { * @private * @memberof DebugManager */ - private connectVSCodeToPTVSD = async (attachOrLaunchRequest: DebugProtocol.AttachRequest | DebugProtocol.LaunchRequest, requestType: 'attach' | 'launch') => { + private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { + if (!response || !response.success) { + return; + } + const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); // By now we're connected to the client. this.ptvsdSocket = await this.debugSession!.debugServer!.client; @@ -354,10 +356,10 @@ class DebugManager implements Disposable { const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); debugSoketProtocolParser.connect(this.ptvsdSocket); const initializedEventPromise = new Promise(resolve => debugSoketProtocolParser.once('event_initialized', resolve)); - const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${requestType}`, resolve)); + const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${this.launchOrAttach}`, resolve)); // Keep track of processid for killing it. - if (requestType === 'launch') { + if (this.launchOrAttach === 'launch') { debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { this.ptvsdProcessId = proc.body.systemProcessId; }); @@ -395,7 +397,7 @@ class DebugManager implements Disposable { private onRequestAttach = (request: DebugProtocol.AttachRequest) => { this.launchOrAttach = 'attach'; this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; - this.connectVSCodeToPTVSDForAttach(request).ignoreErrors(); + this.attachRequestDeferred.resolve(request); } private onEventTerminated = async () => { logger.verbose('onEventTerminated'); From 5d6223cba831c96098a0f8a3fa6232913cd3e423 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:14:21 -0700 Subject: [PATCH 04/20] :white_check_mark: tests for experimental debugger attach --- src/test/debugger/attach.ptvsd.test.ts | 136 ++++++++++++++++++ .../remoteDebugger-start-with-ptvsd.py | 14 ++ 2 files changed, 150 insertions(+) create mode 100644 src/test/debugger/attach.ptvsd.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts new file mode 100644 index 000000000000..dda852d1d434 --- /dev/null +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement + +import { expect } from 'chai'; +import { ChildProcess, spawn } from 'child_process'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +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 { sleep } from '../common'; +import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); +const ptvsdPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); +const DEBUG_ADAPTER = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); + +suite('Attach Debugger -Experimental', () => { + let debugClient: DebugClient; + let procToKill: ChildProcess; + suiteSetup(initialize); + + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await sleep(1000); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + await debugClient.start(); + }); + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await sleep(1000); + try { + await debugClient.stop().catch(() => { }); + } catch (ex) { } + if (procToKill) { + try { + procToKill.kill(); + } catch { } + } + }); + /** + * Creates the debug aimport { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ + function createDebugAdapter(): DebugClient { + if (IS_WINDOWS) { + return new DebugClient('node', DEBUG_ADAPTER, 'pythonExperimental'); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); + return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + test('Confirm we are able to attach to a running program', async () => { + // Lets skip this test on AppVeyor (very flaky on AppVeyor). + if (IS_APPVEYOR) { + return; + } + + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const customEnv = { ...process.env }; + + // Set the path for PTVSD to be picked up. + // tslint:disable-next-line:no-string-literal + customEnv['PYTHONPATH'] = ptvsdPath; + const pythonArgs = ['-m', 'ptvsd', '--server', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; + procToKill = spawn('python', pythonArgs, { env: customEnv, cwd: path.dirname(fileToDebug) }); + + // Send initialize, attach + const initializePromise = debugClient.initializeRequest({ + adapterID: 'pythonExperimental', + linesStartAt1: true, + columnsStartAt1: true, + supportsRunInTerminalRequest: true, + pathFormat: 'path', + supportsVariableType: true, + supportsVariablePaging: true + }); + const attachPromise = debugClient.attachRequest({ + localRoot: path.dirname(fileToDebug), + remoteRoot: path.dirname(fileToDebug), + type: 'pythonExperimental', + port: port, + host: 'localhost', + logToFile: true, + debugOptions: ['RedirectOutput'] + }); + + await Promise.all([ + initializePromise, + attachPromise, + debugClient.waitForEvent('initialized') + ]); + + await debugClient.configurationDoneRequest(); + + const stdOutPromise = debugClient.assertOutput('stdout', 'this is stdout'); + const stdErrPromise = debugClient.assertOutput('stderr', 'this is stderr'); + + const breakpointLocation = { path: fileToDebug, column: 1, line: 12 }; + const breakpointPromise = debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + const exceptionBreakpointPromise = debugClient.setExceptionBreakpointsRequest({ filters: [] }); + await Promise.all([ + breakpointPromise, + exceptionBreakpointPromise, + stdOutPromise, stdErrPromise + ]); + + await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + + const threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await Promise.all([ + debugClient.continueRequest({ threadId: threads.body.threads[0].id }), + debugClient.assertOutput('stdout', 'this is print'), + debugClient.waitForEvent('exited'), + debugClient.waitForEvent('terminated') + ]); + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py new file mode 100644 index 000000000000..ad8d7003cb6d --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py @@ -0,0 +1,14 @@ +import sys +import time +time.sleep(2) +sys.stdout.write('this is stdout') +sys.stdout.flush() +sys.stderr.write('this is stderr') +sys.stderr.flush() +# Give the debugger some time to add a breakpoint. +time.sleep(5) +for i in range(1): + time.sleep(0.5) + pass + +print('this is print') From 15249b34fd9e46d22fd2dd6c6f0e6446e6db00ec Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:17:32 -0700 Subject: [PATCH 05/20] :hammer: increase timeout of this test --- src/test/debugger/attach.ptvsd.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index dda852d1d434..bb9af518ea66 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -61,7 +61,8 @@ suite('Attach Debugger -Experimental', () => { return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); } } - test('Confirm we are able to attach to a running program', async () => { + 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; From febe297dbcfc560823185617cbab7e245e82b20b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:34:22 -0700 Subject: [PATCH 06/20] :memo: change log [skip ci] --- news/1 Enhancements/1229.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/1 Enhancements/1229.md diff --git a/news/1 Enhancements/1229.md b/news/1 Enhancements/1229.md new file mode 100644 index 000000000000..1367edb3bbcc --- /dev/null +++ b/news/1 Enhancements/1229.md @@ -0,0 +1,2 @@ +Add prelimnary support for remote debugging using the experimental debugger. +Attach to a Python program started using the command `python -m ptvsd --server --port 9091 --file pythonFile.py` \ No newline at end of file From 35afa54c03491d4b35765a3a531d49661890f0d3 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 16:40:35 -0700 Subject: [PATCH 07/20] increase timeout for tests --- src/test/debugger/common/constants.ts | 2 +- src/test/debugger/misc.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts index 9be293352e34..a9bcc64f1a24 100644 --- a/src/test/debugger/common/constants.ts +++ b/src/test/debugger/common/constants.ts @@ -4,4 +4,4 @@ 'use strict'; // Sometimes PTVSD can take a while for thread & other events to be reported. -export const DEBUGGER_TIMEOUT = 10000; +export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 90fc5669447f..00dc7cd9b402 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -445,7 +445,9 @@ let testCounter = 0; const pauseLocation = { path: path.join(debugFilesPath, 'sample3WithEx.py'), line: 5 }; await debugClient.assertStoppedLocation('exception', pauseLocation); }); - test('Test multi-threaded debugging', async () => { + test('Test multi-threaded debugging', async function () { + this.timeout(20000); + await Promise.all([ debugClient.configurationSequence(), debugClient.launch(buildLauncArgs('multiThread.py', false)), From c24183b2f2760bebe120f589ac246138c1d0be0f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 29 Mar 2018 23:48:28 -0700 Subject: [PATCH 08/20] add templates for attaching --- package.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/package.json b/package.json index 78b7681f2c2c..280b4ddf3387 100644 --- a/package.json +++ b/package.json @@ -874,6 +874,19 @@ "Pyramid" ] } + }, + { + "label": "%python.snippet.launch.attach.label%", + "description": "%python.snippet.launch.attach.description%", + "body": { + "name": "Attach (Remote Debug)", + "type": "pythonExperimental", + "request": "attach", + "localRoot": "^\"\\${workspaceFolder}\"", + "remoteRoot": "^\"\\${workspaceFolder}\"", + "port": 3000, + "host": "localhost" + } } ], "configurationAttributes": { @@ -1020,6 +1033,15 @@ "program": "${file}", "console": "integratedTerminal" }, + { + "name": "Python Experimental: Attach", + "type": "python", + "request": "pythonExperimental", + "localRoot": "${workspaceFolder}", + "remoteRoot": "${workspaceFolder}", + "port": 3000, + "host": "localhost" + }, { "name": "Python Experimental: Django", "type": "pythonExperimental", From 3275dd54d6d4f2b6604e65a9c78ab1e9255db026 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 00:04:26 -0700 Subject: [PATCH 09/20] change label [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 280b4ddf3387..fff60eeef8af 100644 --- a/package.json +++ b/package.json @@ -876,7 +876,7 @@ } }, { - "label": "%python.snippet.launch.attach.label%", + "label": "Python Experimental: Attach", "description": "%python.snippet.launch.attach.description%", "body": { "name": "Attach (Remote Debug)", From 0e5f0f656c3b79e6bea7604e46f8d90399d7a772 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 00:09:07 -0700 Subject: [PATCH 10/20] chane type [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fff60eeef8af..912a9f7e8955 100644 --- a/package.json +++ b/package.json @@ -1035,7 +1035,7 @@ }, { "name": "Python Experimental: Attach", - "type": "python", + "type": "pythonExperimental", "request": "pythonExperimental", "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}", From 9032acfeb670cbd0d8a5af07136634f9ed85a82f Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 30 Mar 2018 16:42:17 -0700 Subject: [PATCH 11/20] improvements tests (temporary workaround due to PTVSD issues) --- src/test/debugger/misc.test.ts | 58 ++++++++++++++++--- src/test/pythonFiles/debugging/multiThread.py | 2 +- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 00dc7cd9b402..05b67f77898f 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -3,8 +3,7 @@ // tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; +import { expect } from 'chai'; import * as path from 'path'; import { ThreadEvent } from 'vscode-debugadapter'; import { DebugClient } from 'vscode-debugadapter-testsupport'; @@ -22,8 +21,6 @@ import { DebugClientEx } from './debugClient'; const isProcessRunning = require('is-running') as (number) => boolean; -use(chaiAsPromised); - const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); const DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'Main.js'); @@ -446,13 +443,19 @@ let testCounter = 0; await debugClient.assertStoppedLocation('exception', pauseLocation); }); test('Test multi-threaded debugging', async function () { - this.timeout(20000); - + if (debuggerType !== 'python') { + // See GitHub issue #1250 + this.skip(); + return; + } await Promise.all([ debugClient.configurationSequence(), debugClient.launch(buildLauncArgs('multiThread.py', false)), debugClient.waitForEvent('initialized') ]); + + // Add a delay for debugger to start (sometimes it takes a long time for new debugger to break). + await sleep(3000); const pythonFile = path.join(debugFilesPath, 'multiThread.py'); const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; await debugClient.setBreakpointsRequest({ @@ -461,8 +464,49 @@ let testCounter = 0; source: { path: breakpointLocation.path } }); - // hit breakpoint. await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + const threads = await debugClient.threadsRequest(); + expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); + for (const thread of threads.body.threads) { + expect(thread.id).to.be.lessThan(MAX_SIGNED_INT32 + 1, 'ThreadId is not an integer'); + } + }); + test('Test multi-threaded debugging', async function () { + this.timeout(30000); + await Promise.all([ + debugClient.launch(buildLauncArgs('multiThread.py', false)), + debugClient.waitForEvent('initialized') + ]); + + const pythonFile = path.join(debugFilesPath, 'multiThread.py'); + const breakpointLocation = { path: pythonFile, column: 1, line: 11 }; + const breakpointRequestArgs = { + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }; + + function waitForStoppedEventFromTwoThreads() { + return new Promise((resolve, reject) => { + let numberOfStops = 0; + debugClient.addListener('stopped', (event: DebugProtocol.StoppedEvent) => { + numberOfStops += 1; + if (numberOfStops < 2) { + return; + } + resolve(event); + }); + setTimeout(() => reject(new Error('Timeout waiting for two threads to stop at breakpoint')), DEBUGGER_TIMEOUT); + }); + } + + await Promise.all([ + debugClient.setBreakpointsRequest(breakpointRequestArgs), + debugClient.setExceptionBreakpointsRequest({ filters: [] }), + debugClient.configurationDoneRequest(), + waitForStoppedEventFromTwoThreads(), + debugClient.assertStoppedLocation('breakpoint', breakpointLocation) + ]); const threads = await debugClient.threadsRequest(); expect(threads.body.threads).of.lengthOf(2, 'incorrect number of threads'); diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/pythonFiles/debugging/multiThread.py index 707f2568a2da..588971ffb502 100644 --- a/src/test/pythonFiles/debugging/multiThread.py +++ b/src/test/pythonFiles/debugging/multiThread.py @@ -4,7 +4,7 @@ def bar(): time.sleep(2) - print("abcdef") + print('bar') def foo(x): while True: From 4f855f02d016846c91fa7dabe3138dc18a1ff87c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 20:46:56 -0700 Subject: [PATCH 12/20] :hammer: connect vsc to ptvsd early on --- src/client/debugger/mainV2.ts | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 295711ad6b5a..35fec5767868 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -84,7 +84,7 @@ export class PythonDebugger extends DebugSession { const launcher = CreateAttachDebugClient(args as AttachRequestArguments, this); this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); this.debugServer!.Start() - .then(() => this.sendResponse(response)) + .then(() => this.emit('debugger_attached')) .catch(ex => { logger.error('Attach failed'); logger.error(`${ex}, ${ex.name}, ${ex.message}, ${ex.stack}`); @@ -96,7 +96,7 @@ export class PythonDebugger extends DebugSession { protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { this.launchPTVSD(args) .then(() => this.waitForPTVSDToConnect(args)) - .then(() => this.sendResponse(response)) + .then(() => this.emit('debugger_launched')) .catch(ex => { const message = this.getUserFriendlyLaunchErrorMessage(args, ex) || 'Debug Error'; this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); @@ -314,6 +314,9 @@ class DebugManager implements Disposable { this.debugSession = new PythonDebugger(this.serviceContainer); this.debugSession.setRunAsServer(this.isServerMode); + this.debugSession.once('debugger_attached', this.connectVSCodeToPTVSD); + this.debugSession.once('debugger_launched', this.connectVSCodeToPTVSD); + this.debugSessionOutputStream.pipe(this.throughOutputStream); this.debugSessionOutputStream.pipe(this.outputStream); @@ -332,8 +335,6 @@ class DebugManager implements Disposable { this.outputProtocolParser.once('event_terminated', this.onEventTerminated); this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - this.outputProtocolParser.once('response_launch', this.connectVSCodeToPTVSD); - this.outputProtocolParser.once('response_attach', this.connectVSCodeToPTVSD); } /** * Connect PTVSD socket to VS Code. @@ -342,9 +343,6 @@ class DebugManager implements Disposable { * @memberof DebugManager */ private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { - if (!response || !response.success) { - return; - } const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); // By now we're connected to the client. this.ptvsdSocket = await this.debugSession!.debugServer!.client; @@ -353,27 +351,16 @@ class DebugManager implements Disposable { // Note, we need a handler for the error event, else nodejs complains when socket gets closed and there are no error handlers. this.ptvsdSocket.on('end', this.shutdown); this.ptvsdSocket.on('error', this.shutdown); - const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); - debugSoketProtocolParser.connect(this.ptvsdSocket); - const initializedEventPromise = new Promise(resolve => debugSoketProtocolParser.once('event_initialized', resolve)); - const attachedOrLaunchedPromise = new Promise(resolve => debugSoketProtocolParser.once(`response_${this.launchOrAttach}`, resolve)); // Keep track of processid for killing it. if (this.launchOrAttach === 'launch') { + const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); + debugSoketProtocolParser.connect(this.ptvsdSocket); debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { this.ptvsdProcessId = proc.body.systemProcessId; }); } - // Send the launch/attach request to PTVSD and wait for it to reply back. - this.sendMessage(attachOrLaunchRequest, this.ptvsdSocket); - await attachedOrLaunchedPromise; - - // Send the initialize request and wait for it to reply back with the initialized event - this.sendMessage(await this.initializeRequest, this.ptvsdSocket); - - const initializedEvent = await initializedEventPromise; - // Get ready for PTVSD to communicate directly with VS Code. (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); this.debugSessionOutputStream.unpipe(this.outputStream); @@ -382,9 +369,11 @@ class DebugManager implements Disposable { this.ptvsdSocket!.pipe(this.throughOutputStream); this.ptvsdSocket!.pipe(this.outputStream); - // Forward the initialized event sent by PTVSD onto VSCode. - // This is what will cause PTVSD to start the actualy work. - this.sendMessage(initializedEvent, this.outputStream); + // Send the launch/attach request to PTVSD and wait for it to reply back. + this.sendMessage(attachOrLaunchRequest, this.ptvsdSocket); + + // Send the initialize request and wait for it to reply back with the initialized event + this.sendMessage(await this.initializeRequest, this.ptvsdSocket); } private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { this.initializeRequestDeferred.resolve(request); From 08dc3647105e5ce749bb72b0151c2a5d9fc28b6a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 3 Apr 2018 13:02:01 -0700 Subject: [PATCH 13/20] :hammer: use utils --- src/test/.vscode/settings.json | 3 +- src/test/debugger/attach.ptvsd.test.ts | 38 +++-------- src/test/debugger/utils.ts | 92 ++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 src/test/debugger/utils.ts diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index ba9c239e801d..2c36b6fce2fb 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -17,6 +17,5 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false, - "python.pythonPath": "python" + "python.linting.pylintUseMinimalCheckers": false } diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts index bb9af518ea66..8dbe6df09152 100644 --- a/src/test/debugger/attach.ptvsd.test.ts +++ b/src/test/debugger/attach.ptvsd.test.ts @@ -5,24 +5,21 @@ // tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement -import { expect } from 'chai'; import { ChildProcess, spawn } from 'child_process'; import * as getFreePort from 'get-port'; import * as path from 'path'; 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 { DebugOptions } from '../../client/debugger/Common/Contracts'; import { sleep } from '../common'; import { initialize, IS_APPVEYOR, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { DEBUGGER_TIMEOUT } from './common/constants'; -import { DebugClientEx } from './debugClient'; +import { continueDebugging, createDebugAdapter } from './utils'; const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); const ptvsdPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); -const DEBUG_ADAPTER = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); -suite('Attach Debugger -Experimental', () => { +suite('Attach Debugger - Experimental', () => { let debugClient: DebugClient; let procToKill: ChildProcess; suiteSetup(initialize); @@ -31,10 +28,8 @@ suite('Attach Debugger -Experimental', () => { if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { this.skip(); } - await sleep(1000); - debugClient = createDebugAdapter(); - debugClient.defaultTimeout = DEBUGGER_TIMEOUT; - await debugClient.start(); + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); + debugClient = await createDebugAdapter(coverageDirectory); }); teardown(async () => { // Wait for a second before starting another test (sometimes, sockets take a while to get closed). @@ -48,19 +43,6 @@ suite('Attach Debugger -Experimental', () => { } catch { } } }); - /** - * Creates the debug aimport { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; - * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. - * @returns {DebugClient} - */ - function createDebugAdapter(): DebugClient { - if (IS_WINDOWS) { - return new DebugClient('node', DEBUG_ADAPTER, 'pythonExperimental'); - } else { - const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); - return new DebugClientEx(DEBUG_ADAPTER, 'pythonExperimental', coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); - } - } 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). @@ -93,8 +75,8 @@ suite('Attach Debugger -Experimental', () => { type: 'pythonExperimental', port: port, host: 'localhost', - logToFile: true, - debugOptions: ['RedirectOutput'] + logToFile: false, + debugOptions: [DebugOptions.RedirectOutput] }); await Promise.all([ @@ -123,12 +105,8 @@ suite('Attach Debugger -Experimental', () => { await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); - const threads = await debugClient.threadsRequest(); - expect(threads).to.be.not.equal(undefined, 'no threads response'); - expect(threads.body.threads).to.be.lengthOf(1); - await Promise.all([ - debugClient.continueRequest({ threadId: threads.body.threads[0].id }), + continueDebugging(debugClient), debugClient.assertOutput('stdout', 'this is print'), debugClient.waitForEvent('exited'), debugClient.waitForEvent('terminated') diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts new file mode 100644 index 000000000000..ad59a2fd98e5 --- /dev/null +++ b/src/test/debugger/utils.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-http-string + +import { expect } from 'chai'; +import * as path from 'path'; +import * as request from 'request'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); +const debuggerType = 'pythonExperimental'; + +/** + * Creates the debug adapter. + * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. + * @returns {DebugClient} + */ +export async function createDebugAdapter(coverageDirectory: string): Promise { + await new Promise(resolve => setTimeout(resolve, 1000)); + let debugClient: DebugClient; + if (IS_WINDOWS) { + debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); + } else { + debugClient = new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + await debugClient.start(); + return debugClient; +} + +export async function continueDebugging(debugClient: DebugClient) { + const threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await debugClient.continueRequest({ threadId: threads.body.threads[0].id }); +} + +export type ExpectedVariable = { type: string; name: string; value: string }; +export async function validateVariablesInFrame(debugClient: DebugClient, + stackTrace: DebugProtocol.StackTraceResponse, + expectedVariables: ExpectedVariable[], numberOfScopes?: number) { + + const frameId = stackTrace.body.stackFrames[0].id; + + const scopes = await debugClient.scopesRequest({ frameId }); + if (numberOfScopes) { + expect(scopes.body.scopes).of.length(1, 'Incorrect number of scopes'); + } + + const variablesReference = scopes.body.scopes[0].variablesReference; + const variables = await debugClient.variablesRequest({ variablesReference }); + + for (const expectedVariable of expectedVariables) { + const variable = variables.body.variables.find(item => item.name === expectedVariable.name)!; + expect(variable).to.be.not.equal('undefined', `variable '${expectedVariable.name}' is undefined`); + expect(variable.type).to.be.equal(expectedVariable.type); + expect(variable.value).to.be.equal(expectedVariable.value); + } +} +export function makeHttpRequest(uri: string): Promise { + return new Promise((resolve, reject) => { + request.get(uri, (error: any, response: request.Response, body: any) => { + if (response.statusCode !== 200) { + reject(new Error(`Status code = ${response.statusCode}`)); + } else { + resolve(body.toString()); + } + }); + }); +} +export async function hitHttpBreakpoint(debugClient: DebugClient, uri: string, file: string, line: number): Promise<[DebugProtocol.StackTraceResponse, Promise]> { + const breakpointLocation = { path: file, column: 1, line }; + await debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + + // Make the request, we want the breakpoint to be hit. + const breakpointPromise = debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + const httpResult = makeHttpRequest(uri); + return [await breakpointPromise, httpResult]; +} From 39dcd34a2768e76adda18177f59bf8fb59575c7b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 09:50:59 -0700 Subject: [PATCH 14/20] code review changes --- .vscode/settings.json | 2 +- .../DebugServers/RemoteDebugServerv2.ts | 17 ++++++++--------- .../debugger/configProviders/baseProvider.ts | 10 +++++----- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 204fcc045003..311167344456 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ "coverage": true }, "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version - "tslint.enable": false, // We will run our own linting in gulp (& git commit hooks), else tslint extension just complains about unmodified files + "tslint.enable": true, // We will run our own linting in gulp (& git commit hooks), else tslint extension just complains about unmodified files "python.linting.enabled": false, "python.unitTest.promptToConfigure": false, "python.workspaceSymbols.enabled": false, diff --git a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts index 5e52620fb57e..be400c12c5ea 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServerv2.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServerv2.ts @@ -3,12 +3,10 @@ 'use strict'; -// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template - -import { DebugSession } from "vscode-debugadapter"; -import { IPythonProcess, IDebugServer, AttachRequestArguments } from "../Common/Contracts"; -import { connect, Socket } from "net"; -import { BaseDebugServer } from "./BaseDebugServer"; +import { connect, Socket } from 'net'; +import { DebugSession } from 'vscode-debugadapter'; +import { AttachRequestArguments, IDebugServer, IPythonProcess } from '../Common/Contracts'; +import { BaseDebugServer } from './BaseDebugServer'; export class RemoteDebugServerV2 extends BaseDebugServer { private args: AttachRequestArguments; @@ -25,9 +23,10 @@ export class RemoteDebugServerV2 extends BaseDebugServer { } public Start(): Promise { return new Promise((resolve, reject) => { - let portNumber = this.args.port; - let options = { port: portNumber! }; - if (typeof this.args.host === "string" && this.args.host.length > 0) { + const port = this.args.port!; + const options = { port }; + if (typeof this.args.host === 'string' && this.args.host.length > 0) { + // tslint:disable-next-line:no-any (options).host = this.args.host; } try { diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index 53329f185a18..a1556b29367e 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -22,12 +22,12 @@ export type PythonAttachDebugConfiguration = DebugConfiguration & AttachRequestA export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - const config = debugConfiguration as PythonLaunchDebugConfiguration; + const config = debugConfiguration as PythonLaunchDebugConfiguration | PythonAttachDebugConfiguration; const numberOfSettings = Object.keys(config); - const workspaceFolder = this.getWorkspaceFolder(folder, config); + const workspaceFolder = this.getWorkspaceFolder(folder, config as PythonLaunchDebugConfiguration); if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(config); + const defaultProgram = this.getProgram(config as PythonLaunchDebugConfiguration); config.name = 'Launch'; config.type = this.debugType; @@ -38,9 +38,9 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro if (config.request === 'attach') { // tslint:disable-next-line:no-any - this.provideAttachDefaults(workspaceFolder, config as any as PythonAttachDebugConfiguration); + this.provideAttachDefaults(workspaceFolder, config as PythonAttachDebugConfiguration); } else { - this.provideLaunchDefaults(workspaceFolder, config); + this.provideLaunchDefaults(workspaceFolder, config as PythonLaunchDebugConfiguration); } return config; } From 22d710d74b94e93058a4752ddae83c061a59dd12 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 09:53:10 -0700 Subject: [PATCH 15/20] :hammer: fix typo --- src/client/debugger/mainV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 35fec5767868..ac62abc54f19 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -230,7 +230,7 @@ class DebugManager implements Disposable { this.initializeRequestDeferred = createDeferred(); this.launchRequestDeferred = createDeferred(); - this.attachRequestDeferred = createDeferred(); + this.attachRequestDeferred = createDeferred(); } public dispose() { this.shutdown().ignoreErrors(); From 5c1dcfb6d2559fb7912ee0d1f2394a83e90615d9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 09:53:59 -0700 Subject: [PATCH 16/20] :hammer: fix code review comments --- src/client/debugger/mainV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index ac62abc54f19..b7604e3fcc9e 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -81,7 +81,7 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - const launcher = CreateAttachDebugClient(args as AttachRequestArguments, this); + const launcher = CreateAttachDebugClient(args, this); this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); this.debugServer!.Start() .then(() => this.emit('debugger_attached')) From 8ec6576831802f24054f3ed1eb16e0146a64f87e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 10:07:21 -0700 Subject: [PATCH 17/20] fixed code review comments --- src/client/debugger/configProviders/baseProvider.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index a1556b29367e..437ac56295f1 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -24,10 +24,10 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { const config = debugConfiguration as PythonLaunchDebugConfiguration | PythonAttachDebugConfiguration; const numberOfSettings = Object.keys(config); - const workspaceFolder = this.getWorkspaceFolder(folder, config as PythonLaunchDebugConfiguration); + const workspaceFolder = this.getWorkspaceFolder(folder); if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(config as PythonLaunchDebugConfiguration); + const defaultProgram = this.getProgram(); config.name = 'Launch'; config.type = this.debugType; @@ -96,11 +96,11 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonLaunchDebugConfiguration): Uri | undefined { + private getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { if (folder) { return folder.uri; } - const program = this.getProgram(config); + const program = this.getProgram(); const workspaceService = this.serviceContainer.get(IWorkspaceService); if (!Array.isArray(workspaceService.workspaceFolders) || workspaceService.workspaceFolders.length === 0) { return program ? Uri.file(path.dirname(program)) : undefined; @@ -115,7 +115,7 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro } } } - private getProgram(config: PythonLaunchDebugConfiguration): string | undefined { + private getProgram(): string | undefined { const documentManager = this.serviceContainer.get(IDocumentManager); const editor = documentManager.activeTextEditor; if (editor && editor.document.languageId === PythonLanguage.language) { From 80b1903ef58f6936f338cd9f313303d7ac22bc28 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 10:11:33 -0700 Subject: [PATCH 18/20] Further clean up --- .../debugger/configProviders/baseProvider.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index 437ac56295f1..849994a735b1 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -22,27 +22,27 @@ export type PythonAttachDebugConfiguration = DebugConfiguration & AttachRequestA export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - const config = debugConfiguration as PythonLaunchDebugConfiguration | PythonAttachDebugConfiguration; - const numberOfSettings = Object.keys(config); const workspaceFolder = this.getWorkspaceFolder(folder); - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(); + if (debugConfiguration.request === 'attach') { + this.provideAttachDefaults(workspaceFolder, debugConfiguration as PythonAttachDebugConfiguration); + } else { + const config = debugConfiguration as PythonLaunchDebugConfiguration; + const numberOfSettings = Object.keys(config); - config.name = 'Launch'; - config.type = this.debugType; - config.request = 'launch'; - config.program = defaultProgram ? defaultProgram : ''; - config.env = {}; - } + if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { + const defaultProgram = this.getProgram(); - if (config.request === 'attach') { - // tslint:disable-next-line:no-any - this.provideAttachDefaults(workspaceFolder, config as PythonAttachDebugConfiguration); - } else { - this.provideLaunchDefaults(workspaceFolder, config as PythonLaunchDebugConfiguration); + config.name = 'Launch'; + config.type = this.debugType; + config.request = 'launch'; + config.program = defaultProgram ? defaultProgram : ''; + config.env = {}; + } + + this.provideLaunchDefaults(workspaceFolder, config); } - return config; + return debugConfiguration; } protected provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): void { if (!Array.isArray(debugConfiguration.debugOptions)) { From 10297ef366a4279907a97d7abca5bce3eb62174b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 10:17:56 -0700 Subject: [PATCH 19/20] :hammer: rename based on code review --- src/client/debugger/mainV2.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index b7604e3fcc9e..3004ede3ce50 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -176,7 +176,7 @@ class DebugManager implements Disposable { private readonly debugSessionInputStream: PassThrough; // #endregion // #region Streams used to communicate with PTVSD. - private ptvsdSocket!: Socket; + private socket!: Socket; // #endregion private readonly inputProtocolParser: IProtocolParser; private readonly outputProtocolParser: IProtocolParser; @@ -265,9 +265,9 @@ class DebugManager implements Disposable { this.hasShutdown = true; logger.verbose('shutdown'); - if (this.ptvsdSocket) { - this.throughInputStream.unpipe(this.ptvsdSocket); - this.ptvsdSocket.unpipe(this.throughOutputStream); + if (this.socket) { + this.throughInputStream.unpipe(this.socket); + this.socket.unpipe(this.throughOutputStream); } if (!this.terminatedEventSent) { @@ -345,17 +345,17 @@ class DebugManager implements Disposable { private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); // By now we're connected to the client. - this.ptvsdSocket = await this.debugSession!.debugServer!.client; + this.socket = await this.debugSession!.debugServer!.client; // We need to handle both end and error, sometimes the socket will error out without ending (if debugee is killed). // Note, we need a handler for the error event, else nodejs complains when socket gets closed and there are no error handlers. - this.ptvsdSocket.on('end', this.shutdown); - this.ptvsdSocket.on('error', this.shutdown); + this.socket.on('end', this.shutdown); + this.socket.on('error', this.shutdown); // Keep track of processid for killing it. if (this.launchOrAttach === 'launch') { const debugSoketProtocolParser = this.serviceContainer.get(IProtocolParser); - debugSoketProtocolParser.connect(this.ptvsdSocket); + debugSoketProtocolParser.connect(this.socket); debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { this.ptvsdProcessId = proc.body.systemProcessId; }); @@ -365,15 +365,15 @@ class DebugManager implements Disposable { (this.inputStream as any as NodeJS.ReadStream).unpipe(this.debugSessionInputStream); this.debugSessionOutputStream.unpipe(this.outputStream); - this.inputStream.pipe(this.ptvsdSocket!); - this.ptvsdSocket!.pipe(this.throughOutputStream); - this.ptvsdSocket!.pipe(this.outputStream); + this.inputStream.pipe(this.socket!); + this.socket!.pipe(this.throughOutputStream); + this.socket!.pipe(this.outputStream); // Send the launch/attach request to PTVSD and wait for it to reply back. - this.sendMessage(attachOrLaunchRequest, this.ptvsdSocket); + this.sendMessage(attachOrLaunchRequest, this.socket); // Send the initialize request and wait for it to reply back with the initialized event - this.sendMessage(await this.initializeRequest, this.ptvsdSocket); + this.sendMessage(await this.initializeRequest, this.socket); } private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { this.initializeRequestDeferred.resolve(request); From 9bf271a2c392ea31f94c43d8e0e4dbf4726da538 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 4 Apr 2018 10:18:59 -0700 Subject: [PATCH 20/20] revert changes --- src/test/.vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index 2c36b6fce2fb..ba9c239e801d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -17,5 +17,6 @@ "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false + "python.linting.pylintUseMinimalCheckers": false, + "python.pythonPath": "python" }