diff --git a/src/client/api.ts b/src/client/api.ts index 81a5f676cc22..d58e8de0a3df 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -11,7 +11,7 @@ import { PYLANCE_NAME } from './activation/node/languageClientFactory'; import { ILanguageServerOutputChannel } from './activation/types'; import { PythonExtension } from './api/types'; import { isTestExecution, PYTHON_LANGUAGE } from './common/constants'; -import { IConfigurationService, Resource } from './common/types'; +import { IConfigurationService, IExtensions, Resource } from './common/types'; import { getDebugpyLauncherArgs, getDebugpyPackagePath } from './debugger/extension/adapter/remoteLaunchers'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer, IServiceManager } from './ioc/types'; @@ -41,6 +41,7 @@ export function buildApi( TensorboardExtensionIntegration, ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); + const extensions = serviceContainer.get(IExtensions); const api: PythonExtension & { /** @@ -146,7 +147,10 @@ export function buildApi( stop: (client: BaseLanguageClient): Promise => client.stop(), getTelemetryReporter: () => getTelemetryReporter(), }, - environments: buildEnvironmentApi(discoveryApi, serviceContainer), + get environments() { + const info = extensions.determineExtensionFromCallStack(); + return buildEnvironmentApi(discoveryApi, serviceContainer, info.extensionId); + }, }; // In test environment return the DI Container. diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index e4b8f5bce73d..b9215b362f23 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -4,13 +4,24 @@ 'use strict'; -import { inject, injectable } from 'inversify'; +import { injectable } from 'inversify'; import { Event, Extension, extensions } from 'vscode'; import * as stacktrace from 'stack-trace'; -import * as path from 'path'; + import { IExtensions } from '../types'; -import { IFileSystem } from '../platform/types'; -import { EXTENSION_ROOT_DIR } from '../constants'; +import { PVSC_EXTENSION_ID } from '../constants'; +import { traceError } from '../../logging'; + +function parseStack(ex: Error) { + // Work around bug in stackTrace when ex has an array already + if (ex.stack && Array.isArray(ex.stack)) { + const concatenated = { ...ex, stack: ex.stack.join('\n') }; + // Work around for https://github.com/microsoft/vscode-jupyter/issues/12550 + return stacktrace.parse.call(stacktrace, concatenated); + } + // Work around for https://github.com/microsoft/vscode-jupyter/issues/12550 + return stacktrace.parse.call(stacktrace, ex); +} /** * Provides functions for tracking the list of extensions that VSCode has installed. @@ -20,8 +31,6 @@ export class Extensions implements IExtensions { // eslint-disable-next-line @typescript-eslint/no-explicit-any private _cachedExtensions?: readonly Extension[]; - constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any public get all(): readonly Extension[] { return extensions.all; @@ -49,52 +58,46 @@ export class Extensions implements IExtensions { * Code borrowed from: * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts */ - public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + public determineExtensionFromCallStack(): { extensionId: string; displayName: string } { const { stack } = new Error(); - if (stack) { - const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep); - const frames = stack - .split('\n') - .map((f) => { - const result = /\((.*)\)/.exec(f); - if (result) { - return result[1]; - } - return undefined; - }) - .filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot)) - .filter((item) => - // Use cached list of extensions as we need this to be fast. - this.cachedExtensions.some( - (ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath), - ), - ) as string[]; - stacktrace.parse(new Error('Ex')).forEach((item) => { - const fileName = item.getFileName(); - if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) { - frames.push(fileName); - } - }); - for (const frame of frames) { - // This file is from a different extension. Try to find its `package.json`. - let dirName = path.dirname(frame); - let last = frame; - while (dirName && dirName.length < last.length) { - const possiblePackageJson = path.join(dirName, 'package.json'); - if (await this.fs.pathExists(possiblePackageJson)) { - const text = await this.fs.readFile(possiblePackageJson); - try { - const json = JSON.parse(text); - return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName }; - } catch { - // If parse fails, then not an extension. + try { + if (stack) { + const jupyterExtRoot = extensions + .getExtension(PVSC_EXTENSION_ID)! + .extensionUri.toString() + .toLowerCase(); + const frames = stack + .split('\n') + .map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; } + return undefined; + }) + .filter((item) => item && !item.toLowerCase().startsWith(jupyterExtRoot)) as string[]; + parseStack(new Error('Ex')).forEach((item) => { + const fileName = item.getFileName(); + if (fileName && !fileName.toLowerCase().startsWith(jupyterExtRoot)) { + frames.push(fileName); + } + }); + for (const frame of frames) { + const matchingExt = this.cachedExtensions.find( + (ext) => + ext.id !== PVSC_EXTENSION_ID && + (frame.toLowerCase().startsWith(ext.extensionUri.fsPath.toLowerCase()) || + frame.toLowerCase().startsWith(ext.extensionUri.path.toLowerCase())), + ); + if (matchingExt) { + return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName }; } - last = dirName; - dirName = path.dirname(dirName); } } + return { extensionId: 'unknown', displayName: 'unknown' }; + } catch (ex) { + traceError(`Unable to determine the caller of the extension API for trace stack.`, stack, ex); + return { extensionId: 'unknown', displayName: 'unknown' }; } - return { extensionId: 'unknown', displayName: 'unknown' }; } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 67fcf5c7b700..65af0896d376 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -308,7 +308,7 @@ export interface IExtensions { /** * Determines which extension called into our extension code based on call stacks. */ - determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; + determineExtensionFromCallStack(): { extensionId: string; displayName: string }; } export const IBrowserService = Symbol('IBrowserService'); diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index e63670e4bf1b..9c5f70b97f1c 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -65,22 +65,16 @@ export function buildDeprecatedProposedApi( const extensions = serviceContainer.get(IExtensions); const warningLogged = new Set(); function sendApiTelemetry(apiName: string, warnLog = true) { - extensions - .determineExtensionFromCallStack() - .then((info) => { - sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { - apiName, - extensionId: info.extensionId, - }); - traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); - if (warnLog && !warningLogged.has(info.extensionId)) { - console.warn( - `${info.extensionId} extension is using deprecated python APIs which will be removed soon.`, - ); - warningLogged.add(info.extensionId); - } - }) - .ignoreErrors(); + const info = extensions.determineExtensionFromCallStack(); + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + if (warnLog && !warningLogged.has(info.extensionId)) { + console.warn(`${info.extensionId} extension is using deprecated python APIs which will be removed soon.`); + warningLogged.add(info.extensionId); + } } const proposed: DeprecatedProposedAPI = { diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index da6a132b2b44..093b23074882 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -4,7 +4,7 @@ import { ConfigurationTarget, EventEmitter, Uri, workspace, WorkspaceFolder } from 'vscode'; import * as pathUtils from 'path'; -import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; import { IServiceContainer } from './ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; @@ -114,23 +114,18 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) { export function buildEnvironmentApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, + extensionId: string, ): PythonExtension['environments'] { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); - const extensions = serviceContainer.get(IExtensions); const envVarsProvider = serviceContainer.get(IEnvironmentVariablesProvider); function sendApiTelemetry(apiName: string, args?: unknown) { - extensions - .determineExtensionFromCallStack() - .then((info) => { - sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { - apiName, - extensionId: info.extensionId, - }); - traceVerbose(`Extension ${info.extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`); - }) - .ignoreErrors(); + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId, + }); + traceVerbose(`Extension ${extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`); } disposables.push( discoveryApi.onChanged((e) => { diff --git a/src/test/environmentApi.unit.test.ts b/src/test/environmentApi.unit.test.ts index 1d8dc3e5c847..5d7a7feb0ec2 100644 --- a/src/test/environmentApi.unit.test.ts +++ b/src/test/environmentApi.unit.test.ts @@ -72,10 +72,6 @@ suite('Python Environment API', () => { extensions = typemoq.Mock.ofType(); workspaceService = typemoq.Mock.ofType(); envVarsProvider = typemoq.Mock.ofType(); - extensions - .setup((e) => e.determineExtensionFromCallStack()) - .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })) - .verifiable(typemoq.Times.atLeastOnce()); interpreterPathService = typemoq.Mock.ofType(); configService = typemoq.Mock.ofType(); onDidChangeRefreshState = new EventEmitter(); @@ -95,7 +91,7 @@ suite('Python Environment API', () => { discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); - environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object); + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, ''); }); teardown(() => { @@ -325,6 +321,7 @@ suite('Python Environment API', () => { }, ]; discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, ''); const actual = environmentApi.known; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual(