diff --git a/news/3 Code Health/10454.md b/news/3 Code Health/10454.md new file mode 100644 index 000000000000..698c4bbbea30 --- /dev/null +++ b/news/3 Code Health/10454.md @@ -0,0 +1 @@ +Refactor the extension activation code to split on phases. diff --git a/src/client/api.ts b/src/client/api.ts index d84858f8acf0..85b8b6b162b0 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -3,11 +3,13 @@ 'use strict'; +import { isTestExecution } from './common/constants'; import { DebugAdapterNewPtvsd } from './common/experimentGroups'; import { traceError } from './common/logger'; import { IExperimentsManager } from './common/types'; import { RemoteDebuggerExternalLauncherScriptProvider } from './debugger/debugAdapter/DebugClients/launcherProvider'; import { IDebugAdapterDescriptorFactory } from './debugger/extension/types'; +import { IServiceContainer, IServiceManager } from './ioc/types'; /* * Do not introduce any breaking changes to this API. @@ -38,10 +40,13 @@ export interface IExtensionApi { export function buildApi( // tslint:disable-next-line:no-any ready: Promise, - experimentsManager: IExperimentsManager, - debugFactory: IDebugAdapterDescriptorFactory + serviceManager: IServiceManager, + serviceContainer: IServiceContainer ) { - return { + const experimentsManager = serviceContainer.get(IExperimentsManager); + const debugFactory = serviceContainer.get(IDebugAdapterDescriptorFactory); + + const api = { // 'ready' will propagate the exception, but we must log it here first. ready: ready.catch(ex => { traceError('Failure during activation.', ex); @@ -69,4 +74,13 @@ export function buildApi( } } }; + + // In test environment return the DI Container. + if (isTestExecution()) { + // tslint:disable:no-any + (api as any).serviceContainer = serviceContainer; + (api as any).serviceManager = serviceManager; + // tslint:enable:no-any + } + return api; } diff --git a/src/client/extension.ts b/src/client/extension.ts index b13956d714c2..adebd5e9ec1d 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,4 +1,5 @@ 'use strict'; + // tslint:disable:no-var-requires no-require-imports // This line should always be right on top. @@ -13,210 +14,60 @@ initialize(require('vscode')); // Initialize the logger first. require('./common/logger'); +//=============================================== +// We start tracking the extension's startup time at this point. The +// locations at which we record various Intervals are marked below in +// the same way as this. + const durations: Record = {}; import { StopWatch } from './common/utils/stopWatch'; // Do not move this line of code (used to measure extension load times). const stopWatch = new StopWatch(); -import { Container } from 'inversify'; -import { - CodeActionKind, - debug, - DebugConfigurationProvider, - Disposable, - ExtensionContext, - languages, - Memento, - OutputChannel, - ProgressLocation, - ProgressOptions, - window -} from 'vscode'; -import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; -import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types'; +//=============================================== +// loading starts here + +import { ProgressLocation, ProgressOptions, window } from 'vscode'; + import { buildApi, IExtensionApi } from './api'; -import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; -import { IApplicationDiagnostics } from './application/types'; -import { DebugService } from './common/application/debugService'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, isTestExecution, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; -import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; +import { IApplicationShell } from './common/application/types'; import { traceError } from './common/logger'; -import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; -import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; -import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; -import { ITerminalHelper } from './common/terminal/types'; -import { - GLOBAL_MEMENTO, - IAsyncDisposableRegistry, - IConfigurationService, - IDisposableRegistry, - IExperimentsManager, - IExtensionContext, - IFeatureDeprecationManager, - IMemento, - IOutputChannel, - Resource, - WORKSPACE_MEMENTO -} from './common/types'; +import { IAsyncDisposableRegistry, IExtensionContext } from './common/types'; import { createDeferred } from './common/utils/async'; -import { Common, OutputChannelNames } from './common/utils/localize'; -import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; -import { JUPYTER_OUTPUT_CHANNEL } from './datascience/constants'; -import { registerTypes as dataScienceRegisterTypes } from './datascience/serviceRegistry'; -import { IDataScience } from './datascience/types'; -import { DebuggerTypeName } from './debugger/constants'; -import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; -import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; -import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; -import { - IDebugAdapterDescriptorFactory, - IDebugConfigurationService, - IDebuggerBanner -} from './debugger/extension/types'; -import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; -import { - AutoSelectionRule, - IInterpreterAutoSelectionRule, - IInterpreterAutoSelectionService -} from './interpreter/autoSelection/types'; -import { IInterpreterSelector } from './interpreter/configuration/types'; -import { - ICondaService, - IInterpreterLocatorProgressHandler, - IInterpreterLocatorProgressService, - IInterpreterService, - PythonInterpreter -} from './interpreter/contracts'; -import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; -import { ServiceContainer } from './ioc/container'; -import { ServiceManager } from './ioc/serviceManager'; -import { IServiceContainer, IServiceManager } from './ioc/types'; -import { getLanguageConfiguration } from './language/languageConfiguration'; -import { LinterCommands } from './linters/linterCommands'; -import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { PythonCodeActionProvider } from './providers/codeActionProvider/pythonCodeActionProvider'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { ReplProvider } from './providers/replProvider'; -import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; -import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; -import { TerminalProvider } from './providers/terminalProvider'; -import { ISortImportsEditingProvider } from './providers/types'; -import { sendTelemetryEvent } from './telemetry'; -import { EventName } from './telemetry/constants'; -import { EditorLoadTelemetry } from './telemetry/types'; -import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; -import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; -import { TEST_OUTPUT_CHANNEL } from './testing/common/constants'; -import { ITestContextService } from './testing/common/types'; -import { ITestCodeNavigatorCommandHandler, ITestExplorerCommandHandler } from './testing/navigation/types'; -import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { Common } from './common/utils/localize'; +import { activateComponents } from './extensionActivation'; +import { initializeComponents, initializeGlobals } from './extensionInit'; +import { IServiceContainer } from './ioc/types'; +import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; durations.codeLoadingTime = stopWatch.elapsedTime; -const activationDeferred = createDeferred(); -let activatedServiceContainer: ServiceContainer | undefined; - -export async function activate(context: ExtensionContext): Promise { - try { - return await activateUnsafe(context); - } catch (ex) { - handleError(ex); - throw ex; // re-raise - } -} -// tslint:disable-next-line:max-func-body-length -async function activateUnsafe(context: ExtensionContext): Promise { - displayProgress(activationDeferred.promise); - durations.startActivateTime = stopWatch.elapsedTime; - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - const serviceContainer = new ServiceContainer(cont); - activatedServiceContainer = serviceContainer; - registerServices(context, serviceManager, serviceContainer); - await initializeServices(context, serviceManager, serviceContainer); - - const manager = serviceContainer.get(IExtensionActivationManager); - context.subscriptions.push(manager); - const activationPromise = manager.activate(); - - serviceManager.get(ITerminalAutoActivation).register(); - const configuration = serviceManager.get(IConfigurationService); - const pythonSettings = configuration.getSettings(); - - const standardOutputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); - - const sortImports = serviceContainer.get(ISortImportsEditingProvider); - sortImports.registerCommands(); - - serviceManager.get(ICodeExecutionManager).registerCommands(); - - if (!isTestExecution()) { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Move this down to right before durations.endActivateTime is set. - sendStartupTelemetry( - Promise.all([activationDeferred.promise, activationPromise]), - serviceContainer - ).ignoreErrors(); - } +//=============================================== +// loading ends here - const workspaceService = serviceContainer.get(IWorkspaceService); - const interpreterManager = serviceContainer.get(IInterpreterService); - interpreterManager - .refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) - .catch(ex => traceError('Python Extension: interpreterManager.refresh', ex)); +// These persist between activations: +let activatedServiceContainer: IServiceContainer | undefined; - // Activate data science features - const dataScience = serviceManager.get(IDataScience); - dataScience.activate().ignoreErrors(); - - context.subscriptions.push(new LinterCommands(serviceManager)); - - languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); - - if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'internalConsole') { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } - - const deprecationMgr = serviceContainer.get(IFeatureDeprecationManager); - deprecationMgr.initialize(); - context.subscriptions.push(deprecationMgr); - - context.subscriptions.push(new ReplProvider(serviceContainer)); - - const terminalProvider = new TerminalProvider(serviceContainer); - terminalProvider.initialize(window.activeTerminal).ignoreErrors(); - context.subscriptions.push(terminalProvider); - - context.subscriptions.push( - languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { - providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] - }) - ); - - serviceContainer.getAll(IDebugConfigurationService).forEach(debugConfigProvider => { - context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - }); - - serviceContainer.get(IDebuggerBanner).initialize(); - durations.endActivateTime = stopWatch.elapsedTime; - activationDeferred.resolve(); +///////////////////////////// +// public functions - const api = buildApi( - Promise.all([activationDeferred.promise, activationPromise]), - serviceContainer.get(IExperimentsManager), - serviceContainer.get(IDebugAdapterDescriptorFactory) - ); - // In test environment return the DI Container. - if (isTestExecution()) { - // tslint:disable:no-any - (api as any).serviceContainer = serviceContainer; - (api as any).serviceManager = serviceManager; - // tslint:enable:no-any +export async function activate(context: IExtensionContext): Promise { + let api: IExtensionApi; + let ready: Promise; + let serviceContainer: IServiceContainer; + try { + [api, ready, serviceContainer] = await activateUnsafe(context, stopWatch, durations); + } catch (ex) { + // We want to completely handle the error + // before notifying VS Code. + await handleError(ex, durations); + throw ex; // re-raise } + // Send the "success" telemetry only if activation did not fail. + // Otherwise Telemetry is send via the error handler. + sendStartupTelemetry(ready, durations, stopWatch, serviceContainer) + // Run in the background. + .ignoreErrors(); return api; } @@ -232,186 +83,52 @@ export function deactivate(): Thenable { return Promise.resolve(); } -// tslint:disable-next-line:no-any -function displayProgress(promise: Promise) { - const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; - window.withProgress(progressOptions, () => promise); -} - -function registerServices( - context: ExtensionContext, - serviceManager: ServiceManager, - serviceContainer: ServiceContainer -) { - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IServiceManager, serviceManager); - serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); - serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); - serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance(IExtensionContext, context); - - const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python()); - const unitTestOutChannel = window.createOutputChannel(OutputChannelNames.pythonTest()); - const jupyterOutputChannel = window.createOutputChannel(OutputChannelNames.jupyter()); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(IOutputChannel, jupyterOutputChannel, JUPYTER_OUTPUT_CHANNEL); - - commonRegisterTypes(serviceManager); - processRegisterTypes(serviceManager); - variableRegisterTypes(serviceManager); - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); - platformRegisterTypes(serviceManager); - installerRegisterTypes(serviceManager); - commonRegisterTerminalTypes(serviceManager); - dataScienceRegisterTypes(serviceManager); - debugConfigurationRegisterTypes(serviceManager); - - const configuration = serviceManager.get(IConfigurationService); - const languageServerType = configuration.getSettings().languageServer; - - appRegisterTypes(serviceManager, languageServerType); - providersRegisterTypes(serviceManager); - activationRegisterTypes(serviceManager, languageServerType); -} - -async function initializeServices( - context: ExtensionContext, - serviceManager: ServiceManager, - serviceContainer: ServiceContainer -) { - const abExperiments = serviceContainer.get(IExperimentsManager); - await abExperiments.activate(); - const selector = serviceContainer.get(IInterpreterSelector); - selector.initialize(); - context.subscriptions.push(selector); +///////////////////////////// +// activation helpers - const interpreterManager = serviceContainer.get(IInterpreterService); - interpreterManager.initialize(); +// tslint:disable-next-line:max-func-body-length +async function activateUnsafe( + context: IExtensionContext, + startupStopWatch: StopWatch, + startupDurations: Record +): Promise<[IExtensionApi, Promise, IServiceContainer]> { + const activationDeferred = createDeferred(); + displayProgress(activationDeferred.promise); + startupDurations.startActivateTime = startupStopWatch.elapsedTime; - const handlers = serviceManager.getAll(IDebugSessionEventHandlers); - const disposables = serviceManager.get(IDisposableRegistry); - const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); - dispatcher.registerEventHandlers(); + //=============================================== + // activation starts here - const cmdManager = serviceContainer.get(ICommandManager); - const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + const [serviceManager, serviceContainer] = initializeGlobals(context); + activatedServiceContainer = serviceContainer; + initializeComponents(context, serviceManager, serviceContainer); + const activationPromise = activateComponents(context, serviceManager, serviceContainer); - // Display progress of interpreter refreshes only after extension has activated. - serviceContainer.get(IInterpreterLocatorProgressHandler).register(); - serviceContainer.get(IInterpreterLocatorProgressService).register(); - serviceContainer.get(IApplicationDiagnostics).register(); - serviceContainer.get(ITestCodeNavigatorCommandHandler).register(); - serviceContainer.get(ITestExplorerCommandHandler).register(); - serviceContainer.get(ILanguageServerExtension).register(); - serviceContainer.get(ITestContextService).register(); -} + //=============================================== + // activation ends here -// tslint:disable-next-line:no-any -async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { - try { - await activatedPromise; - durations.totalActivateTime = stopWatch.elapsedTime; - const props = await getActivationTelemetryProps(serviceContainer); - sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); - } catch (ex) { - traceError('sendStartupTelemetry() failed.', ex); - } -} -function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceContainer: IServiceContainer): boolean { - const service = serviceContainer.get(IInterpreterAutoSelectionService); - const globalInterpreter = service.getAutoSelectedInterpreter(undefined); - if (!globalInterpreter) { - return false; - } - return currentPythonPath === globalInterpreter.path; -} -function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get(IWorkspaceService); - const settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; - return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || - (settings.workspaceValue && settings.workspaceValue !== 'python') || - (settings.globalValue && settings.globalValue !== 'python') - ? true - : false; -} + startupDurations.endActivateTime = startupStopWatch.elapsedTime; + activationDeferred.resolve(); -function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceInterpreterSelector = serviceContainer.get( - IInterpreterAutoSelectionRule, - AutoSelectionRule.workspaceVirtualEnvs - ); - const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); - return interpreter ? interpreter.path : undefined; + const api = buildApi(activationPromise, serviceManager, serviceContainer); + return [api, activationPromise, serviceContainer]; } -///////////////////////////// -// telemetry - // tslint:disable-next-line:no-any -async function getActivationTelemetryProps(serviceContainer: IServiceContainer): Promise { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Not all of this data is showing up in the database... - // tslint:disable-next-line:no-suspicious-comment - // TODO: If any one of these parts fails we send no info. We should - // be able to partially populate as much as possible instead - // (through granular try-catch statements). - const terminalHelper = serviceContainer.get(ITerminalHelper); - const terminalShellType = terminalHelper.identifyTerminalShell(); - const condaLocator = serviceContainer.get(ICondaService); - const interpreterService = serviceContainer.get(IInterpreterService); - const workspaceService = serviceContainer.get(IWorkspaceService); - const configurationService = serviceContainer.get(IConfigurationService); - const mainWorkspaceUri = workspaceService.hasWorkspaceFolders - ? workspaceService.workspaceFolders![0].uri - : undefined; - const settings = configurationService.getSettings(mainWorkspaceUri); - const [condaVersion, interpreter, interpreters] = await Promise.all([ - condaLocator - .getCondaVersion() - .then(ver => (ver ? ver.raw : '')) - .catch(() => ''), - interpreterService.getActiveInterpreter().catch(() => undefined), - interpreterService.getInterpreters(mainWorkspaceUri).catch(() => []) - ]); - const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; - const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; - const interpreterType = interpreter ? interpreter.type : undefined; - const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); - const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); - const usingGlobalInterpreter = isUsingGlobalInterpreterInWorkspace(settings.pythonPath, serviceContainer); - const usingAutoSelectedWorkspaceInterpreter = preferredWorkspaceInterpreter - ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) - : false; - const hasPython3 = - interpreters!.filter(item => (item && item.version ? item.version.major === 3 : false)).length > 0; - - return { - condaVersion, - terminal: terminalShellType, - pythonVersion, - interpreterType, - workspaceFolderCount, - hasPython3, - usingUserDefinedInterpreter, - usingAutoSelectedWorkspaceInterpreter, - usingGlobalInterpreter - }; +function displayProgress(promise: Promise) { + const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; + window.withProgress(progressOptions, () => promise); } ///////////////////////////// // error handling -function handleError(ex: Error) { +async function handleError(ex: Error, startupDurations: Record) { notifyUser( "Extension activation failed, run the 'Developer: Toggle Developer Tools' command for more information." ); traceError('extension activation failed', ex); - sendErrorTelemetry(ex).ignoreErrors(); + await sendErrorTelemetry(ex, startupDurations, activatedServiceContainer); } interface IAppShell { @@ -428,23 +145,6 @@ function notifyUser(msg: string) { } appShell.showErrorMessage(msg).ignoreErrors(); } catch (ex) { - // ignore - } -} - -async function sendErrorTelemetry(ex: Error) { - try { - // tslint:disable-next-line:no-any - let props: any = {}; - if (activatedServiceContainer) { - try { - props = await getActivationTelemetryProps(activatedServiceContainer); - } catch (ex) { - // ignore - } - } - sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props, ex); - } catch (exc2) { - traceError('sendErrorTelemetry() failed.', exc2); + traceError('failed to notify user', ex); } } diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts new file mode 100644 index 000000000000..4097a08127de --- /dev/null +++ b/src/client/extensionActivation.ts @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { CodeActionKind, debug, DebugConfigurationProvider, languages, OutputChannel, window } from 'vscode'; + +import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; +import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types'; +import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; +import { IApplicationDiagnostics } from './application/types'; +import { DebugService } from './common/application/debugService'; +import { ICommandManager, IWorkspaceService } from './common/application/types'; +import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; +import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; +import { traceError } from './common/logger'; +import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; +import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; +import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IExtensionContext, + IFeatureDeprecationManager, + IOutputChannel +} from './common/types'; +import { OutputChannelNames } from './common/utils/localize'; +import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; +import { JUPYTER_OUTPUT_CHANNEL } from './datascience/constants'; +import { registerTypes as dataScienceRegisterTypes } from './datascience/serviceRegistry'; +import { IDataScience } from './datascience/types'; +import { DebuggerTypeName } from './debugger/constants'; +import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; +import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; +import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; +import { IDebugConfigurationService, IDebuggerBanner } from './debugger/extension/types'; +import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; +import { IInterpreterSelector } from './interpreter/configuration/types'; +import { + IInterpreterLocatorProgressHandler, + IInterpreterLocatorProgressService, + IInterpreterService +} from './interpreter/contracts'; +import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; +import { IServiceContainer, IServiceManager } from './ioc/types'; +import { getLanguageConfiguration } from './language/languageConfiguration'; +import { LinterCommands } from './linters/linterCommands'; +import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; +import { PythonCodeActionProvider } from './providers/codeActionProvider/pythonCodeActionProvider'; +import { PythonFormattingEditProvider } from './providers/formatProvider'; +import { ReplProvider } from './providers/replProvider'; +import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; +import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; +import { TerminalProvider } from './providers/terminalProvider'; +import { ISortImportsEditingProvider } from './providers/types'; +import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; +import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; +import { TEST_OUTPUT_CHANNEL } from './testing/common/constants'; +import { ITestContextService } from './testing/common/types'; +import { ITestCodeNavigatorCommandHandler, ITestExplorerCommandHandler } from './testing/navigation/types'; +import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; + +export async function activateComponents( + context: IExtensionContext, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer +) { + // We will be pulling code over from activateLegacy(). + + return activateLegacy(context, serviceManager, serviceContainer); +} + +///////////////////////////// +// old activation code + +// tslint:disable-next-line:no-suspicious-comment +// TODO(GH-10454): Gradually move simple initialization +// and DI registration currently in this function over +// to initializeComponents(). Likewise with complex +// init and activation: move them to activateComponents(). + +async function activateLegacy( + context: IExtensionContext, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer +) { + // register "services" + + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python()); + const unitTestOutChannel = window.createOutputChannel(OutputChannelNames.pythonTest()); + const jupyterOutputChannel = window.createOutputChannel(OutputChannelNames.jupyter()); + serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, jupyterOutputChannel, JUPYTER_OUTPUT_CHANNEL); + + commonRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + variableRegisterTypes(serviceManager); + unitTestsRegisterTypes(serviceManager); + lintersRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + formattersRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + dataScienceRegisterTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); + + const configuration = serviceManager.get(IConfigurationService); + const languageServerType = configuration.getSettings().languageServer; + + appRegisterTypes(serviceManager, languageServerType); + providersRegisterTypes(serviceManager); + activationRegisterTypes(serviceManager, languageServerType); + + // "initialize" "services" + + const abExperiments = serviceContainer.get(IExperimentsManager); + await abExperiments.activate(); + const selector = serviceContainer.get(IInterpreterSelector); + selector.initialize(); + context.subscriptions.push(selector); + + const interpreterManager = serviceContainer.get(IInterpreterService); + interpreterManager.initialize(); + + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const disposables = serviceManager.get(IDisposableRegistry); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); + + const cmdManager = serviceContainer.get(ICommandManager); + const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + + // Display progress of interpreter refreshes only after extension has activated. + serviceContainer.get(IInterpreterLocatorProgressHandler).register(); + serviceContainer.get(IInterpreterLocatorProgressService).register(); + serviceContainer.get(IApplicationDiagnostics).register(); + serviceContainer.get(ITestCodeNavigatorCommandHandler).register(); + serviceContainer.get(ITestExplorerCommandHandler).register(); + serviceContainer.get(ILanguageServerExtension).register(); + serviceContainer.get(ITestContextService).register(); + + // "activate" everything else + + const manager = serviceContainer.get(IExtensionActivationManager); + context.subscriptions.push(manager); + const activationPromise = manager.activate(); + + serviceManager.get(ITerminalAutoActivation).register(); + const pythonSettings = configuration.getSettings(); + + activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); + + const sortImports = serviceContainer.get(ISortImportsEditingProvider); + sortImports.registerCommands(); + + serviceManager.get(ICodeExecutionManager).registerCommands(); + + const workspaceService = serviceContainer.get(IWorkspaceService); + interpreterManager + .refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) + .catch(ex => traceError('Python Extension: interpreterManager.refresh', ex)); + + // Activate data science features + const dataScience = serviceManager.get(IDataScience); + dataScience.activate().ignoreErrors(); + + context.subscriptions.push(new LinterCommands(serviceManager)); + + languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + + if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'internalConsole') { + const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); + context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); + } + + const deprecationMgr = serviceContainer.get(IFeatureDeprecationManager); + deprecationMgr.initialize(); + context.subscriptions.push(deprecationMgr); + + context.subscriptions.push(new ReplProvider(serviceContainer)); + + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); + context.subscriptions.push(terminalProvider); + + context.subscriptions.push( + languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { + providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] + }) + ); + + serviceContainer.getAll(IDebugConfigurationService).forEach(debugConfigProvider => { + context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); + }); + + serviceContainer.get(IDebuggerBanner).initialize(); + + return activationPromise; +} diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts new file mode 100644 index 000000000000..27e155cd8d12 --- /dev/null +++ b/src/client/extensionInit.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { Container } from 'inversify'; +import { Disposable, Memento } from 'vscode'; + +import { GLOBAL_MEMENTO, IDisposableRegistry, IExtensionContext, IMemento, WORKSPACE_MEMENTO } from './common/types'; +import { ServiceContainer } from './ioc/container'; +import { ServiceManager } from './ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from './ioc/types'; + +// The code in this module should do nothing more complex than register +// objects to DI and simple init (e.g. no side effects). That implies +// that constructors are likewise simple and do no work. It also means +// that it is inherently synchronous. + +export function initializeGlobals(context: IExtensionContext): [IServiceManager, IServiceContainer] { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + const serviceContainer = new ServiceContainer(cont); + + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance(IServiceManager, serviceManager); + + serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); + serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(IExtensionContext, context); + + return [serviceManager, serviceContainer]; +} + +export function initializeComponents( + _context: IExtensionContext, + _serviceManager: IServiceManager, + _serviceContainer: IServiceContainer +) { + // We will be pulling code over from activateLegacy(). +} diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts new file mode 100644 index 000000000000..e87fe8bbd32e --- /dev/null +++ b/src/client/startupTelemetry.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IWorkspaceService } from './common/application/types'; +import { isTestExecution } from './common/constants'; +import { traceError } from './common/logger'; +import { ITerminalHelper } from './common/terminal/types'; +import { IConfigurationService, Resource } from './common/types'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService +} from './interpreter/autoSelection/types'; +import { ICondaService, IInterpreterService, PythonInterpreter } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { EditorLoadTelemetry } from './telemetry/types'; + +interface IStopWatch { + elapsedTime: number; +} + +export async function sendStartupTelemetry( + // tslint:disable-next-line:no-any + activatedPromise: Promise, + durations: Record, + stopWatch: IStopWatch, + serviceContainer: IServiceContainer +) { + if (isTestExecution()) { + return; + } + + try { + await activatedPromise; + durations.totalActivateTime = stopWatch.elapsedTime; + const props = await getActivationTelemetryProps(serviceContainer); + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); + } catch (ex) { + traceError('sendStartupTelemetry() failed.', ex); + } +} + +export async function sendErrorTelemetry( + ex: Error, + durations: Record, + serviceContainer?: IServiceContainer +) { + try { + // tslint:disable-next-line:no-any + let props: any = {}; + if (serviceContainer) { + try { + props = await getActivationTelemetryProps(serviceContainer); + } catch (ex) { + traceError('getActivationTelemetryProps() failed.', ex); + } + } + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props, ex); + } catch (exc2) { + traceError('sendErrorTelemetry() failed.', exc2); + } +} + +function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceContainer: IServiceContainer): boolean { + const service = serviceContainer.get(IInterpreterAutoSelectionService); + const globalInterpreter = service.getAutoSelectedInterpreter(undefined); + if (!globalInterpreter) { + return false; + } + return currentPythonPath === globalInterpreter.path; +} + +function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { + const workspaceService = serviceContainer.get(IWorkspaceService); + const settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; + return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || + (settings.workspaceValue && settings.workspaceValue !== 'python') || + (settings.globalValue && settings.globalValue !== 'python') + ? true + : false; +} + +function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { + const workspaceInterpreterSelector = serviceContainer.get( + IInterpreterAutoSelectionRule, + AutoSelectionRule.workspaceVirtualEnvs + ); + const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); + return interpreter ? interpreter.path : undefined; +} + +async function getActivationTelemetryProps(serviceContainer: IServiceContainer): Promise { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Not all of this data is showing up in the database... + // tslint:disable-next-line:no-suspicious-comment + // TODO: If any one of these parts fails we send no info. We should + // be able to partially populate as much as possible instead + // (through granular try-catch statements). + const terminalHelper = serviceContainer.get(ITerminalHelper); + const terminalShellType = terminalHelper.identifyTerminalShell(); + const condaLocator = serviceContainer.get(ICondaService); + const interpreterService = serviceContainer.get(IInterpreterService); + const workspaceService = serviceContainer.get(IWorkspaceService); + const configurationService = serviceContainer.get(IConfigurationService); + const mainWorkspaceUri = workspaceService.hasWorkspaceFolders + ? workspaceService.workspaceFolders![0].uri + : undefined; + const settings = configurationService.getSettings(mainWorkspaceUri); + const [condaVersion, interpreter, interpreters] = await Promise.all([ + condaLocator + .getCondaVersion() + .then(ver => (ver ? ver.raw : '')) + .catch(() => ''), + interpreterService.getActiveInterpreter().catch(() => undefined), + interpreterService.getInterpreters(mainWorkspaceUri).catch(() => []) + ]); + const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.type : undefined; + const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); + const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); + const usingGlobalInterpreter = isUsingGlobalInterpreterInWorkspace(settings.pythonPath, serviceContainer); + const usingAutoSelectedWorkspaceInterpreter = preferredWorkspaceInterpreter + ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) + : false; + const hasPython3 = + interpreters!.filter(item => (item && item.version ? item.version.major === 3 : false)).length > 0; + + return { + condaVersion, + terminal: terminalShellType, + pythonVersion, + interpreterType, + workspaceFolderCount, + hasPython3, + usingUserDefinedInterpreter, + usingAutoSelectedWorkspaceInterpreter, + usingGlobalInterpreter + }; +} diff --git a/src/test/extension.unit.test.ts b/src/test/api.functional.test.ts similarity index 62% rename from src/test/extension.unit.test.ts rename to src/test/api.functional.test.ts index 12d28ed09bc8..7fd600caacc3 100644 --- a/src/test/extension.unit.test.ts +++ b/src/test/api.functional.test.ts @@ -3,35 +3,44 @@ 'use strict'; -// tslint:disable:no-any +// tslint:disable:no-any max-func-body-length import { expect } from 'chai'; -import * as fs from 'fs'; -import * as glob from 'glob'; import * as path from 'path'; import { anyString, anything, instance, mock, when } from 'ts-mockito'; import { buildApi } from '../client/api'; -import { ApplicationEnvironment } from '../client/common/application/applicationEnvironment'; -import { IApplicationEnvironment } from '../client/common/application/types'; import { EXTENSION_ROOT_DIR } from '../client/common/constants'; import { ExperimentsManager } from '../client/common/experiments'; import { IExperimentsManager } from '../client/common/types'; import { DebugAdapterDescriptorFactory } from '../client/debugger/extension/adapter/factory'; import { IDebugAdapterDescriptorFactory } from '../client/debugger/extension/types'; +import { ServiceContainer } from '../client/ioc/container'; +import { ServiceManager } from '../client/ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -// tslint:disable-next-line: max-func-body-length -suite('Extension API Debugger', () => { +suite('Extension API - Debugger', () => { const expectedLauncherPath = `${EXTENSION_ROOT_DIR.fileToCommandArgument()}/pythonFiles/ptvsd_launcher.py`; const ptvsdPath = path.join('path', 'to', 'ptvsd'); const ptvsdHost = 'somehost'; const ptvsdPort = 12345; + let serviceManager: IServiceManager; + let serviceContainer: IServiceContainer; let experimentsManager: IExperimentsManager; let debugAdapterFactory: IDebugAdapterDescriptorFactory; setup(() => { + serviceManager = mock(ServiceManager); + serviceContainer = mock(ServiceContainer); experimentsManager = mock(ExperimentsManager); debugAdapterFactory = mock(DebugAdapterDescriptorFactory); + + when(serviceContainer.get(IExperimentsManager)) + // Return the mock. + .thenReturn(instance(experimentsManager)); + when(serviceContainer.get(IDebugAdapterDescriptorFactory)) + // Return the mock. + .thenReturn(instance(debugAdapterFactory)); }); function mockInExperiment(host: string, port: number, wait: boolean) { @@ -85,8 +94,8 @@ suite('Extension API Debugger', () => { const args = await buildApi( Promise.resolve(), - instance(experimentsManager), - instance(debugAdapterFactory) + instance(serviceManager), + instance(serviceContainer) ).debug.getRemoteLauncherCommand(ptvsdHost, ptvsdPort, waitForAttach); const expectedArgs = [expectedLauncherPath, '--default', '--host', ptvsdHost, '--port', ptvsdPort.toString()]; @@ -99,8 +108,8 @@ suite('Extension API Debugger', () => { const args = await buildApi( Promise.resolve(), - instance(experimentsManager), - instance(debugAdapterFactory) + instance(serviceManager), + instance(serviceContainer) ).debug.getRemoteLauncherCommand(ptvsdHost, ptvsdPort, waitForAttach); const expectedArgs = [ptvsdPath, '--host', ptvsdHost, '--port', ptvsdPort.toString()]; @@ -113,8 +122,8 @@ suite('Extension API Debugger', () => { const args = await buildApi( Promise.resolve(), - instance(experimentsManager), - instance(debugAdapterFactory) + instance(serviceManager), + instance(serviceContainer) ).debug.getRemoteLauncherCommand(ptvsdHost, ptvsdPort, waitForAttach); const expectedArgs = [ expectedLauncherPath, @@ -135,69 +144,11 @@ suite('Extension API Debugger', () => { const args = await buildApi( Promise.resolve(), - instance(experimentsManager), - instance(debugAdapterFactory) + instance(serviceManager), + instance(serviceContainer) ).debug.getRemoteLauncherCommand(ptvsdHost, ptvsdPort, waitForAttach); const expectedArgs = [ptvsdPath, '--host', ptvsdHost, '--port', ptvsdPort.toString(), '--wait']; expect(args).to.be.deep.equal(expectedArgs); }); }); - -suite('Extension version tests', () => { - let version: string; - let applicationEnvironment: IApplicationEnvironment; - const branchName = process.env.CI_BRANCH_NAME; - - suiteSetup(async function() { - // Skip the entire suite if running locally - if (!branchName) { - // tslint:disable-next-line: no-invalid-this - return this.skip(); - } - }); - - setup(() => { - applicationEnvironment = new ApplicationEnvironment(undefined as any, undefined as any, undefined as any); - version = applicationEnvironment.packageJson.version; - }); - - test('If we are running a pipeline in the master branch, the extension version in `package.json` should have the "-dev" suffix', async function() { - if (branchName !== 'master') { - // tslint:disable-next-line: no-invalid-this - return this.skip(); - } - - return expect( - version.endsWith('-dev'), - 'When running a pipeline in the master branch, the extension version in package.json should have the -dev suffix' - ).to.be.true; - }); - - test('If we are running a pipeline in the release branch, the extension version in `package.json` should not have the "-dev" suffix', async function() { - if (!branchName!.startsWith('release')) { - // tslint:disable-next-line: no-invalid-this - return this.skip(); - } - - return expect( - version.endsWith('-dev'), - 'When running a pipeline in the release branch, the extension version in package.json should not have the -dev suffix' - ).to.be.false; - }); -}); - -suite('Extension localization files', () => { - test('Load localization file', () => { - const filesFailed: string[] = []; - glob.sync('package.nls.*.json', { sync: true, cwd: EXTENSION_ROOT_DIR }).forEach(localizationFile => { - try { - JSON.parse(fs.readFileSync(path.join(EXTENSION_ROOT_DIR, localizationFile)).toString()); - } catch { - filesFailed.push(localizationFile); - } - }); - - expect(filesFailed).to.be.lengthOf(0, `Failed to load JSON for ${filesFailed.join(', ')}`); - }); -}); diff --git a/src/test/extension-version.functional.test.ts b/src/test/extension-version.functional.test.ts new file mode 100644 index 000000000000..70adee3a9489 --- /dev/null +++ b/src/test/extension-version.functional.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as path from 'path'; +import { ApplicationEnvironment } from '../client/common/application/applicationEnvironment'; +import { IApplicationEnvironment } from '../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; + +suite('Extension version tests', () => { + let version: string; + let applicationEnvironment: IApplicationEnvironment; + const branchName = process.env.CI_BRANCH_NAME; + + suiteSetup(async function() { + // Skip the entire suite if running locally + if (!branchName) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + }); + + setup(() => { + applicationEnvironment = new ApplicationEnvironment(undefined as any, undefined as any, undefined as any); + version = applicationEnvironment.packageJson.version; + }); + + test('If we are running a pipeline in the master branch, the extension version in `package.json` should have the "-dev" suffix', async function() { + if (branchName !== 'master') { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the master branch, the extension version in package.json should have the -dev suffix' + ).to.be.true; + }); + + test('If we are running a pipeline in the release branch, the extension version in `package.json` should not have the "-dev" suffix', async function() { + if (!branchName!.startsWith('release')) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the release branch, the extension version in package.json should not have the -dev suffix' + ).to.be.false; + }); +}); + +suite('Extension localization files', () => { + test('Load localization file', () => { + const filesFailed: string[] = []; + glob.sync('package.nls.*.json', { sync: true, cwd: EXTENSION_ROOT_DIR }).forEach(localizationFile => { + try { + JSON.parse(fs.readFileSync(path.join(EXTENSION_ROOT_DIR, localizationFile)).toString()); + } catch { + filesFailed.push(localizationFile); + } + }); + + expect(filesFailed).to.be.lengthOf(0, `Failed to load JSON for ${filesFailed.join(', ')}`); + }); +});