From 157f392165ac177707587117ecf76faffe5c4062 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 16 Apr 2018 20:32:25 -0700 Subject: [PATCH] Running python code without debugging using the experimental debugger (#1373) Fixes #882 Remove python file used to launch the PTVSD debugger Added ability to run code without debugging using PTVSD Launching PTVSD using -m (run as a python module) --- pythonFiles/experimental/ptvsd_launcher.py | 96 --------------- .../debugger/DebugClients/DebugFactory.ts | 16 ++- .../debugger/DebugClients/LocalDebugClient.ts | 23 ++-- .../debugger/DebugClients/launcherProvider.ts | 10 +- .../DebugClients/localDebugClientV2.ts | 29 +++++ .../debugger/DebugClients/nonDebugClientV2.ts | 35 ++++++ src/client/debugger/mainV2.ts | 5 + .../debugger/launcherScriptProvider.test.ts | 8 +- src/test/debugger/run.test.ts | 112 ++++++++++++++++++ .../pythonFiles/debugging/sampleWithSleep.py | 8 ++ 10 files changed, 220 insertions(+), 122 deletions(-) delete mode 100644 pythonFiles/experimental/ptvsd_launcher.py create mode 100644 src/client/debugger/DebugClients/localDebugClientV2.ts create mode 100644 src/client/debugger/DebugClients/nonDebugClientV2.ts create mode 100644 src/test/debugger/run.test.ts create mode 100644 src/test/pythonFiles/debugging/sampleWithSleep.py diff --git a/pythonFiles/experimental/ptvsd_launcher.py b/pythonFiles/experimental/ptvsd_launcher.py deleted file mode 100644 index 3be94266f010..000000000000 --- a/pythonFiles/experimental/ptvsd_launcher.py +++ /dev/null @@ -1,96 +0,0 @@ -# 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://github.com/Microsoft/vscode-python/issues/new - -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/debugger/DebugClients/DebugFactory.ts b/src/client/debugger/DebugClients/DebugFactory.ts index 21229631727e..f8c3b6378e9c 100644 --- a/src/client/debugger/DebugClients/DebugFactory.ts +++ b/src/client/debugger/DebugClients/DebugFactory.ts @@ -1,17 +1,25 @@ import { DebugSession } from 'vscode-debugadapter'; import { AttachRequestArguments, LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; import { DebugClient } from './DebugClient'; -import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; +import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; import { LocalDebugClient } from './LocalDebugClient'; +import { LocalDebugClientV2 } from './localDebugClientV2'; import { NonDebugClient } from './NonDebugClient'; +import { NonDebugClientV2 } from './nonDebugClientV2'; import { RemoteDebugClient } from './RemoteDebugClient'; export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> { + let launchScriptProvider: IDebugLauncherScriptProvider; + let debugClientClass: typeof LocalDebugClient; if (launchRequestOptions.noDebug === true) { - return new NonDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, new NoDebugLauncherScriptProvider()); + launchScriptProvider = new NoDebugLauncherScriptProvider(); + debugClientClass = launchRequestOptions.type === 'pythonExperimental' ? NonDebugClientV2 : NonDebugClient; + } else { + launchScriptProvider = new DebuggerLauncherScriptProvider(); + debugClientClass = launchRequestOptions.type === 'pythonExperimental' ? LocalDebugClientV2 : LocalDebugClient; } - const launchScriptProvider = launchRequestOptions.type === 'pythonExperimental' ? new DebuggerV2LauncherScriptProvider() : new DebuggerLauncherScriptProvider(); - return new LocalDebugClient(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); + return new debugClientClass(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); } 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 84903584b616..117266b91e15 100644 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/DebugClients/LocalDebugClient.ts @@ -101,10 +101,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.launcherScriptProvider.getLauncherFilePath(); - const launcherArgs = this.buildLauncherArguments(); - - const args = [ptVSToolsFilePath, processCwd, dbgServer.port.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14'].concat(launcherArgs); + const args = this.buildLaunchArguments(processCwd, dbgServer.port); switch (this.args.console) { case 'externalTerminal': case 'integratedTerminal': { @@ -161,8 +158,13 @@ export class LocalDebugClient extends DebugClient { let x = 0; }); } + private buildLaunchArguments(cwd: string, debugPort: number): string[] { + return [...this.buildDebugArguments(cwd, debugPort), ...this.buildStandardArguments()]; + } + // tslint:disable-next-line:member-ordering - protected buildLauncherArguments(): string[] { + protected buildDebugArguments(cwd: string, debugPort: number): string[] { + const ptVSToolsFilePath = this.launcherScriptProvider.getLauncherFilePath(); const vsDebugOptions: string[] = [DebugOptions.RedirectOutput]; if (Array.isArray(this.args.debugOptions)) { this.args.debugOptions.filter(opt => VALID_DEBUG_OPTIONS.indexOf(opt) >= 0) @@ -173,15 +175,18 @@ export class LocalDebugClient extends DebugClient { if (djangoIndex >= 0) { vsDebugOptions[djangoIndex] = 'DjangoDebugging'; } + return [ptVSToolsFilePath, cwd, debugPort.toString(), '34806ad9-833a-4524-8cd6-18ca4aa74f14', vsDebugOptions.join(',')]; + } + // tslint:disable-next-line:member-ordering + protected buildStandardArguments() { const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; if (typeof this.args.module === 'string' && this.args.module.length > 0) { - return [vsDebugOptions.join(','), '-m', this.args.module].concat(programArgs); + return ['-m', this.args.module, ...programArgs]; } - const args = [vsDebugOptions.join(',')]; if (this.args.program && this.args.program.length > 0) { - args.push(this.args.program); + return [this.args.program, ...programArgs]; } - return args.concat(programArgs); + return programArgs; } private launchExternalTerminal(sudo: boolean, cwd: string, pythonPath: string, args: string[], env: {}) { return new Promise((resolve, reject) => { diff --git a/src/client/debugger/DebugClients/launcherProvider.ts b/src/client/debugger/DebugClients/launcherProvider.ts index 25f722457918..06ea43e0002e 100644 --- a/src/client/debugger/DebugClients/launcherProvider.ts +++ b/src/client/debugger/DebugClients/launcherProvider.ts @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +'use strict'; + +// tslint:disable:max-classes-per-file + import * as path from 'path'; import { IDebugLauncherScriptProvider } from '../types'; @@ -15,9 +19,3 @@ export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvi 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/DebugClients/localDebugClientV2.ts b/src/client/debugger/DebugClients/localDebugClientV2.ts new file mode 100644 index 000000000000..417efba39e26 --- /dev/null +++ b/src/client/debugger/DebugClients/localDebugClientV2.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugSession } from 'vscode-debugadapter'; +import { LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; +import { LocalDebugClient } from './LocalDebugClient'; + +export class LocalDebugClientV2 extends LocalDebugClient { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + super(args, debugSession, canLaunchTerminal, launcherScriptProvider); + } + protected buildDebugArguments(cwd: string, debugPort: number): string[] { + const noDebugArg = this.args.noDebug ? ['--nodebug'] : []; + return ['-m', 'ptvsd', ...noDebugArg, '--host', 'localhost', '--port', debugPort.toString()]; + } + protected buildStandardArguments() { + const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; + if (typeof this.args.module === 'string' && this.args.module.length > 0) { + return ['-m', this.args.module, ...programArgs]; + } + if (this.args.program && this.args.program.length > 0) { + return ['--file', this.args.program, ...programArgs]; + } + return programArgs; + } +} diff --git a/src/client/debugger/DebugClients/nonDebugClientV2.ts b/src/client/debugger/DebugClients/nonDebugClientV2.ts new file mode 100644 index 000000000000..0d47171e810c --- /dev/null +++ b/src/client/debugger/DebugClients/nonDebugClientV2.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import { DebugSession } from 'vscode-debugadapter'; +import { LaunchRequestArguments } from '../Common/Contracts'; +import { IDebugLauncherScriptProvider } from '../types'; +import { DebugType } from './DebugClient'; +import { LocalDebugClientV2 } from './localDebugClientV2'; + +export class NonDebugClientV2 extends LocalDebugClientV2 { + constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: IDebugLauncherScriptProvider) { + super(args, debugSession, canLaunchTerminal, launcherScriptProvider); + } + + public get DebugType(): DebugType { + return DebugType.RunLocal; + } + + public Stop() { + super.Stop(); + if (this.pyProc) { + try { + this.pyProc!.kill(); + // tslint:disable-next-line:no-empty + } catch { } + this.pyProc = undefined; + } + } + protected handleProcessOutput(proc: ChildProcess, _failedToLaunch: (error: Error | string | Buffer) => void) { + // Do nothing + } +} diff --git a/src/client/debugger/mainV2.ts b/src/client/debugger/mainV2.ts index 280e04b58d59..2d8855e7354b 100644 --- a/src/client/debugger/mainV2.ts +++ b/src/client/debugger/mainV2.ts @@ -21,6 +21,7 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import '../../client/common/extensions'; import { noop, sleep } from '../common/core.utils'; import { createDeferred, Deferred, isNotInstalledError } from '../common/helpers'; +import { IFileSystem } from '../common/platform/types'; import { ICurrentProcess } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from './Common/Contracts'; @@ -98,6 +99,10 @@ export class PythonDebugger extends DebugSession { } protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + const fs = this.serviceContainer.get(IFileSystem); + if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !fs.fileExistsSync(args.program)) { + return this.sendErrorResponse(response, { format: `File does not exist. "${args.program}"`, id: 1 }, undefined, undefined, ErrorDestination.User); + } this.launchPTVSD(args) .then(() => this.waitForPTVSDToConnect(args)) .then(() => this.emit('debugger_launched')) diff --git a/src/test/debugger/launcherScriptProvider.test.ts b/src/test/debugger/launcherScriptProvider.test.ts index 147869aa8f20..3a9358a9ff8e 100644 --- a/src/test/debugger/launcherScriptProvider.test.ts +++ b/src/test/debugger/launcherScriptProvider.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import * as fs from 'fs'; import * as path from 'path'; -import { DebuggerLauncherScriptProvider, DebuggerV2LauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/DebugClients/launcherProvider'; +import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from '../../client/debugger/DebugClients/launcherProvider'; suite('Debugger - Launcher Script Provider', () => { test('Ensure stable debugger gets the old launcher from PythonTools directory', () => { @@ -19,10 +19,4 @@ suite('Debugger - Launcher Script Provider', () => { 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/run.test.ts b/src/test/debugger/run.test.ts new file mode 100644 index 000000000000..f1ebdc3ebd18 --- /dev/null +++ b/src/test/debugger/run.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-require-imports no-var-requires + +import { expect } from 'chai'; +import * as path from 'path'; +import { DebugClient } from 'vscode-debugadapter-testsupport'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { PTVSD_PATH } from '../../client/debugger/Common/constants'; +import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/Common/Contracts'; +import { PYTHON_PATH, sleep } from '../common'; +import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { createDebugAdapter } from './utils'; + +const isProcessRunning = require('is-running') as (number) => boolean; + +const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); +const debuggerType = 'pythonExperimental'; +suite('Run without Debugging', () => { + let debugClient: DebugClient; + setup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_nodebug${this.currentTest.title}`); + debugClient = await createDebugAdapter(coverageDirectory); + }); + 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); + }); + function buildLauncArgs(pythonFile: string, stopOnEntry: boolean = false): LaunchRequestArguments { + // tslint:disable-next-line:no-unnecessary-local-variable + const options: LaunchRequestArguments = { + program: path.join(debugFilesPath, pythonFile), + cwd: debugFilesPath, + stopOnEntry, + noDebug: true, + debugOptions: [DebugOptions.RedirectOutput], + pythonPath: PYTHON_PATH, + args: [], + env: { PYTHONPATH: PTVSD_PATH }, + envFile: '', + logToFile: true, + type: debuggerType + }; + + return options; + } + + test('Should run program to the end', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('simplePrint.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.waitForEvent('terminated') + ]); + }); + test('test stderr output for Python', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('stdErrOutput.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stderr', 'error output'), + debugClient.waitForEvent('terminated') + ]); + }); + test('Test stdout output', async () => { + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('stdOutOutput.py', false)), + debugClient.waitForEvent('initialized'), + debugClient.assertOutput('stdout', 'normal output'), + debugClient.waitForEvent('terminated') + ]); + }); + test('Should kill python process when ending debug session', async () => { + const processIdOutput = new Promise(resolve => { + debugClient.on('output', (event: DebugProtocol.OutputEvent) => { + if (event.event === 'output' && event.body.category === 'stdout') { + resolve(parseInt(event.body.output.trim(), 10)); + } + }); + }); + await Promise.all([ + debugClient.configurationSequence(), + debugClient.launch(buildLauncArgs('sampleWithSleep.py', false)), + debugClient.waitForEvent('initialized'), + processIdOutput + ]); + + const processId = await processIdOutput; + expect(processId).to.be.greaterThan(0, 'Invalid process id'); + + await debugClient.stop(); + await sleep(1000); + + // Confirm the process is dead + expect(isProcessRunning(processId)).to.be.equal(false, 'Python program is still alive'); + }); +}); diff --git a/src/test/pythonFiles/debugging/sampleWithSleep.py b/src/test/pythonFiles/debugging/sampleWithSleep.py new file mode 100644 index 000000000000..7a84f4f0da0c --- /dev/null +++ b/src/test/pythonFiles/debugging/sampleWithSleep.py @@ -0,0 +1,8 @@ +import time +import os +print(os.getpid()) +time.sleep(1) +for i in 10000: + time.sleep(0.1) + print(i) +print('end')