diff --git a/news/2 Fixes/737.md b/news/2 Fixes/737.md new file mode 100644 index 000000000000..e72295893f94 --- /dev/null +++ b/news/2 Fixes/737.md @@ -0,0 +1 @@ +Fix debugging of Pyramid applications on Windows. diff --git a/src/client/debugger/configProviders/baseProvider.ts b/src/client/debugger/configProviders/baseProvider.ts index d1395d8dda6b..744dca9c4ea6 100644 --- a/src/client/debugger/configProviders/baseProvider.ts +++ b/src/client/debugger/configProviders/baseProvider.ts @@ -7,10 +7,9 @@ import { injectable, unmanaged } from 'inversify'; import * as path from 'path'; -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, 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 { BaseAttachRequestArguments, BaseLaunchRequestArguments, DebuggerType, DebugOptions } from '../Common/Contracts'; @@ -21,11 +20,11 @@ export type PythonAttachDebugConfiguration @injectable() export abstract class BaseConfigurationProvider implements DebugConfigurationProvider { constructor(@unmanaged() public debugType: DebuggerType, protected serviceContainer: IServiceContainer) { } - public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { + public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { const workspaceFolder = this.getWorkspaceFolder(folder); if (debugConfiguration.request === 'attach') { - this.provideAttachDefaults(workspaceFolder, debugConfiguration as PythonAttachDebugConfiguration); + await this.provideAttachDefaults(workspaceFolder, debugConfiguration as PythonAttachDebugConfiguration); } else { const config = debugConfiguration as PythonLaunchDebugConfiguration; const numberOfSettings = Object.keys(config); @@ -40,7 +39,7 @@ export abstract class BaseConfigurationProvider): void { + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): Promise { if (!Array.isArray(debugConfiguration.debugOptions)) { debugConfiguration.debugOptions = []; } @@ -57,7 +56,7 @@ export abstract class BaseConfigurationProvider): void { + protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonLaunchDebugConfiguration): Promise { this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { debugConfiguration.cwd = workspaceFolder.fsPath; @@ -83,16 +82,6 @@ export abstract class BaseConfigurationProvider= 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): Uri | undefined { if (folder) { diff --git a/src/client/debugger/configProviders/configurationProviderUtils.ts b/src/client/debugger/configProviders/configurationProviderUtils.ts new file mode 100644 index 000000000000..e5ce625517f5 --- /dev/null +++ b/src/client/debugger/configProviders/configurationProviderUtils.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IConfigurationProviderUtils } from './types'; + +const PSERVE_SCRIPT_FILE_NAME = 'pserve.py'; + +@injectable() +export class ConfigurationProviderUtils implements IConfigurationProviderUtils { + private readonly executionFactory: IPythonExecutionFactory; + private readonly fs: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.executionFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.fs = this.serviceContainer.get(IFileSystem); + } + public async getPyramidStartupScriptFilePath(resource?: Uri): Promise { + try { + const executionService = await this.executionFactory.create(resource); + const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true }); + const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME); + return await this.fs.fileExistsAsync(pserveFilePath) ? pserveFilePath : undefined; + } catch (ex) { + const message = 'Unable to locate \'pserve.py\' required for debugging of Pyramid applications.'; + console.error(message, ex); + const app = this.serviceContainer.get(IApplicationShell); + app.showErrorMessage(message); + return; + } + } +} diff --git a/src/client/debugger/configProviders/pythonProvider.ts b/src/client/debugger/configProviders/pythonProvider.ts index 69b2fe743319..2210e08b1a03 100644 --- a/src/client/debugger/configProviders/pythonProvider.ts +++ b/src/client/debugger/configProviders/pythonProvider.ts @@ -7,15 +7,23 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; import { AttachRequestArgumentsV1, DebugOptions, LaunchRequestArgumentsV1 } from '../Common/Contracts'; -import { BaseConfigurationProvider, PythonAttachDebugConfiguration } from './baseProvider'; +import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; +import { IConfigurationProviderUtils } from './types'; @injectable() export class PythonDebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('python', serviceContainer); } - protected provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): void { - super.provideAttachDefaults(workspaceFolder, debugConfiguration); + protected async provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): Promise { + await super.provideLaunchDefaults(workspaceFolder, debugConfiguration); + if (debugConfiguration.debugOptions!.indexOf(DebugOptions.Pyramid) >= 0) { + const utils = this.serviceContainer.get(IConfigurationProviderUtils); + debugConfiguration.program = (await utils.getPyramidStartupScriptFilePath(workspaceFolder))!; + } + } + protected async provideAttachDefaults(workspaceFolder: Uri | undefined, debugConfiguration: PythonAttachDebugConfiguration): Promise { + await super.provideAttachDefaults(workspaceFolder, debugConfiguration); const debugOptions = debugConfiguration.debugOptions!; // Always redirect output. if (debugOptions.indexOf(DebugOptions.RedirectOutput) === -1) { diff --git a/src/client/debugger/configProviders/pythonV2Provider.ts b/src/client/debugger/configProviders/pythonV2Provider.ts index e8953534c1b0..4b60be89a688 100644 --- a/src/client/debugger/configProviders/pythonV2Provider.ts +++ b/src/client/debugger/configProviders/pythonV2Provider.ts @@ -9,14 +9,15 @@ import { IPlatformService } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../Common/Contracts'; import { BaseConfigurationProvider, PythonAttachDebugConfiguration, PythonLaunchDebugConfiguration } from './baseProvider'; +import { IConfigurationProviderUtils } from './types'; @injectable() export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvider { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super('pythonExperimental', serviceContainer); } - protected provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): void { - super.provideLaunchDefaults(workspaceFolder, debugConfiguration); + protected async provideLaunchDefaults(workspaceFolder: Uri, debugConfiguration: PythonLaunchDebugConfiguration): Promise { + await super.provideLaunchDefaults(workspaceFolder, debugConfiguration); const debugOptions = debugConfiguration.debugOptions!; if (debugConfiguration.debugStdLib) { this.debugOption(debugOptions, DebugOptions.DebugStdLib); @@ -41,9 +42,13 @@ export class PythonV2DebugConfigurationProvider extends BaseConfigurationProvide && debugConfiguration.jinja !== false) { this.debugOption(debugOptions, DebugOptions.Jinja); } + if (debugConfiguration.pyramid) { + const utils = this.serviceContainer.get(IConfigurationProviderUtils); + debugConfiguration.program = (await utils.getPyramidStartupScriptFilePath(workspaceFolder))!; + } } - protected provideAttachDefaults(workspaceFolder: Uri, debugConfiguration: PythonAttachDebugConfiguration): void { - super.provideAttachDefaults(workspaceFolder, debugConfiguration); + protected async provideAttachDefaults(workspaceFolder: Uri, debugConfiguration: PythonAttachDebugConfiguration): Promise { + await super.provideAttachDefaults(workspaceFolder, debugConfiguration); const debugOptions = debugConfiguration.debugOptions!; if (debugConfiguration.debugStdLib) { this.debugOption(debugOptions, DebugOptions.DebugStdLib); diff --git a/src/client/debugger/configProviders/serviceRegistry.ts b/src/client/debugger/configProviders/serviceRegistry.ts index d664bfac771c..3e6dbded95c4 100644 --- a/src/client/debugger/configProviders/serviceRegistry.ts +++ b/src/client/debugger/configProviders/serviceRegistry.ts @@ -8,8 +8,11 @@ import { DebugConfigurationProvider } from 'vscode'; import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '..'; import { IServiceManager } from '../../ioc/types'; import { IDebugConfigurationProvider } from '../types'; +import { ConfigurationProviderUtils } from './configurationProviderUtils'; +import { IConfigurationProviderUtils } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugConfigurationProvider, PythonDebugConfigurationProvider); serviceManager.addSingleton(IDebugConfigurationProvider, PythonV2DebugConfigurationProvider); + serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); } diff --git a/src/client/debugger/configProviders/types.ts b/src/client/debugger/configProviders/types.ts new file mode 100644 index 000000000000..8ece886ab352 --- /dev/null +++ b/src/client/debugger/configProviders/types.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri } from 'vscode'; + +export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); + +export interface IConfigurationProviderUtils { + getPyramidStartupScriptFilePath(resource?: Uri): Promise; +} diff --git a/src/test/debugger/configProvider/provider.test.ts b/src/test/debugger/configProvider/provider.test.ts index c8670b6f0e68..0ea18a47ac56 100644 --- a/src/test/debugger/configProvider/provider.test.ts +++ b/src/test/debugger/configProvider/provider.test.ts @@ -9,12 +9,15 @@ import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { PythonLanguage } from '../../../client/common/constants'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; import { PythonDebugConfigurationProvider, PythonV2DebugConfigurationProvider } from '../../../client/debugger'; import { DebugOptions } from '../../../client/debugger/Common/Contracts'; +import { ConfigurationProviderUtils } from '../../../client/debugger/configProviders/configurationProviderUtils'; +import { IConfigurationProviderUtils } from '../../../client/debugger/configProviders/types'; import { IServiceContainer } from '../../../client/ioc/types'; [ @@ -26,6 +29,8 @@ import { IServiceContainer } from '../../../client/ioc/types'; let debugProvider: DebugConfigurationProvider; let platformService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let pythonExecutionService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); debugProvider = new provider.class(serviceContainer.object); @@ -39,9 +44,20 @@ import { IServiceContainer } from '../../../client/ioc/types'; const confgService = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + + pythonExecutionService = TypeMoq.Mock.ofType(); + pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); + const factory = TypeMoq.Mock.ofType(); + factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); + + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))).returns(() => factory.object); 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); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationProviderUtils))).returns(() => new ConfigurationProviderUtils(serviceContainer.object)); + const settings = TypeMoq.Mock.ofType(); settings.setup(s => s.pythonPath).returns(() => pythonPath); confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); @@ -295,26 +311,39 @@ 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) { + async function testPyramidConfiguration(isWindows: boolean, isLinux: boolean, isMac: boolean, addPyramidDebugOption: boolean = true, pyramidExists = 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 pyramidFilePath = path.join(path.dirname(pythonPath), 'lib', 'site_packages', 'pyramid', '__init__.py'); + const pserveFilePath = path.join(path.dirname(pyramidFilePath), 'scripts', 'pserve.py'); + const args = ['-c', 'import pyramid;print(pyramid.__file__)']; const workspaceFolder = createMoqWorkspaceFolder(workspacePath); const pythonFile = 'xyz.py'; + setupIoc(pythonPath, isWindows, isMac, isLinux); setupActiveEditor(pythonFile, PythonLanguage.language); - const options = addPyramidDebugOption ? { debugOptions: [DebugOptions.Pyramid] } : {}; - fileSystem.setup(fs => fs.fileExistsSync(TypeMoq.It.isValue(pythonPath))).returns(() => pythonPathExists); + const execOutput = pyramidExists ? Promise.resolve({ stdout: pyramidFilePath }) : Promise.reject(new Error('No Module')); + pythonExecutionService.setup(e => e.exec(TypeMoq.It.isValue(args), TypeMoq.It.isAny())) + .returns(() => execOutput) + .verifiable(TypeMoq.Times.exactly(addPyramidDebugOption ? 1 : 0)); + fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pserveFilePath))) + .returns(() => Promise.resolve(pyramidExists)) + .verifiable(TypeMoq.Times.exactly(pyramidExists && addPyramidDebugOption ? 1 : 0)); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); + const options = addPyramidDebugOption ? { debugOptions: [DebugOptions.Pyramid], pyramid: true } : {}; const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, options as any as DebugConfiguration); if (shouldWork) { - expect(debugConfig).to.have.property('program', pservePath); + expect(debugConfig).to.have.property('program', pserveFilePath); } else { - expect(debugConfig!.program).to.be.not.equal(pservePath); + expect(debugConfig!.program).to.be.not.equal(pserveFilePath); } - } + pythonExecutionService.verifyAll(); + fileSystem.verifyAll(); + appShell.verifyAll(); + } test('Program is set for Pyramid (windows)', async () => { await testPyramidConfiguration(true, false, false); }); @@ -333,14 +362,14 @@ import { IServiceContainer } from '../../../client/ioc/types'; 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('Message is displayed when pyramid script does not exist (windows)', async () => { + await testPyramidConfiguration(true, false, false, true, false, false); }); - 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('Message is displayed when pyramid script does not exist (Linux)', async () => { + await testPyramidConfiguration(false, true, false, true, false, false); }); - 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); + test('Message is displayed when pyramid script does not exist (Mac)', async () => { + await testPyramidConfiguration(false, false, true, true, false, false); }); test('Auto detect flask debugging', async () => { if (provider.debugType === 'python') {