From 2fd50a73c760d3ccceecdb6d531d24949c4b1bc6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 27 Mar 2018 13:31:51 -0700 Subject: [PATCH 01/17] 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/17] :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/17] :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/17] :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/17] :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/17] :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/17] 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/17] 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/17] 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/17] 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/17] 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 f6067888b1a619af344812ca2bf9d11a75251bc8 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 20:22:46 -0700 Subject: [PATCH 12/17] :white_check_mark: reattach tests --- src/test/debugger/attach.again.ptvsd.test.ts | 151 ++++++++++++++++++ ...moteDebugger-start-with-ptvsd.re-attach.py | 9 ++ 2 files changed, 160 insertions(+) create mode 100644 src/test/debugger/attach.again.ptvsd.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py diff --git a/src/test/debugger/attach.again.ptvsd.test.ts b/src/test/debugger/attach.again.ptvsd.test.ts new file mode 100644 index 000000000000..80ab6b9a0e2d --- /dev/null +++ b/src/test/debugger/attach.again.ptvsd.test.ts @@ -0,0 +1,151 @@ +// 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.re-attach.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 - detach and again again - Experimental', () => { + let debugClient: DebugClient; + let procToKill: ChildProcess; + suiteSetup(initialize); + + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await startDebugger(); + }); + 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 { } + } + }); + async function startDebugger() { + await sleep(1000); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = DEBUGGER_TIMEOUT; + await debugClient.start(); + } + /** + * 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 }); + } + } + async function startRemoteProcess() { + const port = await getFreePort({ host: 'localhost', port: 9091 }); + 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) }); + // wait for socket server to start. + await sleep(1000); + return port; + } + + async function waitForDebuggerCondfigurationDone(port: number) { + // 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: false, + debugOptions: ['RedirectOutput'] + }); + + await Promise.all([ + initializePromise, + attachPromise, + debugClient.waitForEvent('initialized') + ]); + + await debugClient.configurationDoneRequest(); + } + async function testAttaching(port: number) { + await waitForDebuggerCondfigurationDone(port); + let threads = await debugClient.threadsRequest(); + expect(threads).to.be.not.equal(undefined, 'no threads response'); + expect(threads.body.threads).to.be.lengthOf(1); + + await debugClient.setExceptionBreakpointsRequest({ filters: [] }); + const breakpointLocation = { path: fileToDebug, column: 1, line: 7 }; + await debugClient.setBreakpointsRequest({ + lines: [breakpointLocation.line], + breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], + source: { path: breakpointLocation.path } + }); + + await debugClient.assertStoppedLocation('breakpoint', breakpointLocation); + await debugClient.setBreakpointsRequest({ lines: [], breakpoints: [], source: { path: breakpointLocation.path } }); + + 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 }); + } + + test('Confirm we are able to attach, detach and attach to a running program', async function () { + this.timeout(20000); + // Lets skip this test on AppVeyor (very flaky on AppVeyor). + if (IS_APPVEYOR) { + return; + } + + const port = await startRemoteProcess(); + await testAttaching(port); + await debugClient.disconnectRequest({}); + await startDebugger(); + await testAttaching(port); + + const terminatedPromise = debugClient.waitForEvent('terminated'); + procToKill.kill(); + await terminatedPromise; + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py new file mode 100644 index 000000000000..1f4808fb5fb0 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.re-attach.py @@ -0,0 +1,9 @@ +import sys +import time +# Give the debugger some time to add a breakpoint. +time.sleep(5) +for i in range(10000): + time.sleep(0.5) + pass + +print('bye') From 4f855f02d016846c91fa7dabe3138dc18a1ff87c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 20:46:56 -0700 Subject: [PATCH 13/17] :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 1b24681876ae1286fecfa854cdf7d543b639d09d Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 21:15:46 -0700 Subject: [PATCH 14/17] :white_check_mark: fixed tests --- .vscode/settings.json | 4 +- src/test/debugger/attach.again.ptvsd.test.ts | 69 +++++++++++++++++--- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 204fcc045003..64daab8331f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "out": true, // set this to true to hide the "out" folder with the compiled JS files "**/*.pyc": true, "**/__pycache__": true, - "node_modules": true, + "node_modules": false, ".vscode-test": true, "**/.mypy_cache/**": true, "**/.ropeproject/**": true @@ -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/test/debugger/attach.again.ptvsd.test.ts b/src/test/debugger/attach.again.ptvsd.test.ts index 80ab6b9a0e2d..1c640c17c48b 100644 --- a/src/test/debugger/attach.again.ptvsd.test.ts +++ b/src/test/debugger/attach.again.ptvsd.test.ts @@ -23,7 +23,8 @@ const ptvsdPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', ' const DEBUG_ADAPTER = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); suite('Attach Debugger - detach and again again - Experimental', () => { - let debugClient: DebugClient; + let debugClient1: DebugClient; + let debugClient2: DebugClient; let procToKill: ChildProcess; suiteSetup(initialize); @@ -31,13 +32,19 @@ suite('Attach Debugger - detach and again again - Experimental', () => { if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { this.skip(); } - await startDebugger(); }); 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(() => { }); + if (debugClient1) { + await debugClient1.disconnectRequest(); + } + } catch (ex) { } + try { + if (debugClient2) { + await debugClient2.disconnectRequest(); + } } catch (ex) { } if (procToKill) { try { @@ -47,9 +54,10 @@ suite('Attach Debugger - detach and again again - Experimental', () => { }); async function startDebugger() { await sleep(1000); - debugClient = createDebugAdapter(); + const debugClient = createDebugAdapter(); debugClient.defaultTimeout = DEBUGGER_TIMEOUT; await debugClient.start(); + return debugClient; } /** * Creates the debug aimport { AttachRequestArguments } from '../../client/debugger/Common/Contracts'; @@ -78,7 +86,7 @@ suite('Attach Debugger - detach and again again - Experimental', () => { return port; } - async function waitForDebuggerCondfigurationDone(port: number) { + async function waitForDebuggerCondfigurationDone(debugClient: DebugClient, port: number) { // Send initialize, attach const initializePromise = debugClient.initializeRequest({ adapterID: 'pythonExperimental', @@ -107,8 +115,8 @@ suite('Attach Debugger - detach and again again - Experimental', () => { await debugClient.configurationDoneRequest(); } - async function testAttaching(port: number) { - await waitForDebuggerCondfigurationDone(port); + async function testAttaching(debugClient: DebugClient, port: number) { + await waitForDebuggerCondfigurationDone(debugClient, port); let threads = await debugClient.threadsRequest(); expect(threads).to.be.not.equal(undefined, 'no threads response'); expect(threads.body.threads).to.be.lengthOf(1); @@ -138,14 +146,55 @@ suite('Attach Debugger - detach and again again - Experimental', () => { return; } + let debugClient = debugClient1 = await startDebugger(); + const port = await startRemoteProcess(); - await testAttaching(port); + await testAttaching(debugClient, port); await debugClient.disconnectRequest({}); - await startDebugger(); - await testAttaching(port); + debugClient = await startDebugger(); + await testAttaching(debugClient, port); const terminatedPromise = debugClient.waitForEvent('terminated'); procToKill.kill(); await terminatedPromise; }); + + test('Confirm we are unable to attach if already attached to a running program', async function () { + this.timeout(200000); + // Lets skip this test on AppVeyor (very flaky on AppVeyor). + if (IS_APPVEYOR) { + return; + } + + debugClient1 = await startDebugger(); + + const port = await startRemoteProcess(); + await testAttaching(debugClient1, port); + + debugClient2 = await startDebugger(); + // Send initialize, attach + const initializePromise = debugClient2.initializeRequest({ + adapterID: 'pythonExperimental', + linesStartAt1: true, + columnsStartAt1: true, + supportsRunInTerminalRequest: true, + pathFormat: 'path', + supportsVariableType: true, + supportsVariablePaging: true + }); + + const attachMustFail = 'A debugger is already attached to this process'; + const attachPromise = debugClient2.attachRequest({ + localRoot: path.dirname(fileToDebug), + remoteRoot: path.dirname(fileToDebug), + type: 'pythonExperimental', + port: port, + host: 'localhost', + logToFile: true, + debugOptions: ['RedirectOutput'] + }).catch(() => Promise.resolve(attachMustFail)); + // tslint:disable-next-line:no-unused-variable + const [_, attachResponse] = await Promise.all([initializePromise, attachPromise]); + expect(attachResponse).to.be.equal(attachMustFail); + }); }); From 433f796d59fa18186f165f67e39f6c773e606014 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 21:16:19 -0700 Subject: [PATCH 15/17] hide node_modules folder --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 64daab8331f2..311167344456 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "out": true, // set this to true to hide the "out" folder with the compiled JS files "**/*.pyc": true, "**/__pycache__": true, - "node_modules": false, + "node_modules": true, ".vscode-test": true, "**/.mypy_cache/**": true, "**/.ropeproject/**": true From be15ee221a879a1af5f20f29b51e89095456985b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 2 Apr 2018 22:45:13 -0700 Subject: [PATCH 16/17] :white_check_mark: tests for modules --- src/test/debugger/misc.test.ts | 6 +- src/test/debugger/module.test.ts | 88 +++++++++++++++++++ .../workspace5/mymod/__init__.py | 0 .../workspace5/mymod/__main__.py | 1 + 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/test/debugger/module.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/mymod/__init__.py create mode 100644 src/testMultiRootWkspc/workspace5/mymod/__main__.py diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts index 05b67f77898f..7b0897dc70ce 100644 --- a/src/test/debugger/misc.test.ts +++ b/src/test/debugger/misc.test.ts @@ -71,6 +71,7 @@ let testCounter = 0; // tslint:disable-next-line:no-string-literal env['PYTHONPATH'] = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd'); } + // tslint:disable-next-line:no-unnecessary-local-variable const options: LaunchRequestArguments = { program: path.join(debugFilesPath, pythonFile), cwd: debugFilesPath, @@ -84,11 +85,6 @@ let testCounter = 0; type: debuggerType }; - // Custom experimental debugger options (filled in by DebugConfigurationProvider). - if (debuggerType === 'pythonExperimental') { - (options as any).redirectOutput = true; - } - return options; } diff --git a/src/test/debugger/module.test.ts b/src/test/debugger/module.test.ts new file mode 100644 index 000000000000..891e7086cc56 --- /dev/null +++ b/src/test/debugger/module.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any + +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); +const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5'); +let testCounter = 0; +const debuggerType = 'pythonExperimental'; +suite(`Module Debugging - Misc tests: ${debuggerType}`, () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await new Promise(resolve => setTimeout(resolve, 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(noop); + // tslint:disable-next-line:no-empty + } catch (ex) { } + await sleep(1000); + }); + /** + * Creates the debug adapter. + * 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', testAdapterFilePath, debuggerType); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage${testCounter += 1}`); + return new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + function buildLauncArgs(): LaunchRequestArguments { + const env = {}; + // tslint:disable-next-line:no-string-literal + env['PYTHONPATH'] = `.${path.delimiter}${path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd')}`; + + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + module: 'mymod', + program: '', + cwd: workspaceDirectory, + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: 'python', + args: [], + env, + envFile: '', + logToFile: false, + type: debuggerType + }; + + return options; + } + + test('Test stdout output', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs()), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stdout', 'hello world'), + debugClient.waitForEvent('exited'), + debugClient.waitForEvent('terminated') + ]); + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/mymod/__init__.py b/src/testMultiRootWkspc/workspace5/mymod/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/mymod/__main__.py b/src/testMultiRootWkspc/workspace5/mymod/__main__.py new file mode 100644 index 000000000000..8cde7829c178 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/mymod/__main__.py @@ -0,0 +1 @@ +print("hello world") From a21e7ea00ce5f5f9211ce77f9c23d5cecb1a15bc Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 3 Apr 2018 08:52:02 -0700 Subject: [PATCH 17/17] :white_check_mark: flak template debugging --- package.json | 2 + src/test/debugger/flask.test.ts | 173 ++++++++++++++++++ .../workspace5/flskApp/run.py | 14 ++ .../workspace5/flskApp/templates/hello.html | 8 + yarn.lock | 116 +++++++++++- 5 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 src/test/debugger/flask.test.ts create mode 100644 src/testMultiRootWkspc/workspace5/flskApp/run.py create mode 100644 src/testMultiRootWkspc/workspace5/flskApp/templates/hello.html diff --git a/package.json b/package.json index 912a9f7e8955..3946f88f596b 100644 --- a/package.json +++ b/package.json @@ -1859,6 +1859,7 @@ "@types/md5": "^2.1.32", "@types/mocha": "^2.2.48", "@types/node": "^9.4.7", + "@types/request": "^2.47.0", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", "@types/sinon": "^4.3.0", @@ -1890,6 +1891,7 @@ "mocha": "^5.0.4", "relative": "^3.0.2", "remap-istanbul": "^0.10.1", + "request": "^2.85.0", "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", "shortid": "^2.2.8", "sinon": "^4.4.5", diff --git a/src/test/debugger/flask.test.ts b/src/test/debugger/flask.test.ts new file mode 100644 index 000000000000..2bd2d14f59b7 --- /dev/null +++ b/src/test/debugger/flask.test.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any no-http-string + +import { expect } from 'chai'; +import * as getFreePort from 'get-port'; +import * as path from 'path'; +import * as request from 'request'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { DEBUGGER_TIMEOUT } from './common/constants'; +import { DebugClientEx } from './debugClient'; + +const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'mainV2.js'); +const workspaceDirectory = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'flskApp'); +let testCounter = 0; +const debuggerType = 'pythonExperimental'; +suite(`Flask Debugging - Misc tests: ${debuggerType}`, () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + debugClient = createDebugAdapter(); + debugClient.defaultTimeout = 2 * 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(noop); + // tslint:disable-next-line:no-empty + } catch (ex) { } + await sleep(1000); + }); + /** + * Creates the debug adapter. + * 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', testAdapterFilePath, debuggerType); + } else { + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage${testCounter += 1}`); + return new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); + } + } + function buildLauncArgs(port: number): LaunchRequestArguments { + const env = {}; + // tslint:disable-next-line:no-string-literal + env['PYTHONPATH'] = `.${path.delimiter}${path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'experimental', 'ptvsd')}`; + // tslint:disable-next-line:no-string-literal + env['FLASK_APP'] = path.join(workspaceDirectory, 'run.py'); + + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + module: 'flask', + program: '', + cwd: workspaceDirectory, + debugOptions: [DebugOptions.RedirectOutput, DebugOptions.Jinja], + pythonPath: 'python', + args: [ + 'run', + '--no-debugger', + '--no-reload', + '--port', + `${port}` + ], + env, + envFile: '', + logToFile: true, + type: debuggerType + }; + + return options; + } + + test('Test Flask Route and Template debugging', async function () { + this.timeout(5 * DEBUGGER_TIMEOUT); + const port = await getFreePort({ host: 'localhost' }); + + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs(port)), + debugClient.waitForEvent('initialized'), + debugClient.waitForEvent('process'), + debugClient.waitForEvent('thread') + ]); + + const httpResult = await new Promise((resolve, reject) => { + request.get(`http://localhost:${port}`, (error: any, response: request.Response, body: any) => { + if (response.statusCode !== 200) { + reject(new Error(`Status code = ${response.statusCode}`)); + } else { + resolve(body.toString()); + } + }); + }); + + expect(httpResult.trim()).to.be.equal('Hello World!'); + + const breakpointLocation = { path: path.join(workspaceDirectory, 'run.py'), column: 1, line: 7 }; + 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); + request.get(`http://localhost:${port}`); + await breakpointPromise; + + async function continueDebugging() { + 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 }); + } + + await continueDebugging(); + + // Template debugging. + const templateBreakpointLocation = { path: path.join(workspaceDirectory, 'templates', 'hello.html'), column: 1, line: 5 }; + await debugClient.setBreakpointsRequest({ + lines: [templateBreakpointLocation.line], + breakpoints: [{ line: templateBreakpointLocation.line, column: templateBreakpointLocation.column }], + source: { path: templateBreakpointLocation.path } + }); + + const templateBreakpointPromise = debugClient.assertStoppedLocation('breakpoint', templateBreakpointLocation); + const httpTemplateResult = new Promise((resolve, reject) => { + request.get(`http://localhost:${port}/hello/Don`, { timeout: 100000 }, (error: any, response: request.Response, body: any) => { + if (response.statusCode !== 200) { + reject(new Error(`Status code = ${response.statusCode}`)); + } else { + resolve(body.toString()); + } + }); + }); + + const frames = await templateBreakpointPromise; + + // Wait for breakpoint to hit + const frameId = frames.body.stackFrames[0].id; + const scopes = await debugClient.scopesRequest({ frameId }); + + expect(scopes.body.scopes).of.length(1, 'Incorrect number of scopes'); + const variablesReference = scopes.body.scopes[0].variablesReference; + const variables = await debugClient.variablesRequest({ variablesReference }); + + const vari = variables.body.variables.find(item => item.name === 'name')!; + expect(vari).to.be.not.equal('undefined', 'variable \'name\' is undefined'); + expect(vari.type).to.be.equal('str'); + expect(vari.value).to.be.equal('\'Don\''); + + await continueDebugging(); + const htmlResult = await httpTemplateResult; + expect(htmlResult).to.contain('Hello Don'); + }); +}); diff --git a/src/testMultiRootWkspc/workspace5/flskApp/run.py b/src/testMultiRootWkspc/workspace5/flskApp/run.py new file mode 100644 index 000000000000..2a246f7cbb24 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flskApp/run.py @@ -0,0 +1,14 @@ +from flask import Flask, render_template +app = Flask(__name__) + + +@app.route('/') +def hello(): + return "Hello World!" + +@app.route('/hello/') +def hello_name(user): + return render_template('hello.html', name=user) + +if __name__ == '__main__': + app.run() diff --git a/src/testMultiRootWkspc/workspace5/flskApp/templates/hello.html b/src/testMultiRootWkspc/workspace5/flskApp/templates/hello.html new file mode 100644 index 000000000000..68f97e206fdd --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flskApp/templates/hello.html @@ -0,0 +1,8 @@ + + + + +

Hello {{ name }}!

+ + + diff --git a/yarn.lock b/yarn.lock index 19893afaaa94..197767d5f1cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,10 @@ dependencies: samsam "1.3.0" +"@types/caseless@*": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a" + "@types/chai-as-promised@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz#010b04cde78eacfb6e72bfddb3e58fe23c2e78b9" @@ -57,6 +61,12 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" +"@types/form-data@*": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" + dependencies: + "@types/node" "*" + "@types/fs-extra@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.1.tgz#cd856fbbdd6af2c11f26f8928fd8644c9e9616c9" @@ -107,6 +117,15 @@ version "9.4.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" +"@types/request@^2.47.0": + version "2.47.0" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.47.0.tgz#76a666cee4cb85dcffea6cd4645227926d9e114e" + dependencies: + "@types/caseless" "*" + "@types/form-data" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + "@types/semver@^5.4.0", "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" @@ -119,6 +138,10 @@ version "4.3.0" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-4.3.0.tgz#7f53915994a00ccea24f4e0c24709822ed11a3b1" +"@types/tough-cookie@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.2.tgz#e0d481d8bb282ad8a8c9e100ceb72c995fb5e709" + "@types/untildify@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/untildify/-/untildify-3.0.0.tgz#cd3e6624e46ccf292d3823fb48fa90dda0deaec0" @@ -470,6 +493,13 @@ binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" +"binary@>= 0.3.0 < 1": + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -550,6 +580,10 @@ buffer-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -619,6 +653,12 @@ chai@^4.1.2: pathval "^1.0.0" type-detect "^4.0.0" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + dependencies: + traverse ">=0.3.0 <0.4" + chalk@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" @@ -1472,6 +1512,15 @@ fstream-ignore@^1.0.5: inherits "2" minimatch "^3.0.0" +"fstream@>= 0.1.30 < 1": + version "0.1.31" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-0.1.31.tgz#7337f058fbbbbefa8c9f561a28cab0849202c988" + dependencies: + graceful-fs "~3.0.2" + inherits "~2.0.0" + mkdirp "0.5" + rimraf "2" + fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" @@ -1695,7 +1744,7 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, gr version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -graceful-fs@^3.0.0: +graceful-fs@^3.0.0, graceful-fs@~3.0.2: version "3.0.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" dependencies: @@ -1826,7 +1875,7 @@ gulp-untar@^0.0.6: tar "^2.2.1" through2 "~2.0.3" -gulp-util@^3.0.0, gulp-util@^3.0.7, gulp-util@~3.0.0, gulp-util@~3.0.7, gulp-util@~3.0.8: +gulp-util@^3.0.0, gulp-util@^3.0.7, gulp-util@~3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -2915,6 +2964,13 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +"match-stream@>= 0.0.2 < 1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/match-stream/-/match-stream-0.0.2.tgz#99eb050093b34dffade421b9ac0b410a9cfa17cf" + dependencies: + buffers "~0.1.1" + readable-stream "~1.0.0" + md5.js@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" @@ -3052,7 +3108,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: +mkdirp@0.5, mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -3367,6 +3423,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +"over@>= 0.0.5 < 1": + version "0.0.5" + resolved "https://registry.yarnpkg.com/over/-/over-0.0.5.tgz#f29852e70fd7e25f360e013a8ec44c82aedb5708" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -3547,6 +3607,15 @@ pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +"pullstream@>= 0.4.1 < 1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/pullstream/-/pullstream-0.4.1.tgz#d6fb3bf5aed697e831150eb1002c25a3f8ae1314" + dependencies: + over ">= 0.0.5 < 1" + readable-stream "~1.0.31" + setimmediate ">= 1.0.2 < 2" + slice-stream ">= 1.0.0 < 2" + pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -3625,7 +3694,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: +"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.0, readable-stream@~1.0.17, readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" dependencies: @@ -3763,6 +3832,12 @@ replace-ext@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + dependencies: + throttleit "^1.0.0" + request@2.81.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -3790,7 +3865,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@^2.83.0: +request@^2.83.0, request@^2.85.0: version "2.85.0" resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" dependencies: @@ -3904,7 +3979,7 @@ ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" -retyped-diff-match-patch-tsd-ambient@^1.0.0-1: +retyped-diff-match-patch-tsd-ambient@^1.0.0-0: version "1.0.0-1" resolved "https://registry.yarnpkg.com/retyped-diff-match-patch-tsd-ambient/-/retyped-diff-match-patch-tsd-ambient-1.0.0-1.tgz#26482bf4915c7ed9f8300bb5cbec48fd4ff5bc62" @@ -3986,6 +4061,10 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" +"setimmediate@>= 1.0.1 < 2", "setimmediate@>= 1.0.2 < 2": + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + shortid@^2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.8.tgz#033b117d6a2e975804f6f0969dbe7d3d0b355131" @@ -4014,6 +4093,12 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +"slice-stream@>= 1.0.0 < 2": + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-stream/-/slice-stream-1.0.0.tgz#5b33bd66f013b1a7f86460b03d463dec39ad3ea0" + dependencies: + readable-stream "~1.0.31" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -4341,6 +4426,10 @@ text-encoding@^0.6.4: version "0.6.4" resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + through2-filter@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec" @@ -4450,6 +4539,10 @@ tough-cookie@~2.3.0, tough-cookie@~2.3.3: dependencies: punycode "^1.4.1" +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + tree-kill@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" @@ -4620,6 +4713,17 @@ untildify@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1" +unzip@^0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/unzip/-/unzip-0.1.11.tgz#89749c63b058d7d90d619f86b98aa1535d3b97f0" + dependencies: + binary ">= 0.3.0 < 1" + fstream ">= 0.1.30 < 1" + match-stream ">= 0.0.2 < 1" + pullstream ">= 0.4.1 < 1" + readable-stream "~1.0.31" + setimmediate ">= 1.0.1 < 2" + upath@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d"