diff --git a/.vscode/launch.json b/.vscode/launch.json index 50bdb33e744b..264d49e34f11 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -33,6 +33,22 @@ "cwd": "${workspaceFolder}", "preLaunchTask": "Compile" }, + { + "name": "Launch Experimental Debugger as debugServer", // https://code.visualstudio.com/docs/extensions/example-debuggers + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/out/client/debugger/mainV2.js", + "stopOnEntry": false, + "args": [ + "--server=4711" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/client/**/*.js" + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "Compile" + }, { "name": "Launch Tests", "type": "extensionHost", diff --git a/package.json b/package.json index 1aaf7b19b500..5eed4680c9a9 100644 --- a/package.json +++ b/package.json @@ -385,7 +385,8 @@ "console": "integratedTerminal", "env": {}, "envFile": "^\"\\${workspaceFolder}/.env\"", - "debugOptions": [] + "debugOptions": [], + "internalConsoleOptions": "neverOpen" } }, { @@ -402,7 +403,8 @@ "console": "externalTerminal", "env": {}, "envFile": "^\"\\${workspaceFolder}/.env\"", - "debugOptions": [] + "debugOptions": [], + "internalConsoleOptions": "neverOpen" } }, { @@ -740,7 +742,8 @@ "console": "integratedTerminal", "env": {}, "envFile": "${workspaceFolder}/.env", - "debugOptions": [] + "debugOptions": [], + "internalConsoleOptions": "neverOpen" }, { "name": "Python: Terminal (external)", @@ -753,7 +756,8 @@ "console": "externalTerminal", "env": {}, "envFile": "${workspaceFolder}/.env", - "debugOptions": [] + "debugOptions": [], + "internalConsoleOptions": "neverOpen" }, { "name": "Python: Django", @@ -884,6 +888,161 @@ ] } ] + }, + { + "type": "pythonExperimental", + "label": "Python Experimental", + "languages": [ + "python" + ], + "enableBreakpointsFor": { + "languageIds": [ + "python", + "html" + ] + }, + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "program": "./out/client/debugger/mainV2.js", + "runtime": "node", + "configurationSnippets": [ + { + "label": "Python Experimental: Terminal (integrated)", + "description": "%python.snippet.launch.terminal.description%", + "body": { + "name": "Integrated Terminal/Console", + "type": "pythonExperimental", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${file}\"", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "^\"\\${workspaceFolder}/.env\"", + "debugOptions": [], + "internalConsoleOptions": "neverOpen" + } + }, + { + "label": "Python Experimental: Terminal (external)", + "description": "%python.snippet.launch.externalTerminal.description%", + "body": { + "name": "External Terminal/Console", + "type": "pythonExperimental", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "^\"\\${config:python.pythonPath}\"", + "program": "^\"\\${file}\"", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "^\"\\${workspaceFolder}/.env\"", + "debugOptions": [], + "internalConsoleOptions": "neverOpen" + } + } + ], + "configurationAttributes": { + "launch": { + "properties": { + "program": { + "type": "string", + "description": "Absolute path to the program.", + "default": "${file}" + }, + "pythonPath": { + "type": "string", + "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", + "default": "${config:python.pythonPath}" + }, + "args": { + "type": "array", + "description": "Command line arguments passed to the program", + "default": [], + "items": { + "type": "string" + } + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "console": { + "enum": [ + "integratedTerminal", + "externalTerminal" + ], + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "default": "integratedTerminal" + }, + "cwd": { + "type": "string", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "" + }, + "debugOptions": { + "type": "array", + "description": "Advanced options, view read me for further details.", + "items": { + "type": "string", + "enum": [ + "Sudo" + ] + }, + "default": [] + }, + "env": { + "type": "object", + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "default": {} + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "" + }, + "port": { + "type": "number", + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP address of the of the local debug server (default is localhost).", + "default": "localhost" + } + } + } + }, + "initialConfigurations": [ + { + "name": "Python Experimental: Current File (Integrated Terminal)", + "type": "pythonExperimental", + "request": "launch", + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [], + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Python Experimental: Current File (External Terminal)", + "type": "pythonExperimental", + "request": "launch", + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceFolder}/.env", + "debugOptions": [], + "internalConsoleOptions": "neverOpen" + } + ] } ], "configuration": { @@ -1663,5 +1822,10 @@ "typescript-formatter": "^6.0.0", "vscode": "^1.1.5", "vscode-debugadapter-testsupport": "^1.25.0" + }, + "__metadata": { + "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", + "publisherDisplayName": "Microsoft", + "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } } diff --git a/pythonFiles/experimental/ptvsd_launcher.py b/pythonFiles/experimental/ptvsd_launcher.py new file mode 100644 index 000000000000..98c4c5525bcf --- /dev/null +++ b/pythonFiles/experimental/ptvsd_launcher.py @@ -0,0 +1,96 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +""" +Starts Debugging, expected to start with normal program +to start as first argument and directory to run from as +the second argument. +""" + +__author__ = "Microsoft Corporation " +__version__ = "3.2.0.0" + +import os +import os.path +import sys +import traceback + +# Arguments are: +# 1. Working directory. +# 2. VS debugger port to connect to. +# 3. GUID for the debug session. +# 4. Debug options (as list of names - see enum PythonDebugOptions). +# 5. '-g' to use the installed ptvsd package, rather than bundled one. +# 6. '-m' or '-c' to override the default run-as mode. [optional] +# 7. Startup script name. +# 8. Script arguments. + +# change to directory we expected to start from +os.chdir(sys.argv[1]) + +port_num = int(sys.argv[2]) +debug_id = sys.argv[3] +debug_options = set([opt.strip() for opt in sys.argv[4].split(',')]) + +del sys.argv[0:5] + +# Use bundled ptvsd or not? +bundled_ptvsd = True +if sys.argv and sys.argv[0] == '-g': + bundled_ptvsd = False + del sys.argv[0] + +# set run_as mode appropriately +run_as = 'script' +if sys.argv and sys.argv[0] == '-m': + run_as = 'module' + del sys.argv[0] +if sys.argv and sys.argv[0] == '-c': + run_as = 'code' + del sys.argv[0] + +# preserve filename before we del sys +filename = sys.argv[0] + +# fix sys.path to be the script file dir +sys.path[0] = '' + +# Load the debugger package +try: + if bundled_ptvsd: + ptvs_lib_path = os.path.dirname(__file__) + sys.path.insert(0, ptvs_lib_path) + import ptvsd + import ptvsd.debugger as vspd + vspd.DONT_DEBUG.append(os.path.normcase(__file__)) +except: + traceback.print_exc() + print(''' +Internal error detected. Please copy the above traceback and report at +https://go.microsoft.com/fwlink/?LinkId=293415 + +Press Enter to close. . .''') + try: + raw_input() + except NameError: + input() + sys.exit(1) +finally: + if bundled_ptvsd: + sys.path.remove(ptvs_lib_path) + +# and start debugging +vspd.debug(filename, port_num, debug_id, debug_options, run_as) diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index f3eb7d507fbf..42c60d57175e 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as vscode from 'vscode'; import { ConfigurationTarget, Uri } from 'vscode'; +import { isTestExecution } from './constants'; import { IAutoCompeteSettings, IFormattingSettings, @@ -22,11 +23,6 @@ const untildify = require('untildify'); export const IS_WINDOWS = /^win/.test(process.platform); -export function isTestExecution(): boolean { - // tslint:disable-next-line:interface-name no-string-literal - return process.env['VSC_PYTHON_CI_TEST'] === '1'; -} - // tslint:disable-next-line:completed-docs export class PythonSettings extends EventEmitter implements IPythonSettings { private static pythonSettings: Map = new Map(); diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index b577ba044753..8b30c2b5d3ed 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -63,3 +63,8 @@ export namespace LinterErrors { } export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; + +export function isTestExecution(): boolean { + // tslint:disable-next-line:interface-name no-string-literal + return process.env['VSC_PYTHON_CI_TEST'] === '1'; +} diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts index 698159acb3f4..7f340b11499b 100644 --- a/src/client/common/net/socket/socketServer.ts +++ b/src/client/common/net/socket/socketServer.ts @@ -1,13 +1,23 @@ import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; import * as net from 'net'; -import { createDeferred } from '../../helpers'; +import { createDeferred, Deferred } from '../../helpers'; +import { ISocketServer } from '../../types'; -export class SocketServer extends EventEmitter { +@injectable() +export class SocketServer extends EventEmitter implements ISocketServer { private socketServer: net.Server | undefined; + private clientSocket: Deferred; + public get client(): Promise { + return this.clientSocket.promise; + } constructor() { super(); + this.clientSocket = createDeferred(); + } + public dispose() { + this.Stop(); } - public Stop() { if (!this.socketServer) { return; } try { @@ -37,6 +47,9 @@ export class SocketServer extends EventEmitter { } private connectionListener(client: net.Socket) { + if (!this.clientSocket.completed) { + this.clientSocket.resolve(client); + } client.on('close', () => { this.emit('close', client); }); diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index d573256e403e..794d9efc04d6 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -9,7 +9,7 @@ import { IFileSystem, IPlatformService } from './types'; @injectable() export class FileSystem implements IFileSystem { - constructor( @inject(IPlatformService) private platformService: IPlatformService) { } + constructor(@inject(IPlatformService) private platformService: IPlatformService) { } public get directorySeparatorChar(): string { return path.sep; @@ -77,4 +77,13 @@ export class FileSystem implements IFileSystem { return path1 === path2; } } + + public appendFileSync(filename: string, data: {}, encoding: string): void; + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string; }): void; + // tslint:disable-next-line:unified-signatures + public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string; }): void; + public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void { + return fs.appendFileSync(filename, data, optionsOrEncoding); + } + } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 7d861532dd20..15f1bae1bdfa 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -37,4 +37,8 @@ export interface IFileSystem { getSubDirectoriesAsync(rootDir: string): Promise; arePathsSame(path1: string, path2: string): boolean; readFile(filePath: string): Promise; + appendFileSync(filename: string, data: {}, encoding: string): void; + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string; }): void; + // tslint:disable-next-line:unified-signatures + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string; }): void; } diff --git a/src/client/common/process/currentProcess.ts b/src/client/common/process/currentProcess.ts index c092b0f657d6..fb02b56af1c2 100644 --- a/src/client/common/process/currentProcess.ts +++ b/src/client/common/process/currentProcess.ts @@ -7,4 +7,13 @@ export class CurrentProcess implements ICurrentProcess { public get env(): EnvironmentVariables { return process.env; } + public get argv(): string[] { + return process.argv; + } + public get stdout(): NodeJS.WriteStream { + return process.stdout; + } + public get stdin(): NodeJS.ReadStream { + return process.stdin; + } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index e35ddee8de1e..fd81483e2ffe 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -2,7 +2,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, DiagnosticSeverity, Uri } from 'vscode'; +import {Socket} from 'net'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, Uri } from 'vscode'; + import { EnvironmentVariables } from './variables/types'; export const IOutputChannel = Symbol('IOutputChannel'); export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); @@ -86,6 +88,9 @@ export interface IPathUtils { export const ICurrentProcess = Symbol('ICurrentProcess'); export interface ICurrentProcess { readonly env: EnvironmentVariables; + readonly argv: string[]; + readonly stdout: NodeJS.WriteStream; + readonly stdin: NodeJS.ReadStream; } export interface IPythonSettings { @@ -211,3 +216,9 @@ export interface IConfigurationService { isTestExecution(): boolean; updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; } + +export const ISocketServer = Symbol('ISocketServer'); +export interface ISocketServer extends Disposable { + readonly client: Promise; + Start(options?: { port?: number, host?: string }): Promise; +} diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 88396172cb69..1a77e6ff98c8 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -45,6 +45,7 @@ export interface ExceptionHandling { } export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { + type?: 'python' | 'pythonExperimental'; /** An absolute path to the program to debug. */ module?: string; program: string; @@ -61,6 +62,8 @@ export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArgum console?: 'none' | 'integratedTerminal' | 'externalTerminal'; port?: number; host?: string; + diagnosticLogging?: boolean; + logToFile?: boolean; } export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { diff --git a/src/client/debugger/Common/debugStreamProvider.ts b/src/client/debugger/Common/debugStreamProvider.ts new file mode 100644 index 000000000000..d05be94d7a15 --- /dev/null +++ b/src/client/debugger/Common/debugStreamProvider.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { createServer, Socket } from 'net'; +import { isTestExecution } from '../../common/constants'; +import { ICurrentProcess } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IDebugStreamProvider } from '../types'; + +@injectable() +export class DebugStreamProvider implements IDebugStreamProvider { + constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } + public get useDebugSocketStream(): boolean { + return this.getDebugPort() > 0; + } + public async getInputAndOutputStreams(): Promise<{ input: NodeJS.ReadStream | Socket; output: NodeJS.WriteStream | Socket }> { + const debugPort = this.getDebugPort(); + let debugSocket: Promise | undefined; + + if (debugPort > 0) { + // This section is what allows VS Code extension developers to attach to the current debugger. + // Used in scenarios where extension developers would like to debug the debugger. + debugSocket = new Promise(resolve => { + // start as a server, and print to console in VS Code debugger for extension developer. + // Do not print this out when running unit tests. + if (!isTestExecution()) { + console.error(`waiting for debug protocol on port ${debugPort}`); + } + createServer((socket) => { + if (!isTestExecution()) { + console.error('>> accepted connection from client'); + } + resolve(socket); + }).listen(debugPort); + }); + } + + const currentProcess = this.serviceContainer.get(ICurrentProcess); + const input = debugSocket ? await debugSocket : currentProcess.stdin; + const output = debugSocket ? await debugSocket : currentProcess.stdout; + + return { input, output }; + } + private getDebugPort() { + const currentProcess = this.serviceContainer.get(ICurrentProcess); + + let debugPort = 0; + const args = currentProcess.argv.slice(2); + args.forEach((val, index, array) => { + const portMatch = /^--server=(\d{4,5})$/.exec(val); + if (portMatch) { + debugPort = parseInt(portMatch[1], 10); + } + }); + return debugPort; + } +} diff --git a/src/client/debugger/Common/protocolLogger.ts b/src/client/debugger/Common/protocolLogger.ts new file mode 100644 index 000000000000..5fcdd5cb3f77 --- /dev/null +++ b/src/client/debugger/Common/protocolLogger.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { Readable } from 'stream'; +import { Logger } from 'vscode-debugadapter'; +import { IProtocolLogger } from '../types'; + +@injectable() +export class ProtocolLogger implements IProtocolLogger { + private inputStream?: Readable; + private outputStream?: Readable; + private messagesToLog: string[] = []; + private logger?: Logger.ILogger; + public dispose() { + if (this.inputStream) { + this.inputStream.removeListener('data', this.fromDataCallbackHandler); + this.outputStream!.removeListener('data', this.toDataCallbackHandler); + this.messagesToLog = []; + this.inputStream = undefined; + this.outputStream = undefined; + } + } + public connect(inputStream: Readable, outputStream: Readable) { + this.inputStream = inputStream; + this.outputStream = outputStream; + + inputStream.addListener('data', this.fromDataCallbackHandler); + outputStream.addListener('data', this.toDataCallbackHandler); + } + public setup(logger: Logger.ILogger) { + this.logger = logger; + this.logMessages([`Started @ ${new Date().toString()}`]); + this.logMessages(this.messagesToLog); + this.messagesToLog = []; + } + private fromDataCallbackHandler = (data: string | Buffer) => { + this.logMessages(['From Client:', (data as Buffer).toString('utf8')]); + } + private toDataCallbackHandler = (data: string | Buffer) => { + this.logMessages(['To Client:', (data as Buffer).toString('utf8')]); + } + private logMessages(messages: string[]) { + if (this.logger) { + messages.forEach(message => this.logger!.verbose(`${message}`)); + } else { + this.messagesToLog.push(...messages); + } + } +} diff --git a/src/client/debugger/Common/protocolParser.ts b/src/client/debugger/Common/protocolParser.ts new file mode 100644 index 000000000000..45157dcac5f3 --- /dev/null +++ b/src/client/debugger/Common/protocolParser.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-constant-condition no-typeof-undefined + +import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; +import { Readable } from 'stream'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IProtocolParser } from '../types'; + +const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; + +/** + * Parsers the debugger Protocol messages and raises the following events: + * 1. 'data', message (for all protocol messages) + * 1. 'event_', message (for all protocol events) + * 1. 'request_', message (for all protocol requests) + * 1. 'response_', message (for all protocol responses) + * 1. '', message (for all protocol messages that are not events, requests nor responses) + * @export + * @class ProtocolParser + * @extends {EventEmitter} + * @implements {IProtocolParser} + */ +@injectable() +export class ProtocolParser extends EventEmitter implements IProtocolParser { + private rawData = new Buffer(0); + private contentLength: number = -1; + private disposed: boolean; + private stream?: Readable; + constructor() { + super(); + } + public dispose() { + if (this.stream) { + this.stream.removeListener('data', this.dataCallbackHandler); + this.stream = undefined; + } + } + public connect(stream: Readable) { + this.stream = stream; + stream.addListener('data', this.dataCallbackHandler); + } + private dataCallbackHandler = (data: string | Buffer) => { + this.handleData(data as Buffer); + } + private dispatch(body: string): void { + const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; + + switch (message.type) { + case 'event': { + const event = message as DebugProtocol.Event; + if (typeof event.event === 'string') { + this.emit(`${message.type}_${event.event}`, event); + break; + } + } + case 'request': { + const request = message as DebugProtocol.Request; + if (typeof request.command === 'string') { + this.emit(`${message.type}_${request.command}`, request); + break; + } + } + case 'response': { + const reponse = message as DebugProtocol.Response; + if (typeof reponse.command === 'string') { + this.emit(`${message.type}_${reponse.command}`, reponse); + break; + } + } + default: { + this.emit(`${message.type}`, message); + } + } + + this.emit('data', message); + } + private handleData(data: Buffer): void { + if (this.disposed) { + return; + } + this.rawData = Buffer.concat([this.rawData, data]); + + while (true) { + if (this.contentLength >= 0) { + if (this.rawData.length >= this.contentLength) { + const message = this.rawData.toString('utf8', 0, this.contentLength); + this.rawData = this.rawData.slice(this.contentLength); + this.contentLength = -1; + if (message.length > 0) { + this.dispatch(message); + } + // there may be more complete messages to process. + continue; + } + } else { + const idx = this.rawData.indexOf(PROTOCOL_START_INDENTIFIER); + if (idx !== -1) { + const header = this.rawData.toString('utf8', 0, idx); + const lines = header.split('\r\n'); + for (const line of lines) { + const pair = line.split(/: +/); + if (pair[0] === 'Content-Length') { + this.contentLength = +pair[1]; + } + } + this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); + continue; + } + } + break; + } + } +} diff --git a/src/client/debugger/Common/protocolWriter.ts b/src/client/debugger/Common/protocolWriter.ts new file mode 100644 index 000000000000..305830a34dc4 --- /dev/null +++ b/src/client/debugger/Common/protocolWriter.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Socket } from 'net'; +import { Message } from 'vscode-debugadapter/lib/messages'; +import { IProtocolMessageWriter } from '../types'; + +const TWO_CRLF = '\r\n\r\n'; + +@injectable() +export class ProtocolMessageWriter implements IProtocolMessageWriter { + public write(stream: Socket | NodeJS.WriteStream, message: Message): void { + const json = JSON.stringify(message); + const length = Buffer.byteLength(json, 'utf8'); + + stream.write(`Content-Length: ${length.toString()}${TWO_CRLF}`, 'utf8'); + stream.write(json, 'utf8'); + } +} diff --git a/src/client/debugger/DebugClients/DebugClient.ts b/src/client/debugger/DebugClients/DebugClient.ts index 2c3c9d99f77a..b4bd97a902ab 100644 --- a/src/client/debugger/DebugClients/DebugClient.ts +++ b/src/client/debugger/DebugClients/DebugClient.ts @@ -4,19 +4,20 @@ import { BaseDebugServer } from "../DebugServers/BaseDebugServer"; import { IPythonProcess, IDebugServer } from "../Common/Contracts"; import { DebugSession } from "vscode-debugadapter"; import { EventEmitter } from 'events'; +import { IServiceContainer } from "../../ioc/types"; export enum DebugType { Local, Remote, RunLocal } -export abstract class DebugClient extends EventEmitter { +export abstract class DebugClient extends EventEmitter { protected debugSession: DebugSession; - constructor(protected args: any, debugSession: DebugSession) { + constructor(protected args: T, debugSession: DebugSession) { super(); this.debugSession = debugSession; } - public abstract CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer; + public abstract CreateDebugServer(pythonProcess?: IPythonProcess, serviceContainer?: IServiceContainer): BaseDebugServer ; public get DebugType(): DebugType { return DebugType.Local; } @@ -24,7 +25,7 @@ export abstract class DebugClient extends EventEmitter { public Stop() { } - public LaunchApplicationToDebug(dbgServer: IDebugServer, processErrored: (error: any) => void): Promise { + public LaunchApplicationToDebug(dbgServer: IDebugServer): Promise { return Promise.resolve(); } } diff --git a/src/client/debugger/DebugClients/DebugFactory.ts b/src/client/debugger/DebugClients/DebugFactory.ts index 7824ec9b5cd4..21229631727e 100644 --- a/src/client/debugger/DebugClients/DebugFactory.ts +++ b/src/client/debugger/DebugClients/DebugFactory.ts @@ -1,16 +1,18 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, LaunchRequestArguments } from '../Common/Contracts'; import { DebugClient } from './DebugClient'; +import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; import { LocalDebugClient } from './LocalDebugClient'; import { NonDebugClient } from './NonDebugClient'; import { RemoteDebugClient } from './RemoteDebugClient'; -export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient { +export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> { if (launchRequestOptions.noDebug === true) { - return new NonDebugClient(launchRequestOptions, debugSession, canLaunchTerminal); + return new NonDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, new NoDebugLauncherScriptProvider()); } - return new LocalDebugClient(launchRequestOptions, debugSession, canLaunchTerminal); + const launchScriptProvider = launchRequestOptions.type === 'pythonExperimental' ? new DebuggerV2LauncherScriptProvider() : new DebuggerLauncherScriptProvider(); + return new LocalDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); } -export function CreateAttachDebugClient(attachRequestOptions: AttachRequestArguments, debugSession: DebugSession): DebugClient { +export function CreateAttachDebugClient(attachRequestOptions: AttachRequestArguments, debugSession: DebugSession): DebugClient<{}> { return new RemoteDebugClient(attachRequestOptions, debugSession); } diff --git a/src/client/debugger/DebugClients/LocalDebugClient.ts b/src/client/debugger/DebugClients/LocalDebugClient.ts index 77255c527078..9065ed40cc60 100644 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/DebugClients/LocalDebugClient.ts @@ -7,10 +7,13 @@ import { open } from '../../common/open'; import { PathUtils } from '../../common/platform/pathUtils'; import { CurrentProcess } from '../../common/process/currentProcess'; import { EnvironmentVariablesService } from '../../common/variables/environment'; -import { IDebugServer, IPythonProcess } from '../Common/Contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { IDebugServer, IPythonProcess, LaunchRequestArguments } from '../Common/Contracts'; import { IS_WINDOWS } from '../Common/Utils'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { LocalDebugServer } from '../DebugServers/LocalDebugServer'; +import { LocalDebugServerV2 } from '../DebugServers/LocalDebugServerV2'; +import { IDebugLauncherScriptProvider } from '../types'; import { DebugClient, DebugType } from './DebugClient'; import { DebugClientHelper } from './helper'; @@ -26,7 +29,7 @@ enum DebugServerStatus { NotRunning = 3 } -export class LocalDebugClient extends DebugClient { +export class LocalDebugClient extends DebugClient { protected pyProc: child_process.ChildProcess | undefined; protected pythonProcess: IPythonProcess; protected debugServer: BaseDebugServer | undefined; @@ -40,13 +43,17 @@ export class LocalDebugClient extends DebugClient { return DebugServerStatus.Unknown; } // tslint:disable-next-line:no-any - constructor(args: any, debugSession: DebugSession, private canLaunchTerminal: boolean) { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, private launcherScriptProvider: IDebugLauncherScriptProvider) { super(args, debugSession); } - public CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess; - this.debugServer = new LocalDebugServer(this.debugSession, this.pythonProcess, this.args); + public CreateDebugServer(pythonProcess?: IPythonProcess, serviceContainer?: IServiceContainer): BaseDebugServer { + if (this.args.type === 'pythonExperimental') { + this.debugServer = new LocalDebugServerV2(this.debugSession, this.args, serviceContainer!); + } else { + this.pythonProcess = pythonProcess!; + this.debugServer = new LocalDebugServer(this.debugSession, this.pythonProcess!, this.args); + } return this.debugServer; } @@ -59,7 +66,9 @@ export class LocalDebugClient extends DebugClient { this.debugServer!.Stop(); this.debugServer = undefined; } - + if (this.args.type === 'pythonExperimental' && this.pyProc) { + this.pyProc.kill(); + } if (this.pyProc) { try { this.pyProc!.send('EXIT'); @@ -76,11 +85,6 @@ export class LocalDebugClient extends DebugClient { this.pyProc = undefined; } } - protected getLauncherFilePath(): string { - const currentFileName = module.filename; - const ptVSToolsPath = path.join(path.dirname(currentFileName), '..', '..', '..', '..', 'pythonFiles', 'PythonTools'); - return path.join(ptVSToolsPath, 'visualstudio_py_launcher.py'); - } // tslint:disable-next-line:no-any private displayError(error: any) { const errorMsg = typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); @@ -89,7 +93,7 @@ export class LocalDebugClient extends DebugClient { } } // tslint:disable-next-line:max-func-body-length member-ordering no-any - public async LaunchApplicationToDebug(dbgServer: IDebugServer, processErrored: (error: any) => void): Promise { + public async LaunchApplicationToDebug(dbgServer: IDebugServer): Promise { const pathUtils = new PathUtils(IS_WINDOWS); const currentProcess = new CurrentProcess(); const helper = new DebugClientHelper(new EnvironmentVariablesService(pathUtils), pathUtils, currentProcess); @@ -105,7 +109,7 @@ export class LocalDebugClient extends DebugClient { if (typeof this.args.pythonPath === 'string' && this.args.pythonPath.trim().length > 0) { pythonPath = this.args.pythonPath; } - const ptVSToolsFilePath = this.getLauncherFilePath(); + const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath(); const launcherArgs = this.buildLauncherArguments(); const args = [ptVSToolsFilePath, processCwd, dbgServer.port.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14'].concat(launcherArgs); @@ -146,6 +150,9 @@ export class LocalDebugClient extends DebugClient { }); proc.stderr.setEncoding('utf8'); proc.stderr.on('data', error => { + if (this.args.type === 'pythonExperimental') { + return; + } // We generally don't need to display the errors as stderr output is being captured by debugger // and it gets sent out to the debug client. diff --git a/src/client/debugger/DebugClients/NonDebugClient.ts b/src/client/debugger/DebugClients/NonDebugClient.ts index 8acef3ab875a..f7f81c892915 100644 --- a/src/client/debugger/DebugClients/NonDebugClient.ts +++ b/src/client/debugger/DebugClients/NonDebugClient.ts @@ -1,14 +1,14 @@ import { ChildProcess } from 'child_process'; -import * as path from 'path'; import { DebugSession } from 'vscode-debugadapter'; import { LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; import { DebugType } from './DebugClient'; import { LocalDebugClient } from './LocalDebugClient'; export class NonDebugClient extends LocalDebugClient { // tslint:disable-next-line:no-any - constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean) { - super(args, debugSession, canLaunchTerminal); + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + super(args, debugSession, canLaunchTerminal, launcherScriptProvider); } public get DebugType(): DebugType { @@ -28,9 +28,4 @@ export class NonDebugClient extends LocalDebugClient { protected handleProcessOutput(proc: ChildProcess, _failedToLaunch: (error: Error | string | Buffer) => void) { this.pythonProcess.attach(proc); } - protected getLauncherFilePath(): string { - const currentFileName = module.filename; - const ptVSToolsPath = path.join(path.dirname(currentFileName), '..', '..', '..', '..', 'pythonFiles', 'PythonTools'); - return path.join(ptVSToolsPath, 'visualstudio_py_launcher_nodebug.py'); - } } diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts index 1acccaadbf60..45aec00388a4 100644 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ b/src/client/debugger/DebugClients/RemoteDebugClient.ts @@ -1,10 +1,10 @@ import { DebugSession } from 'vscode-debugadapter'; -import { IPythonProcess } from '../Common/Contracts'; +import { AttachRequestArguments, IPythonProcess } from '../Common/Contracts'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer'; import { DebugClient, DebugType } from './DebugClient'; -export class RemoteDebugClient extends DebugClient { +export class RemoteDebugClient extends DebugClient { private pythonProcess: IPythonProcess; private debugServer?: BaseDebugServer; // tslint:disable-next-line:no-any @@ -12,9 +12,9 @@ export class RemoteDebugClient extends DebugClient { super(args, debugSession); } - public CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess; - this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess, this.args); + public CreateDebugServer(pythonProcess?: IPythonProcess): BaseDebugServer { + 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/DebugClients/launcherProvider.ts b/src/client/debugger/DebugClients/launcherProvider.ts new file mode 100644 index 000000000000..25f722457918 --- /dev/null +++ b/src/client/debugger/DebugClients/launcherProvider.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { IDebugLauncherScriptProvider } from '../types'; + +export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider { + public getLauncherFilePath(): string { + return path.join(path.dirname(__dirname), '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_launcher_nodebug.py'); + } +} + +export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider { + public getLauncherFilePath(): string { + return path.join(path.dirname(__dirname), '..', '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_launcher.py'); + } +} + +export class DebuggerV2LauncherScriptProvider implements IDebugLauncherScriptProvider { + public getLauncherFilePath(): string { + return path.join(path.dirname(__dirname), '..', '..', '..', 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); + } +} diff --git a/src/client/debugger/DebugServers/BaseDebugServer.ts b/src/client/debugger/DebugServers/BaseDebugServer.ts index 824c6f103299..18975d25e959 100644 --- a/src/client/debugger/DebugServers/BaseDebugServer.ts +++ b/src/client/debugger/DebugServers/BaseDebugServer.ts @@ -5,8 +5,13 @@ import { DebugSession } from "vscode-debugadapter"; import { IPythonProcess, IDebugServer } from "../Common/Contracts"; import { EventEmitter } from "events"; import { Deferred, createDeferred } from '../../common/helpers'; +import { Socket } from 'net'; export abstract class BaseDebugServer extends EventEmitter { + protected clientSocket: Deferred; + public get client(): Promise { + return this.clientSocket.promise; + } protected pythonProcess: IPythonProcess; protected debugSession: DebugSession; @@ -18,11 +23,12 @@ export abstract class BaseDebugServer extends EventEmitter { public get DebugClientConnected(): Promise { return this.debugClientConnected.promise; } - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess) { + constructor(debugSession: DebugSession, pythonProcess?: IPythonProcess) { super(); this.debugSession = debugSession; - this.pythonProcess = pythonProcess; + this.pythonProcess = pythonProcess!; this.debugClientConnected = createDeferred(); + this.clientSocket = createDeferred(); } public abstract Start(): Promise; diff --git a/src/client/debugger/DebugServers/DebugServerFactory.ts b/src/client/debugger/DebugServers/DebugServerFactory.ts index a400ce58296a..88d9063dc9ff 100644 --- a/src/client/debugger/DebugServers/DebugServerFactory.ts +++ b/src/client/debugger/DebugServers/DebugServerFactory.ts @@ -3,6 +3,6 @@ import { IPythonProcess, LaunchRequestArguments } from '../Common/Contracts'; import { BaseDebugServer } from './BaseDebugServer'; import { LocalDebugServer } from './LocalDebugServer'; -export function CreateDebugServer(debugSession: DebugSession, pythonProcess: IPythonProcess, args: LaunchRequestArguments): BaseDebugServer { +export function CreateDebugServer(debugSession: DebugSession, pythonProcess: IPythonProcess | undefined, args: LaunchRequestArguments): BaseDebugServer { return new LocalDebugServer(debugSession, pythonProcess, args); } diff --git a/src/client/debugger/DebugServers/LocalDebugServer.ts b/src/client/debugger/DebugServers/LocalDebugServer.ts index 6fb9cae1c83d..b53340daf0f8 100644 --- a/src/client/debugger/DebugServers/LocalDebugServer.ts +++ b/src/client/debugger/DebugServers/LocalDebugServer.ts @@ -9,7 +9,7 @@ import { BaseDebugServer } from './BaseDebugServer'; export class LocalDebugServer extends BaseDebugServer { private debugSocketServer: net.Server | undefined; - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, private args: LaunchRequestArguments) { + constructor(debugSession: DebugSession, pythonProcess: IPythonProcess | undefined, private args: LaunchRequestArguments) { super(debugSession, pythonProcess); } diff --git a/src/client/debugger/DebugServers/LocalDebugServerV2.ts b/src/client/debugger/DebugServers/LocalDebugServerV2.ts new file mode 100644 index 000000000000..cc5486cd1332 --- /dev/null +++ b/src/client/debugger/DebugServers/LocalDebugServerV2.ts @@ -0,0 +1,48 @@ + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as net from 'net'; +import { DebugSession } from 'vscode-debugadapter'; +import { createDeferred } from '../../common/helpers'; +import { ISocketServer } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IDebugServer, LaunchRequestArguments } from '../Common/Contracts'; +import { BaseDebugServer } from './BaseDebugServer'; + +export class LocalDebugServerV2 extends BaseDebugServer { + private socketServer?: ISocketServer; + + constructor(debugSession: DebugSession, private args: LaunchRequestArguments, private serviceContainer: IServiceContainer) { + super(debugSession); + this.clientSocket = createDeferred(); + } + + public Stop() { + if (this.socketServer) { + try { + this.socketServer.dispose(); + // tslint:disable-next-line:no-empty + } catch { } + this.socketServer = undefined; + } + } + + public async Start(): Promise { + const host = typeof this.args.host === 'string' && this.args.host.trim().length > 0 ? this.args.host!.trim() : 'localhost'; + const socketServer = this.socketServer = this.serviceContainer.get(ISocketServer); + const port = await socketServer.Start({ port: this.args.port, host }); + socketServer.client.then(socket => { + // This is required to prevent the launcher from aborting if the PTVSD process spits out any errors in stderr stream. + this.isRunning = true; + this.debugClientConnected.resolve(true); + this.clientSocket.resolve(socket); + }).catch(ex => { + this.debugClientConnected.reject(ex); + this.clientSocket.reject(ex); + }); + return { port, host }; + } +} diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index d7d848eb79fd..63ca19ac3e4a 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -2,14 +2,14 @@ "use strict"; // This line should always be right on top. -// tslint:disable-next-line:no-any +// tslint:disable:no-any no-floating-promises if ((Reflect as any).metadata === undefined) { // tslint:disable-next-line:no-require-imports no-var-requires require('reflect-metadata'); } import * as fs from "fs"; import * as path from "path"; -import { DebugSession, Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable } from "vscode-debugadapter"; +import { Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable, LoggingDebugSession, logger } from "vscode-debugadapter"; import { ThreadEvent } from "vscode-debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import { DEBUGGER } from '../../client/telemetry/constants'; @@ -25,6 +25,7 @@ import { BaseDebugServer } from "./DebugServers/BaseDebugServer"; import { PythonProcess } from "./PythonProcess"; import { IS_WINDOWS } from './Common/Utils'; import { sendPerformanceTelemetry, capturePerformanceTelemetry, PerformanceTelemetryCondition } from "./Common/telemetry"; +import { LogLevel } from "vscode-debugadapter/lib/logger"; const CHILD_ENUMEARATION_TIMEOUT = 5000; @@ -33,7 +34,7 @@ interface IDebugVariable { evaluateChildren?: Boolean; } -export class PythonDebugger extends DebugSession { +export class PythonDebugger extends LoggingDebugSession { private _variableHandles: Handles; private _pythonStackFrames: Handles; private breakPointCounter: number = 0; @@ -41,14 +42,14 @@ export class PythonDebugger extends DebugSession { private registeredBreakpointsByFileName: Map; private debuggerLoaded: Promise; private debuggerLoadedPromiseResolve: () => void; - private debugClient?: DebugClient; + private debugClient?: DebugClient<{}>; private configurationDone: Promise; private configurationDonePromiseResolve?: () => void; private lastException?: IPythonException; private _supportsRunInTerminalRequest: boolean; private terminateEventSent: boolean; public constructor(debuggerLinesStartAt1: boolean, isServer: boolean) { - super(debuggerLinesStartAt1, isServer === true); + super(path.join(__dirname, '..', '..', '..', 'debug.log'), debuggerLinesStartAt1, isServer === true); this._variableHandles = new Handles(); this._pythonStackFrames = new Handles(); this.registeredBreakpoints = new Map(); @@ -210,6 +211,9 @@ export class PythonDebugger extends DebugSession { } @capturePerformanceTelemetry('launch') protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + if (args.diagnosticLogging === true) { + logger.setup(LogLevel.Verbose, args.logToFile === true); + } // Some versions may still exist with incorrect launch.json values const setting = '${config.python.pythonPath}'; if (args.pythonPath === setting) { @@ -274,7 +278,7 @@ export class PythonDebugger extends DebugSession { const that = this; this.startDebugServer().then(dbgServer => { - return that.debugClient!.LaunchApplicationToDebug(dbgServer, that.unhandledProcessError.bind(that)); + return that.debugClient!.LaunchApplicationToDebug(dbgServer); }).catch(error => { this.sendEvent(new OutputEvent(`${error}${'\n'}`, "stderr")); response.success = false; @@ -285,19 +289,10 @@ export class PythonDebugger extends DebugSession { this.sendErrorResponse(response, 200, errorMsg); }); } - protected unhandledProcessError(error: any) { - if (!error) { return; } - let errorMsg = typeof error === "string" ? error : ((error.message && error.message.length > 0) ? error.message : ""); - if (isNotInstalledError(error)) { - errorMsg = `Failed to launch the Python Process, please validate the path '${this.launchArgs.pythonPath}'`; - } - if (errorMsg.length > 0) { - this.sendEvent(new OutputEvent(`${errorMsg}${'\n'}`, "stderr")); - } - this.terminateEventSent = true; - this.sendEvent(new TerminatedEvent()); - } protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { + if ((args as any).diagnosticLogging === true) { + logger.setup(LogLevel.Verbose, (args as any).logToFile === true); + } this.sendEvent(new TelemetryEvent(DEBUGGER, { trigger: 'attach' })); this.attachArgs = args; @@ -308,7 +303,7 @@ export class PythonDebugger extends DebugSession { this.canStartDebugger().then(() => { return this.startDebugServer(); }).then(dbgServer => { - return that.debugClient!.LaunchApplicationToDebug(dbgServer, () => { }); + return that.debugClient!.LaunchApplicationToDebug(dbgServer); }).catch(error => { this.sendEvent(new OutputEvent(`${error}${'\n'}`, "stderr")); this.sendErrorResponse(that.entryResponse!, 2000, error); @@ -741,4 +736,4 @@ export class PythonDebugger extends DebugSession { } } -DebugSession.run(PythonDebugger); +LoggingDebugSession.run(PythonDebugger); diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts new file mode 100644 index 000000000000..f00d109d8740 --- /dev/null +++ b/src/client/debugger/mainV2.ts @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length +if ((Reflect as any).metadata === undefined) { + // tslint:disable-next-line:no-require-imports no-var-requires + require('reflect-metadata'); +} + +import { Socket } from 'net'; +import * as path from 'path'; +import { PassThrough } from 'stream'; +import { DebugSession, ErrorDestination, logger, OutputEvent } from 'vscode-debugadapter'; +import { LogLevel } from 'vscode-debugadapter/lib/logger'; +import { Event } from 'vscode-debugadapter/lib/messages'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { createDeferred, isNotInstalledError } from '../common/helpers'; +import { IServiceContainer } from '../ioc/types'; +import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; +import { DebugClient } from './DebugClients/DebugClient'; +import { CreateLaunchDebugClient } from './DebugClients/DebugFactory'; +import { BaseDebugServer } from './DebugServers/BaseDebugServer'; +import { initializeIoc } from './serviceRegistry'; +import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; + +export class PythonDebugger extends DebugSession { + public debugServer?: BaseDebugServer; + public debugClient?: DebugClient<{}>; + public client = createDeferred(); + private supportsRunInTerminalRequest: boolean; + constructor(private readonly serviceContainer: IServiceContainer, + isServer?: boolean) { + super(false, isServer); + } + public static async run() { + const serviceContainer = initializeIoc(); + const debugStreamProvider = serviceContainer.get(IDebugStreamProvider); + const { input, output } = await debugStreamProvider.getInputAndOutputStreams(); + const isServerMode = debugStreamProvider.useDebugSocketStream; + const protocolMessageWriter = serviceContainer.get(IProtocolMessageWriter); + // tslint:disable-next-line:no-empty + logger.init(() => { }, path.join(__dirname, '..', '..', '..', 'experimental_debug.log')); + const stdin = input; + const stdout = output; + + try { + + stdin.pause(); + + const handshakeDebugOutStream = new PassThrough(); + const handshakeDebugInStream = new PassThrough(); + + const throughOutStream = new PassThrough(); + const throughInStream = new PassThrough(); + + const inputProtocolParser = serviceContainer.get(IProtocolParser); + inputProtocolParser.connect(throughInStream); + + const outputProtocolParser = serviceContainer.get(IProtocolParser); + outputProtocolParser.connect(throughOutStream); + + const protocolLogger = serviceContainer.get(IProtocolLogger); + protocolLogger.connect(throughInStream, throughOutStream); + + // Keep track of the initialize message, we'll need to re-send this to ptvsd, for bootstrapping. + const initializeRequest = new Promise(resolve => { + inputProtocolParser.on('request_initialize', (data) => { + resolve(data); + inputProtocolParser.dispose(); + }); + }); + + throughOutStream.pipe(stdout); + handshakeDebugOutStream.pipe(throughOutStream); + + // Lets start our debugger. + const session = new PythonDebugger(serviceContainer, isServerMode); + session.setRunAsServer(isServerMode); + + function dispose() { + session.shutdown(); + } + outputProtocolParser.once('event_terminated', dispose); + outputProtocolParser.once('response_disconnect', dispose); + if (!isServerMode) { + process.on('SIGTERM', dispose); + } + + session.on('_py_enable_protocol_logging', enabled => { + if (enabled) { + logger.setup(LogLevel.Verbose, true); + protocolLogger.setup(logger); + } else { + protocolLogger.dispose(); + } + }); + + outputProtocolParser.on('response_launch', async () => { + const debuggerSocket = await session.debugServer!.client; + const debugSoketProtocolParser = serviceContainer.get(IProtocolParser); + debugSoketProtocolParser.connect(debuggerSocket); + + // The PTVSD process has launched, now send the initialize request to it. + const request = await initializeRequest; + protocolMessageWriter.write(debuggerSocket, request); + + // Wait for PTVSD to reply back with initialized event. + debugSoketProtocolParser.once('event_initialized', (initialized: DebugProtocol.InitializedEvent) => { + throughInStream.unpipe(handshakeDebugInStream); + + throughInStream.pipe(debuggerSocket); + + debuggerSocket.pipe(throughOutStream); + + // Forward the initialized event sent by PTVSD onto VSCode. + protocolMessageWriter.write(throughOutStream, initialized); + }); + }); + + throughInStream.pipe(handshakeDebugInStream); + stdin.pipe(throughInStream); + session.start(handshakeDebugInStream, handshakeDebugOutStream); + stdin.resume(); + } catch (ex) { + logger.error(`Debugger crashed.${ex.message}`); + protocolMessageWriter.write(stdout, new Event('error', `Debugger Error: ${ex.message}`)); + protocolMessageWriter.write(stdout, new OutputEvent(ex.toString(), 'stderr')); + } + } + public shutdown(): void { + if (this.debugServer) { + this.debugServer.Stop(); + this.debugServer = undefined; + } + if (this.debugClient) { + this.debugClient.Stop(); + this.debugClient = undefined; + } + super.shutdown(); + } + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + const body = response.body!; + + body.supportsExceptionInfoRequest = true; + body.supportsConfigurationDoneRequest = true; + body.supportsConditionalBreakpoints = true; + body.supportsSetVariable = true; + body.supportsExceptionOptions = true; + body.exceptionBreakpointFilters = [ + { + filter: 'raised', + label: 'Raised Exceptions', + default: true + }, + { + filter: 'uncaught', + label: 'Uncaught Exceptions', + default: true + } + ]; + if (typeof args.supportsRunInTerminalRequest === 'boolean') { + this.supportsRunInTerminalRequest = args.supportsRunInTerminalRequest; + } + this.sendResponse(response); + } + protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + this.sendResponse(response); + } + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const enableLogging = args.logToFile === true; + this.emit('_py_enable_protocol_logging', enableLogging); + + this.emit('_py_pre_launch'); + + this.startPTVSDDebugger(args) + .then(() => this.sendResponse(response)) + .catch(ex => { + const message = this.getErrorUserFriendlyMessage(args, ex) || 'Debug Error'; + this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); + }); + } + private async startPTVSDDebugger(args: LaunchRequestArguments) { + const launcher = CreateLaunchDebugClient(args, this, this.supportsRunInTerminalRequest); + this.debugServer = launcher.CreateDebugServer(undefined, this.serviceContainer); + const serverInfo = await this.debugServer!.Start(); + return launcher.LaunchApplicationToDebug(serverInfo); + } + private getErrorUserFriendlyMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { + if (!error) { + return; + } + const errorMsg = typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); + if (isNotInstalledError(error)) { + return `Failed to launch the Python Process, please validate the path '${launchArgs.pythonPath}'`; + } else { + return errorMsg; + } + } +} + +PythonDebugger.run().catch(ex => { + // Not necessary except for perhaps debugging. +}); diff --git a/src/client/debugger/serviceRegistry.ts b/src/client/debugger/serviceRegistry.ts new file mode 100644 index 000000000000..b3e2a657d32a --- /dev/null +++ b/src/client/debugger/serviceRegistry.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Container } from 'inversify'; +import { SocketServer } from '../common/net/socket/socketServer'; +import { FileSystem } from '../common/platform/fileSystem'; +import { PlatformService } from '../common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { CurrentProcess } from '../common/process/currentProcess'; +import { ICurrentProcess, ISocketServer } from '../common/types'; +import { ServiceContainer } from '../ioc/container'; +import { ServiceManager } from '../ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { DebugStreamProvider } from './Common/debugStreamProvider'; +import { ProtocolLogger } from './Common/protocolLogger'; +import { ProtocolParser } from './Common/protocolParser'; +import { ProtocolMessageWriter } from './Common/protocolWriter'; +import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; + +export function initializeIoc(): IServiceContainer { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + const serviceContainer = new ServiceContainer(cont); + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + registerTypes(serviceManager); + return serviceContainer; +} + +function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ICurrentProcess, CurrentProcess); + serviceManager.addSingleton(IDebugStreamProvider, DebugStreamProvider); + serviceManager.addSingleton(IProtocolLogger, ProtocolLogger); + serviceManager.add(IProtocolParser, ProtocolParser); + serviceManager.addSingleton(IFileSystem, FileSystem); + serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingleton(ISocketServer, SocketServer); + serviceManager.addSingleton(IProtocolMessageWriter, ProtocolMessageWriter); +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts new file mode 100644 index 000000000000..4bb931ca8a91 --- /dev/null +++ b/src/client/debugger/types.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Socket } from 'net'; +import { Readable } from 'stream'; +import { Disposable } from 'vscode'; +import { Logger } from 'vscode-debugadapter'; +import { Message } from 'vscode-debugadapter/lib/messages'; + +export interface IDebugLauncherScriptProvider { + getLauncherFilePath(): string; +} + +export const IProtocolParser = Symbol('IProtocolParser'); +export interface IProtocolParser extends Disposable { + connect(stream: Readable): void; + once(event: string | symbol, listener: Function): this; + on(event: string | symbol, listener: Function): this; +} + +export const IProtocolLogger = Symbol('IProtocolLogger'); +export interface IProtocolLogger extends Disposable { + connect(inputStream: Readable, outputStream: Readable): void; + setup(logger: Logger.ILogger): void; +} + +export const IDebugStreamProvider = Symbol('IDebugStreamProvider'); +export interface IDebugStreamProvider { + readonly useDebugSocketStream: boolean; + getInputAndOutputStreams(): Promise<{ input: NodeJS.ReadStream | Socket; output: NodeJS.WriteStream | Socket }>; +} + +export const IProtocolMessageWriter = Symbol('IProtocolMessageWriter'); +export interface IProtocolMessageWriter { + write(stream: Socket | NodeJS.WriteStream, message: Message): void; +} diff --git a/src/test/common/misc.test.ts b/src/test/common/misc.test.ts new file mode 100644 index 000000000000..549c705d77f1 --- /dev/null +++ b/src/test/common/misc.test.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { isTestExecution } from '../../client/common/constants'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('Common - Misc', () => { + test('Ensure its identified that we\re running unit tests', () => { + expect(isTestExecution()).to.be.equal(true, 'incorrect'); + }); +}); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts index f11873b08afc..fa9331ce60fa 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.test.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import * as fs from 'fs-extra'; +import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { FileSystem } from '../../../client/common/platform/fileSystem'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; @@ -11,11 +12,18 @@ import { IFileSystem, IPlatformService } from '../../../client/common/platform/t suite('FileSystem', () => { let platformService: TypeMoq.IMock; let fileSystem: IFileSystem; + const fileToAppendTo = path.join(__dirname, 'created_for_testing_dummy.txt'); setup(() => { platformService = TypeMoq.Mock.ofType(); fileSystem = new FileSystem(platformService.object); + cleanTestFiles(); }); - + teardown(cleanTestFiles); + function cleanTestFiles() { + if (fs.existsSync(fileToAppendTo)) { + fs.unlinkSync(fileToAppendTo); + } + } test('ReadFile returns contents of a file', async () => { const file = __filename; const expectedContents = await fs.readFile(file).then(buffer => buffer.toString()); @@ -59,4 +67,11 @@ suite('FileSystem', () => { test('Case sensitivity is not ignored when comparing file names on linux', async () => { caseSensitivityFileCheck(false, false, true); }); + + test('Test appending to file', async () => { + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + fileSystem.appendFileSync(fileToAppendTo, dataToAppend); + const fileContents = await fileSystem.readFile(fileToAppendTo); + expect(fileContents).to.be.equal(dataToAppend); + }); }); diff --git a/src/test/common/process/currentProcess.test.ts b/src/test/common/process/currentProcess.test.ts new file mode 100644 index 000000000000..d37077d05a03 --- /dev/null +++ b/src/test/common/process/currentProcess.test.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { ICurrentProcess } from '../../../client/common/types'; + +suite('Current Process', () => { + let currentProcess: ICurrentProcess; + setup(() => { + currentProcess = new CurrentProcess(); + }); + + test('Current process argv is returned', () => { + expect(currentProcess.argv).to.deep.equal(process.argv); + }); + + test('Current process env is returned', () => { + expect(currentProcess.env).to.deep.equal(process.env); + }); + + test('Current process stdin is returned', () => { + expect(currentProcess.stdin).to.deep.equal(process.stdin); + }); + + test('Current process stdout is returned', () => { + expect(currentProcess.stdout).to.deep.equal(process.stdout); + }); +}); diff --git a/src/test/debugger/common/debugStreamProvider.test.ts b/src/test/debugger/common/debugStreamProvider.test.ts new file mode 100644 index 000000000000..f58dc141a250 --- /dev/null +++ b/src/test/debugger/common/debugStreamProvider.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as getFreePort from 'get-port'; +import * as net from 'net'; +import * as TypeMoq from 'typemoq'; +import { ICurrentProcess } from '../../../client/common/types'; +import { DebugStreamProvider } from '../../../client/debugger/Common/debugStreamProvider'; +import { IDebugStreamProvider } from '../../../client/debugger/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { sleep } from '../../common'; + +// tslint:disable-next-line:max-func-body-length +suite('Debugging - Stream Provider', () => { + let streamProvider: IDebugStreamProvider; + let serviceContainer: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + streamProvider = new DebugStreamProvider(serviceContainer.object); + }); + test('Process is returned as is if there is no port number if args', async () => { + const mockProcess = { argv: [], env: [], stdin: '1234', stdout: '5678' }; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); + + const streams = await streamProvider.getInputAndOutputStreams(); + expect(streams.input).to.be.equal(mockProcess.stdin); + expect(streams.output).to.be.equal(mockProcess.stdout); + }); + test('Starts a socketserver on the port provided and returns the client socket', async () => { + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const mockProcess = { argv: ['node', 'index.js', `--server=${port}`], env: [], stdin: '1234', stdout: '5678' }; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); + + const streamsPromise = streamProvider.getInputAndOutputStreams(); + await sleep(1); + + await new Promise(resolve => { + net.connect({ port, host: 'localhost' }, resolve); + }); + + const streams = await streamsPromise; + expect(streams.input).to.not.be.equal(mockProcess.stdin); + expect(streams.output).to.not.be.equal(mockProcess.stdout); + }); + test('Ensure existence of port is identified', async () => { + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const mockProcess = { argv: ['node', 'index.js', `--server=${port}`], env: [], stdin: '1234', stdout: '5678' }; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); + + expect(streamProvider.useDebugSocketStream).to.be.equal(true, 'incorrect'); + }); + test('Ensure non-existence of port is identified', async () => { + const port = await getFreePort({ host: 'localhost', port: 3000 }); + const mockProcess = { argv: ['node', 'index.js', `--other=${port}`], env: [], stdin: '1234', stdout: '5678' }; + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); + + expect(streamProvider.useDebugSocketStream).to.not.be.equal(true, 'incorrect'); + }); +}); diff --git a/src/test/debugger/common/protocolWriter.test.ts b/src/test/debugger/common/protocolWriter.test.ts new file mode 100644 index 000000000000..762ac90187c4 --- /dev/null +++ b/src/test/debugger/common/protocolWriter.test.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { Transform } from 'stream'; +import { InitializedEvent } from 'vscode-debugadapter/lib/main'; +import { ProtocolMessageWriter } from '../../../client/debugger/Common/protocolWriter'; + +suite('Debugging - Protocol Writer', () => { + test('Test request, response and event messages', async () => { + let dataWritten = ''; + const throughOutStream = new Transform({ + transform: (chunk, encoding, callback) => { + dataWritten += (chunk as Buffer).toString('utf8'); + callback(null, chunk); + } + }); + + const message = new InitializedEvent(); + message.seq = 123; + const writer = new ProtocolMessageWriter(); + writer.write(throughOutStream, message); + + const json = JSON.stringify(message); + const expectedMessage = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; + expect(dataWritten).to.be.equal(expectedMessage); + }); +}); diff --git a/src/test/debugger/common/protocoloLogger.test.ts b/src/test/debugger/common/protocoloLogger.test.ts new file mode 100644 index 000000000000..2d1d7e36a50d --- /dev/null +++ b/src/test/debugger/common/protocoloLogger.test.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PassThrough } from 'stream'; +import * as TypeMoq from 'typemoq'; +import { Logger } from 'vscode-debugadapter'; +import { ProtocolLogger } from '../../../client/debugger/Common/protocolLogger'; +import { IProtocolLogger } from '../../../client/debugger/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Debugging - Protocol Logger', () => { + let protocolLogger: IProtocolLogger; + setup(() => { + protocolLogger = new ProtocolLogger(); + }); + test('Ensure messages are buffered untill logger is provided', async () => { + const inputStream = new PassThrough(); + const outputStream = new PassThrough(); + + protocolLogger.connect(inputStream, outputStream); + + inputStream.write('A'); + outputStream.write('1'); + + inputStream.write('B'); + inputStream.write('C'); + + outputStream.write('2'); + outputStream.write('3'); + + const logger = TypeMoq.Mock.ofType(); + protocolLogger.setup(logger.object); + + logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(3)); + logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(3)); + + const expectedLogFileContents = ['A', '1', 'B', 'C', '2', '3']; + for (const expectedContent of expectedLogFileContents) { + logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); + } + }); + test('Ensure messages are are logged as they arrive', async () => { + const inputStream = new PassThrough(); + const outputStream = new PassThrough(); + + protocolLogger.connect(inputStream, outputStream); + + inputStream.write('A'); + outputStream.write('1'); + + const logger = TypeMoq.Mock.ofType(); + protocolLogger.setup(logger.object); + + inputStream.write('B'); + inputStream.write('C'); + + outputStream.write('2'); + outputStream.write('3'); + + logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(3)); + logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(3)); + + const expectedLogFileContents = ['A', '1', 'B', 'C', '2', '3']; + for (const expectedContent of expectedLogFileContents) { + logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); + } + }); + test('Ensure nothing is logged once logging is disabled', async () => { + const inputStream = new PassThrough(); + const outputStream = new PassThrough(); + + protocolLogger.connect(inputStream, outputStream); + const logger = TypeMoq.Mock.ofType(); + protocolLogger.setup(logger.object); + + inputStream.write('A'); + outputStream.write('1'); + + protocolLogger.dispose(); + + inputStream.write('B'); + inputStream.write('C'); + + outputStream.write('2'); + outputStream.write('3'); + + logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(1)); + logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(1)); + + const expectedLogFileContents = ['A', '1']; + const notExpectedLogFileContents = ['B', 'C', '2', '3']; + + for (const expectedContent of expectedLogFileContents) { + logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); + } + for (const notExpectedContent of notExpectedLogFileContents) { + logger.verify(l => l.verbose(notExpectedContent), TypeMoq.Times.never()); + } + }); +}); diff --git a/src/test/debugger/common/protocolparser.test.ts b/src/test/debugger/common/protocolparser.test.ts new file mode 100644 index 000000000000..3434940c383c --- /dev/null +++ b/src/test/debugger/common/protocolparser.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { PassThrough } from 'stream'; +import { createDeferred } from '../../../client/common/helpers'; +import { ProtocolParser } from '../../../client/debugger/Common/protocolParser'; +import { sleep } from '../../common'; + +suite('Debugging - Protocol Parser', () => { + test('Test request, response and event messages', async () => { + const stream = new PassThrough(); + + const protocolParser = new ProtocolParser(); + protocolParser.connect(stream); + let messagesDetected = 0; + protocolParser.on('data', () => messagesDetected += 1); + const requestDetected = new Promise(resolve => { + protocolParser.on('request_initialize', () => resolve(true)); + }); + const responseDetected = new Promise(resolve => { + protocolParser.on('response_initialize', () => resolve(true)); + }); + const eventDetected = new Promise(resolve => { + protocolParser.on('event_initialized', () => resolve(true)); + }); + + stream.write('Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}'); + await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); + + stream.write('Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}'); + await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); + + stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); + await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); + + expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); + }); + test('Ensure messages are not received after disposing the parser', async () => { + const stream = new PassThrough(); + + const protocolParser = new ProtocolParser(); + protocolParser.connect(stream); + let messagesDetected = 0; + protocolParser.on('data', () => messagesDetected += 1); + const requestDetected = new Promise(resolve => { + protocolParser.on('request_initialize', () => resolve(true)); + }); + stream.write('Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}'); + await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); + + protocolParser.dispose(); + + const responseDetected = createDeferred(); + protocolParser.on('response_initialize', () => responseDetected.resolve(true)); + + stream.write('Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}'); + // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). + await sleep(1000); + expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); + }); +}); diff --git a/src/test/debugger/core/capabilities.test.ts b/src/test/debugger/core/capabilities.test.ts new file mode 100644 index 000000000000..63bc7314a0e1 --- /dev/null +++ b/src/test/debugger/core/capabilities.test.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// 1. Ensure the capabilitites of the debugger sent into the response to initialize matches +// that of the underlying (ptvsd) debugger +// I.e. ensure the response sent by us matches the response sent by ptvsd to the initialize request. diff --git a/src/test/debugger/launcherScriptProvider.test.ts b/src/test/debugger/launcherScriptProvider.test.ts new file mode 100644 index 000000000000..147869aa8f20 --- /dev/null +++ b/src/test/debugger/launcherScriptProvider.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; +import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/DebugClients/launcherProvider'; + +suite('Debugger - Launcher Script Provider', () => { + test('Ensure stable debugger gets the old launcher from PythonTools directory', () => { + const launcherPath = new DebuggerLauncherScriptProvider().getLauncherFilePath(); + const expectedPath = path.join(path.dirname(__dirname), '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_launcher.py'); + expect(launcherPath).to.be.equal(expectedPath); + expect(fs.existsSync(launcherPath)).to.be.equal(true, 'file does not exist'); + }); + test('Ensure stable debugger when not debugging gets the non debnug launcher from PythonTools directory', () => { + const launcherPath = new NoDebugLauncherScriptProvider().getLauncherFilePath(); + const expectedPath = path.join(path.dirname(__dirname), '..', '..', 'pythonFiles', 'PythonTools', 'visualstudio_py_launcher_nodebug.py'); + expect(launcherPath).to.be.equal(expectedPath); + expect(fs.existsSync(launcherPath)).to.be.equal(true, 'file does not exist'); + }); + test('Ensure experimental debugger gets the new launcher from experimentals directory', () => { + const launcherPath = new DebuggerV2LauncherScriptProvider().getLauncherFilePath(); + const expectedPath = path.join(path.dirname(__dirname), '..', '..', 'pythonFiles', 'experimental', 'ptvsd_launcher.py'); + expect(launcherPath).to.be.equal(expectedPath); + expect(fs.existsSync(launcherPath)).to.be.equal(true, 'file does not exist'); + }); +}); diff --git a/src/test/debugger/portAndHost.test.ts b/src/test/debugger/portAndHost.test.ts index 3094e17c2e56..dcde236f0ba7 100644 --- a/src/test/debugger/portAndHost.test.ts +++ b/src/test/debugger/portAndHost.test.ts @@ -6,122 +6,115 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as getFreePort from 'get-port'; import * as net from 'net'; import * as path from 'path'; -import { ThreadEvent } from 'vscode-debugadapter'; import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { createDeferred } from '../../client/common/helpers'; import { LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; -import { initialize } from '../initialize'; +import { IS_MULTI_ROOT_TEST } from '../initialize'; use(chaiAsPromised); const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); const DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'Main.js'); - -// tslint:disable-next-line:max-func-body-length -suite('Standard Debugging', () => { - let debugClient: DebugClient; - suiteSetup(initialize); - - setup(async () => { - await new Promise(resolve => setTimeout(resolve, 1000)); - debugClient = new DebugClient('node', DEBUG_ADAPTER, 'python'); - await debugClient.start(); - }); - teardown(async () => { - // Wait for a second before starting another test (sometimes, sockets take a while to get closed). - await new Promise(resolve => setTimeout(resolve, 1000)); - try { - debugClient.stop(); - // tslint:disable-next-line:no-empty - } catch (ex) { } - }); - - async function testDebuggingWithProvidedPort(port?: number | undefined, host?: string | undefined) { - const args: LaunchRequestArguments = { - program: path.join(debugFilesPath, 'simplePrint.py'), - cwd: debugFilesPath, - stopOnEntry: false, - debugOptions: ['RedirectOutput'], - pythonPath: 'python', - args: [], - envFile: '', - port, - host - }; - const threadIdPromise = createDeferred(); - debugClient.on('thread', (data: ThreadEvent) => { - if (data.body.reason === 'started') { - threadIdPromise.resolve(data.body.threadId); +const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'mainV2.js'); + +[DEBUG_ADAPTER, EXPERIMENTAL_DEBUG_ADAPTER].forEach(testAdapterFilePath => { + const debugAdapterFileName = path.basename(testAdapterFilePath); + const debuggerType = debugAdapterFileName === 'Main.js' ? 'python' : 'pythonExperimental'; + // tslint:disable-next-line:max-func-body-length + suite(`Standard Debugging of ports and hosts: ${debuggerType}`, () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); } + if (debuggerType !== 'python') { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); + await debugClient.start(); }); - - const initializePromise = debugClient.initializeRequest({ - adapterID: 'python', - linesStartAt1: true, - columnsStartAt1: true, - supportsRunInTerminalRequest: true, - pathFormat: 'path' + teardown(async () => { + // Wait for a second before starting another test (sometimes, sockets take a while to get closed). + await new Promise(resolve => setTimeout(resolve, 1000)); + try { + // tslint:disable-next-line:no-empty + debugClient.stop().catch(() => { }); + // tslint:disable-next-line:no-empty + } catch (ex) { } }); - await debugClient.launch(args); - await initializePromise; - - // Wait till we get the thread of the program. - const threadId = await threadIdPromise.promise; - expect(threadId).to.be.greaterThan(0, 'ThreadId not received'); - - // Confirm port is in use (if one was provided). - if (typeof port === 'number' && port > 0) { - // We know the port 'debuggerPort' was free, now that the debugger has started confirm that this port is no longer free. - const portBasedOnDebuggerPort = await getFreePort({ host: 'localhost', port }); - expect(portBasedOnDebuggerPort).is.not.equal(port, 'Port assigned to debugger not used by the debugger'); + function buildLauncArgs(pythonFile: string, stopOnEntry: boolean = false, port?: number, host?: string): LaunchRequestArguments { + // pythonPath: '/Users/donjayamanne/anaconda3/envs/py36/bin/python', + return { + program: path.join(debugFilesPath, pythonFile), + cwd: debugFilesPath, + stopOnEntry, + debugOptions: ['RedirectOutput'], + pythonPath: 'python', + args: [], + envFile: '', + host, port, + type: debuggerType + }; } - // Continue the program. - debugClient.continueRequest({ threadId }); - - await debugClient.waitForEvent('terminated'); - } + async function testDebuggingWithProvidedPort(port?: number | undefined, host?: string | undefined) { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('startAndWait.py', false, port, host)), + debugClient.waitForEvent('initialized') + ]); + + // Confirm port is in use (if one was provided). + if (typeof port === 'number' && port > 0) { + // We know the port 'debuggerPort' was free, now that the debugger has started confirm that this port is no longer free. + const portBasedOnDebuggerPort = await getFreePort({ host: 'localhost', port }); + expect(portBasedOnDebuggerPort).is.not.equal(port, 'Port assigned to debugger not used by the debugger'); + } + } - test('Confirm debuggig works if both port and host are not provided', async () => { - await testDebuggingWithProvidedPort(); - }); + test('Confirm debuggig works if both port and host are not provided', async () => { + await testDebuggingWithProvidedPort(); + }); - test('Confirm debuggig works if port=0', async () => { - await testDebuggingWithProvidedPort(0, 'localhost'); - }); + test('Confirm debuggig works if port=0', async () => { + await testDebuggingWithProvidedPort(0, 'localhost'); + }); - test('Confirm debuggig works if port=0 or host=localhost', async () => { - await testDebuggingWithProvidedPort(0, 'localhost'); - }); + test('Confirm debuggig works if port=0 or host=localhost', async () => { + await testDebuggingWithProvidedPort(0, 'localhost'); + }); - test('Confirm debuggig works if port=0 or host=127.0.0.1', async () => { - await testDebuggingWithProvidedPort(0, '127.0.0.1'); - }); + test('Confirm debuggig works if port=0 or host=127.0.0.1', async () => { + await testDebuggingWithProvidedPort(0, '127.0.0.1'); + }); - test('Confirm debuggig fails when an invalid host is provided', async () => { - const promise = testDebuggingWithProvidedPort(0, 'xyz123409924ple_ewf'); - let exception: Error | undefined; - try { - await promise; - } catch (ex) { - exception = ex; - } - expect(exception!.message).contains('ENOTFOUND', 'Debugging failed for some other reason'); - }); - test('Confirm debuggig fails when provided port is in use', async () => { - // tslint:disable-next-line:no-empty - const server = net.createServer((s) => { }); - const port = await new Promise((resolve, reject) => server.listen({ host: 'localhost', port: 0 }, () => resolve(server.address().port))); - let exception: Error | undefined; - try { - await testDebuggingWithProvidedPort(port); - } catch (ex) { - exception = ex; - } finally { - server.close(); - } - expect(exception!.message).contains('EADDRINUSE', 'Debugging failed for some other reason'); + test('Confirm debuggig fails when an invalid host is provided', async () => { + const promise = testDebuggingWithProvidedPort(0, 'xyz123409924ple_ewf'); + let exception: Error | undefined; + try { + await promise; + } catch (ex) { + exception = ex; + } + expect(exception!.message).contains('ENOTFOUND', 'Debugging failed for some other reason'); + }); + test('Confirm debuggig fails when provided port is in use', async () => { + // tslint:disable-next-line:no-empty + const server = net.createServer((s) => { }); + const port = await new Promise((resolve, reject) => server.listen({ host: 'localhost', port: 0 }, () => resolve(server.address().port))); + let exception: Error | undefined; + try { + await testDebuggingWithProvidedPort(port); + } catch (ex) { + exception = ex; + } finally { + server.close(); + } + expect(exception!.message).contains('EADDRINUSE', 'Debugging failed for some other reason'); + }); }); }); diff --git a/src/test/mocks/process.ts b/src/test/mocks/process.ts index 451f19ba9964..117bccde7f6a 100644 --- a/src/test/mocks/process.ts +++ b/src/test/mocks/process.ts @@ -2,10 +2,20 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; +import * as TypeMoq from 'typemoq'; import { ICurrentProcess } from '../../client/common/types'; import { EnvironmentVariables } from '../../client/common/variables/types'; @injectable() export class MockProcess implements ICurrentProcess { constructor(public env: EnvironmentVariables = { ...process.env }) { } + public get argv(): string[] { + return []; + } + public get stdout(): NodeJS.WriteStream { + return TypeMoq.Mock.ofType().object; + } + public get stdin(): NodeJS.ReadStream { + return TypeMoq.Mock.ofType().object; + } } diff --git a/src/test/pythonFiles/debugging/startAndWait.py b/src/test/pythonFiles/debugging/startAndWait.py new file mode 100644 index 000000000000..c9f1a913e98d --- /dev/null +++ b/src/test/pythonFiles/debugging/startAndWait.py @@ -0,0 +1,2 @@ +import time +time.sleep(10) diff --git a/yarn.lock b/yarn.lock index 48cfc8fe0c7c..0f981cb77091 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3184,6 +3184,10 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" +pidusage@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-1.2.0.tgz#65ee96ace4e08a4cd3f9240996c85b367171ee92" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -4170,6 +4174,10 @@ underscore@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" +unicode@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-10.0.0.tgz#e5d51c1db93b6c71a0b879e0b0c4af7e6fdf688e" + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"