diff --git a/news/1 Enhancements/1030.md b/news/1 Enhancements/1030.md new file mode 100644 index 000000000000..5822cd9b67ee --- /dev/null +++ b/news/1 Enhancements/1030.md @@ -0,0 +1 @@ +Add a Pyramid debug configuration for the experimental debugger. diff --git a/package.json b/package.json index 4c63c670f85f..b02bdb43601e 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "activationEvents": [ "onLanguage:python", "onDebugResolve:python", + "onDebugResolve:pythonExperimental", "onCommand:python.execInTerminal", "onCommand:python.sortImports", "onCommand:python.runtests", @@ -831,6 +832,21 @@ "bikes.json" ] } + }, + { + "label": "Python Experimental: Pyramid", + "description": "%python.snippet.launch.pyramid.description%", + "body": { + "name": "Pyramid", + "type": "pythonExperimental", + "request": "launch", + "args": [ + "^\"\\${workspaceFolder}/development.ini\"" + ], + "debugOptions": [ + "Pyramid" + ] + } } ], "configurationAttributes": { @@ -884,7 +900,8 @@ "items": { "type": "string", "enum": [ - "Sudo" + "Sudo", + "Pyramid" ] }, "default": [] diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 89ec9aac3e9e..463b1089b6fe 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -29,6 +29,9 @@ export class FileSystem implements IFileSystem { public fileExistsAsync(filePath: string): Promise { return this.objectExistsAsync(filePath, (stats) => stats.isFile()); } + public fileExistsSync(filePath: string): boolean { + return fs.existsSync(filePath); + } /** * Reads the contents of the file using utf8 and returns the string contents. * @param {string} filePath @@ -79,9 +82,9 @@ export class FileSystem implements IFileSystem { } public appendFileSync(filename: string, data: {}, encoding: string): void; - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: 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: {}, 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 d87722efdd38..6c40a7a6a068 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -33,14 +33,15 @@ export interface IFileSystem { directorySeparatorChar: string; objectExistsAsync(path: string, statCheck: (s: fs.Stats) => boolean): Promise; fileExistsAsync(path: string): Promise; + fileExistsSync(path: string): boolean; directoryExistsAsync(path: string): Promise; createDirectoryAsync(path: string): Promise; 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; + 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; + appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; getRealPathAsync(path: string): Promise; } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 687f9898c6b4..4083b0965fd9 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -223,5 +223,5 @@ export interface IConfigurationService { export const ISocketServer = Symbol('ISocketServer'); export interface ISocketServer extends Disposable { readonly client: Promise; - Start(options?: { port?: number, host?: string }): 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 bd5c697fede4..e8cce80fe552 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -1,4 +1,4 @@ -// tslint:disable:interface-name member-access no-single-line-block-comment no-any no-stateless-class member-ordering prefer-method-signature +// tslint:disable:interface-name member-access no-single-line-block-comment no-any no-stateless-class member-ordering prefer-method-signature no-unnecessary-class 'use strict'; import { ChildProcess } from 'child_process'; @@ -8,7 +8,7 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { DebuggerPerformanceTelemetry, DebuggerTelemetry } from '../../telemetry/types'; export class TelemetryEvent extends OutputEvent { - body: { + body!: { /** The category of output (such as: 'console', 'stdout', 'stderr', 'telemetry'). If not specified, 'console' is assumed. */ category: string; /** The output to report. */ diff --git a/src/client/debugger/Common/telemetry.ts b/src/client/debugger/Common/telemetry.ts index cecde60b2658..2e454c350379 100644 --- a/src/client/debugger/Common/telemetry.ts +++ b/src/client/debugger/Common/telemetry.ts @@ -10,7 +10,7 @@ import { DebuggerPerformanceTelemetry } from '../../telemetry/types'; import { TelemetryEvent } from './Contracts'; type DebugAction = 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; -type DebugPerformanceInformation = { action: DebugAction, timer: StopWatch }; +type DebugPerformanceInformation = { action: DebugAction; timer: StopWatch }; const executionStack: DebugPerformanceInformation[] = []; diff --git a/src/client/debugger/DebugClients/LocalDebugClient.ts b/src/client/debugger/DebugClients/LocalDebugClient.ts index 10b8b2de24c9..d376f18a34aa 100644 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ b/src/client/debugger/DebugClients/LocalDebugClient.ts @@ -1,5 +1,4 @@ -import * as child_process from 'child_process'; -import { ChildProcess } from 'child_process'; +import { ChildProcess, spawn } from 'child_process'; import * as path from 'path'; import { DebugSession, OutputEvent } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -31,8 +30,8 @@ enum DebugServerStatus { } export class LocalDebugClient extends DebugClient { - protected pyProc: child_process.ChildProcess | undefined; - protected pythonProcess: IPythonProcess; + protected pyProc: ChildProcess | undefined; + protected pythonProcess!: IPythonProcess; protected debugServer: BaseDebugServer | undefined; private get debugServerStatus(): DebugServerStatus { if (this.debugServer && this.debugServer!.IsRunning) { @@ -114,7 +113,7 @@ export class LocalDebugClient extends DebugClient { break; } default: { - this.pyProc = child_process.spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); + this.pyProc = spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); this.handleProcessOutput(this.pyProc!, reject); // Here we wait for the application to connect to the socket server. @@ -174,7 +173,11 @@ export class LocalDebugClient extends DebugClient { if (typeof this.args.module === 'string' && this.args.module.length > 0) { return [vsDebugOptions.join(','), '-m', this.args.module].concat(programArgs); } - return [vsDebugOptions.join(','), this.args.program].concat(programArgs); + const args = [vsDebugOptions.join(',')]; + if (this.args.program && this.args.program.length > 0) { + args.push(this.args.program); + } + return args.concat(programArgs); } private launchExternalTerminal(sudo: boolean, cwd: string, pythonPath: string, args: string[], env: {}) { return new Promise((resolve, reject) => { diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index 4b5bfe3e4a3a..04a0348948e4 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -1,4 +1,4 @@ -// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self +// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self no-duplicate-imports "use strict"; // This line should always be right on top. @@ -9,8 +9,7 @@ if ((Reflect as any).metadata === undefined) { } import * as fs from "fs"; import * as path from "path"; -import { Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable, LoggingDebugSession, logger, BreakpointEvent, Breakpoint } from "vscode-debugadapter"; -import { ThreadEvent } from "vscode-debugadapter"; +import { Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable, LoggingDebugSession, logger, BreakpointEvent, Breakpoint, ThreadEvent } from "vscode-debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import { DEBUGGER } from '../../client/telemetry/constants'; import { DebuggerTelemetry } from '../../client/telemetry/types'; @@ -23,7 +22,6 @@ import { DebugClient } from "./DebugClients/DebugClient"; import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients/DebugFactory"; 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"; @@ -41,13 +39,13 @@ export class PythonDebugger extends LoggingDebugSession { private registeredBreakpoints: Map; private registeredBreakpointsByFileName: Map; private debuggerLoaded: Promise; - private debuggerLoadedPromiseResolve: () => void; + private debuggerLoadedPromiseResolve!: () => void; private debugClient?: DebugClient<{}>; - private configurationDone: Promise; + private configurationDone!: Promise; private configurationDonePromiseResolve?: () => void; private lastException?: IPythonException; - private _supportsRunInTerminalRequest: boolean; - private terminateEventSent: boolean; + private _supportsRunInTerminalRequest: boolean = false; + private terminateEventSent: boolean = false; public constructor(debuggerLinesStartAt1: boolean, isServer: boolean) { super(path.join(__dirname, '..', '..', '..', 'debug.log'), debuggerLinesStartAt1, isServer === true); this._variableHandles = new Handles(); @@ -92,7 +90,7 @@ export class PythonDebugger extends LoggingDebugSession { } private pythonProcess?: PythonProcess; - private debugServer: BaseDebugServer; + private debugServer!: BaseDebugServer; private startDebugServer(): Promise { let programDirectory = ''; @@ -208,8 +206,8 @@ export class PythonDebugger extends LoggingDebugSession { this.sendEvent(new OutputEvent(output, outputChannel)); } private entryResponse?: DebugProtocol.LaunchResponse; - private launchArgs: LaunchRequestArguments; - private attachArgs: AttachRequestArguments; + private launchArgs!: LaunchRequestArguments; + private attachArgs!: AttachRequestArguments; private canStartDebugger(): Promise { return Promise.resolve(true); } @@ -230,15 +228,6 @@ export class PythonDebugger extends LoggingDebugSession { } catch (ex) { } - if (Array.isArray(args.debugOptions) && args.debugOptions.indexOf("Pyramid") >= 0) { - const pserve = IS_WINDOWS ? "pserve.exe" : "pserve"; - if (fs.existsSync(args.pythonPath)) { - args.program = path.join(path.dirname(args.pythonPath), pserve); - } - else { - args.program = pserve; - } - } // Confirm the file exists if (typeof args.module !== 'string' || args.module.length === 0) { if (!fs.existsSync(args.program)) { @@ -279,6 +268,7 @@ export class PythonDebugger extends LoggingDebugSession { }); this.entryResponse = response; + // tslint:disable-next-line:no-this-assignment const that = this; this.startDebugServer().then(dbgServer => { @@ -302,6 +292,7 @@ export class PythonDebugger extends LoggingDebugSession { this.attachArgs = args; this.debugClient = CreateAttachDebugClient(args, this); this.entryResponse = response; + // tslint:disable-next-line:no-this-assignment const that = this; this.canStartDebugger().then(() => { @@ -375,7 +366,7 @@ export class PythonDebugger extends LoggingDebugSession { } // VSC needs `id` to uniquely identify each breakpoint (part of the protocol spec). - const breakpoints: { verified: boolean, line: number, id: number }[] = []; + const breakpoints: { verified: boolean; line: number; id: number }[] = []; const linesToAdd = args.breakpoints!.map(b => b.line); const registeredBks = this.registeredBreakpointsByFileName.get(args.source.path!)!; const linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index 2459fe8f4f1f..fef1a3c377da 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../common/application/types'; import { PythonLanguage } from '../../common/constants'; +import { IFileSystem, IPlatformService } from '../../common/platform/types'; import { IConfigurationService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { DebuggerType, LaunchRequestArguments } from '../Common/Contracts'; @@ -15,7 +16,7 @@ import { DebuggerType, LaunchRequestArguments } from '../Common/Contracts'; // tslint:disable:no-invalid-template-strings export type PythonDebugConfiguration = DebugConfiguration & LaunchRequestArguments; -export type PTVSDDebugConfiguration = PythonDebugConfiguration & { redirectOutput: boolean, fixFilePathCase: boolean }; +export type PTVSDDebugConfiguration = PythonDebugConfiguration & { redirectOutput: boolean; fixFilePathCase: boolean }; @injectable() export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { @@ -64,6 +65,16 @@ export abstract class BaseConfigurationProvider implements DebugConfigurationPro if (debugConfiguration.debugOptions.indexOf('RedirectOutput') === -1) { debugConfiguration.debugOptions.push('RedirectOutput'); } + if (debugConfiguration.debugOptions.indexOf('Pyramid') >= 0) { + const platformService = this.serviceContainer.get(IPlatformService); + const fs = this.serviceContainer.get(IFileSystem); + const pserve = platformService.isWindows ? 'pserve.exe' : 'pserve'; + if (fs.fileExistsSync(debugConfiguration.pythonPath)) { + debugConfiguration.program = path.join(path.dirname(debugConfiguration.pythonPath), pserve); + } else { + debugConfiguration.program = pserve; + } + } } private getWorkspaceFolder(folder: WorkspaceFolder | undefined, config: PythonDebugConfiguration): Uri | undefined { if (folder) { diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts index fa9331ce60fa..0e77a631f2c4 100644 --- a/src/test/common/platform/filesystem.test.ts +++ b/src/test/common/platform/filesystem.test.ts @@ -67,6 +67,9 @@ suite('FileSystem', () => { test('Case sensitivity is not ignored when comparing file names on linux', async () => { caseSensitivityFileCheck(false, false, true); }); + test('Check existence of files synchronously', async () => { + expect(fileSystem.fileExistsSync(__filename)).to.be.equal(true, 'file not found'); + }); test('Test appending to file', async () => { const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; diff --git a/src/test/debugger/configProvider/provider.test.ts b/src/test/debugger/configProvider/provider.test.ts index e244e9a28c8c..050631d18d01 100644 --- a/src/test/debugger/configProvider/provider.test.ts +++ b/src/test/debugger/configProvider/provider.test.ts @@ -3,7 +3,7 @@ 'use strict'; -// tslint:disable:max-func-body-length no-invalid-template-strings no-any +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion import { expect } from 'chai'; import * as path from 'path'; @@ -11,7 +11,7 @@ import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { PythonLanguage } from '../../../client/common/constants'; -import { IPlatformService } from '../../../client/common/platform/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -24,6 +24,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; let serviceContainer: TypeMoq.IMock; let debugProvider: DebugConfigurationProvider; let platformService: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); debugProvider = new provider.class(serviceContainer.object); @@ -36,8 +37,10 @@ import { IServiceContainer } from '../../../client/ioc/types'; function setupIoc(pythonPath: string, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { const confgService = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => confgService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); const settings = TypeMoq.Mock.ofType(); settings.setup(s => s.pythonPath).returns(() => pythonPath); confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -305,5 +308,52 @@ import { IServiceContainer } from '../../../client/ioc/types'; } await testFixFilePathCase(false, true, false); }); + async function testPyramidConfiguration(isWindows: boolean, isLinux: boolean, isMac: boolean, addPyramidDebugOption: boolean = true, pythonPathExists = true, shouldWork = true) { + const workspacePath = path.join('usr', 'development', 'wksp1'); + const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); + const pserveExecutableName = isWindows ? 'pserve.exe' : 'pserve'; + const pservePath = pythonPathExists ? path.join(path.dirname(pythonPath), pserveExecutableName) : pserveExecutableName; + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, isWindows, isMac, isLinux); + setupActiveEditor(pythonFile, PythonLanguage.language); + + const options = addPyramidDebugOption ? { debugOptions: ['Pyramid'] } : {}; + fileSystem.setup(fs => fs.fileExistsSync(TypeMoq.It.isValue(pythonPath))).returns(() => pythonPathExists); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, options as any as DebugConfiguration); + if (shouldWork) { + expect(debugConfig).to.have.property('program', pservePath); + } else { + expect(debugConfig!.program).to.be.not.equal(pservePath); + } + } + test('Program is set for Pyramid (windows)', async () => { + await testPyramidConfiguration(true, false, false); + }); + test('Program is set for Pyramid (Linux)', async () => { + await testPyramidConfiguration(false, true, false); + }); + test('Program is set for Pyramid (Mac)', async () => { + await testPyramidConfiguration(false, false, true); + }); + test('Program is not set for Pyramid when DebugOption is not set (windows)', async () => { + await testPyramidConfiguration(true, false, false, false, false, false); + }); + test('Program is not set for Pyramid when DebugOption is not set (Linux)', async () => { + await testPyramidConfiguration(false, true, false, false, false, false); + }); + test('Program is not set for Pyramid when DebugOption is not set (Mac)', async () => { + await testPyramidConfiguration(false, false, true, false, false, false); + }); + test('Program is set to executable name for Pyramid when python exec does not exist (windows)', async () => { + await testPyramidConfiguration(true, false, false, true, false, true); + }); + test('Program is set to executable name for Pyramid when python exec does not exist (Linux)', async () => { + await testPyramidConfiguration(false, true, false, true, false, true); + }); + test('Program is set to executable name for Pyramid when python exec does not exist (Mac)', async () => { + await testPyramidConfiguration(false, false, true, true, false, true); + }); }); });