Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Determine calling ext without I/O operations #22998

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,6 +41,7 @@ export function buildApi(
TensorboardExtensionIntegration,
);
const outputChannel = serviceContainer.get<ILanguageServerOutputChannel>(ILanguageServerOutputChannel);
const extensions = serviceContainer.get<IExtensions>(IExtensions);

const api: PythonExtension & {
/**
Expand Down Expand Up @@ -146,7 +147,10 @@ export function buildApi(
stop: (client: BaseLanguageClient): Promise<void> => 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.
Expand Down
97 changes: 50 additions & 47 deletions src/client/common/application/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,8 +31,6 @@ export class Extensions implements IExtensions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _cachedExtensions?: readonly Extension<any>[];

constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public get all(): readonly Extension<any>[] {
return extensions.all;
Expand Down Expand Up @@ -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' };
}
}
2 changes: 1 addition & 1 deletion src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
26 changes: 10 additions & 16 deletions src/client/deprecatedProposedApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,16 @@ export function buildDeprecatedProposedApi(
const extensions = serviceContainer.get<IExtensions>(IExtensions);
const warningLogged = new Set<string>();
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 = {
Expand Down
19 changes: 7 additions & 12 deletions src/client/environmentApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -114,23 +114,18 @@ function filterUsingVSCodeContext(e: PythonEnvInfo) {
export function buildEnvironmentApi(
discoveryApi: IDiscoveryAPI,
serviceContainer: IServiceContainer,
extensionId: string,
): PythonExtension['environments'] {
const interpreterPathService = serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
const configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry);
const extensions = serviceContainer.get<IExtensions>(IExtensions);
const envVarsProvider = serviceContainer.get<IEnvironmentVariablesProvider>(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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isParentPath,
pathExists,
pathExistsSync,
readFileSync,
readFile,
shellExecute,
} from '../externalDependencies';
import { getEnvironmentDirFromPath } from '../commonUtils';
Expand Down Expand Up @@ -63,7 +63,7 @@ async function isLocalPoetryEnvironment(interpreterPath: string): Promise<boolea
return false;
}
const project = path.dirname(envDir);
if (!hasValidPyprojectToml(project)) {
if (!(await hasValidPyprojectToml(project))) {
return false;
}
// The assumption is that we need to be able to run poetry CLI for an environment in order to mark it as poetry.
Expand Down Expand Up @@ -126,7 +126,7 @@ export class Poetry {
*/
public static async getPoetry(cwd: string): Promise<Poetry | undefined> {
// Following check should be performed synchronously so we trigger poetry execution as soon as possible.
if (!hasValidPyprojectToml(cwd)) {
if (!(await hasValidPyprojectToml(cwd))) {
// This check is not expensive and may change during a session, so we need not cache it.
return undefined;
}
Expand Down Expand Up @@ -325,12 +325,12 @@ export async function isPoetryEnvironmentRelatedToFolder(
*
* @param folder Folder to look for pyproject.toml file in.
*/
function hasValidPyprojectToml(folder: string): boolean {
async function hasValidPyprojectToml(folder: string): Promise<boolean> {
const pyprojectToml = path.join(folder, 'pyproject.toml');
if (!pathExistsSync(pyprojectToml)) {
return false;
}
const content = readFileSync(pyprojectToml);
const content = await readFile(pyprojectToml);
if (!content.includes('[tool.poetry]')) {
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/test/environmentApi.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ suite('Python Environment API', () => {
envVarsProvider = typemoq.Mock.ofType<IEnvironmentVariablesProvider>();
extensions
.setup((e) => e.determineExtensionFromCallStack())
.returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' }))
.returns(() => ({ extensionId: 'id', displayName: 'displayName' }))
.verifiable(typemoq.Times.atLeastOnce());
interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>();
configService = typemoq.Mock.ofType<IConfigurationService>();
Expand All @@ -95,7 +95,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(() => {
Expand Down
Loading