From 395e3016d462736f0590f3bb7de7f502813956e0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 5 Mar 2024 20:13:47 +1100 Subject: [PATCH 1/3] Identification of caller on remote servers --- .../application/extension.node.unit.test.ts | 120 ++++++++++++++++++ .../common/application/extensions.node.ts | 67 +++++++++- 2 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 src/platform/common/application/extension.node.unit.test.ts diff --git a/src/platform/common/application/extension.node.unit.test.ts b/src/platform/common/application/extension.node.unit.test.ts new file mode 100644 index 00000000000..60ab89219c2 --- /dev/null +++ b/src/platform/common/application/extension.node.unit.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { anything, when } from 'ts-mockito'; +import { Disposable, EventEmitter, Uri } from 'vscode'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../../test/vscode-mock'; +import { DisposableStore } from '../utils/lifecycle'; +import { Extensions } from './extensions.node'; +import { EOL } from 'os'; + +const stack1 = [ + 'Error: ', + ' at Extensions.determineExtensionFromCallStack (/Users/username/Development/vsc/vscode-jupyter/src/platform/common/application/extensions.node.ts:18:26)', + ' at JupyterKernelServiceFactory.getService (/Users/username/Development/vsc/vscode-jupyter/src/standalone/api/unstable/kernelApi.ts:43:38)', + ' at getKernelService (/Users/username/Development/vsc/vscode-jupyter/src/standalone/api/unstable/index.ts:92:33)', + ' at Object.getKernelService (/Users/username/Development/vsc/vscode-jupyter/src/standalone/api/index.ts:43:33)', + ' at activateFeature (/Users/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:13466:56)', + ' at activate (/Users/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:13479:9)', + ' at activate (/Users/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:14160:5)', + ' at u.n (/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js:140:6255)', + ' at u.m (/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js:140:6218)', + ' at u.l (/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/out/vs/workbench/api/node/extensionHostProcess.js:140:5675)' +]; +const stack2 = [ + 'Error:', + ' at kg.determineExtensionFromCallStack (/storage/username/.vscode-insiders/extensions/ms-toolsai.jupyter-2024.3.0/dist/extension.node.js:203:3695)', + ' at F9 (/storage/username/.vscode-insiders/extensions/ms-toolsai.jupyter-2024.3.0/dist/extension.node.js:198:24742)', + ' at Object.getKernelService (/storage/username/.vscode-insiders/extensions/ms-toolsai.jupyter-2024.3.0/dist/extension.node.js:198:26312)', + ' at activateFeature (/storage/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:335:56)', + ' at process.processTicksAndRejections (node:internal/process/task_queues:95:5)', + ' at async activate (/storage/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:343:9)', + ' at async activate (/storage/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js:13995:5)', + ' at async u.n (/storage/username/.vscode-insiders/cli/servers/Insiders-bb171489789c9a49e985a4a2c8694138d70d42c1/server/out/vs/workbench/api/node/extensionHostProcess.js:140:6255)', + ' at async u.m (/storage/username/.vscode-insiders/cli/servers/Insiders-bb171489789c9a49e985a4a2c8694138d70d42c1/server/out/vs/workbench/api/node/extensionHostProcess.js:140:6218)', + ' at async u.l (/storage/username/.vscode-insiders/cli/servers/Insiders-bb171489789c9a49e985a4a2c8694138d70d42c1/server/out/vs/workbench/api/node/extensionHostProcess.js:140:5675)' +]; +const extensions1 = [ + { + id: 'vscode.bat', + packageJSON: { displayName: 'vscode.bat' }, + extensionUri: Uri.file('/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions/bat') + }, + { + id: 'donjayamanne.kusto', + packageJSON: { displayName: 'donjayamanne.kusto' }, + extensionUri: Uri.file('/Users/username/.vscode-insiders/extensions/donjayamanne.kusto-0.4.4') + }, + { + id: 'ms-python.python', + packageJSON: { displayName: 'ms-python.python' }, + extensionUri: Uri.file('/Users/username/.vscode-insiders/extensions/ms-python.python-2024.3.10640539') + }, + { + id: 'ms-toolsai.vscode-jupyter-powertoys', + packageJSON: { displayName: 'ms-toolsai.vscode-jupyter-powertoys' }, + extensionUri: Uri.file('/Users/username/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0') + }, + { + id: 'ms-toolsai.jupyter', + packageJSON: { displayName: 'ms-toolsai.jupyter' }, + extensionUri: Uri.file('/Users/username/Development/vsc/vscode-jupyter') + } +]; +const extensions2 = [ + { + id: 'vscode.bat', + packageJSON: { displayName: 'vscode.bat', version: '1.0.0' }, + extensionUri: Uri.file('/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions/bat') + }, + { + id: 'donjayamanne.kusto', + packageJSON: { displayName: 'donjayamanne.kusto', version: '0.4.4' }, + extensionUri: Uri.file('/Users/username/.vscode-insiders/extensions/donjayamanne.kusto-0.4.4') + }, + { + id: 'ms-python.python', + packageJSON: { displayName: 'ms-python.python', version: '2024.3.10640539' }, + extensionUri: Uri.file('/storage/username/.vscode-server-insiders/extensions/ms-python.python-2024.3.10640539') + }, + { + id: 'ms-toolsai.vscode-jupyter-powertoys', + packageJSON: { displayName: 'ms-toolsai.vscode-jupyter-powertoys', version: '0.1.0' }, + extensionUri: Uri.file( + '/storage/username/.vscode-server-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0' + ) + }, + { + id: 'ms-toolsai.jupyter', + packageJSON: { displayName: 'ms-toolsai.jupyter', version: '2024.3.0' }, + extensionUri: Uri.file('/storage/username/.vscode-server-insiders/extensions/ms-toolsai.jupyter-2024.3.0') + } +]; + +suite(`Interpreter Service`, () => { + const disposables = new DisposableStore(); + setup(() => { + when(mockedVSCodeNamespaces.extensions.onDidChange).thenReturn(disposables.add(new EventEmitter()).event); + disposables.add(new Disposable(() => resetVSCodeMocks())); + }); + teardown(() => disposables.clear()); + test('Identify from callstack', () => { + when(mockedVSCodeNamespaces.extensions.all).thenReturn(extensions1 as any); + when(mockedVSCodeNamespaces.extensions.getExtension(anything())).thenCall(function (id: string) { + return extensions1.find((e) => e.id === id); + }); + const { displayName, extensionId } = new Extensions([]).determineExtensionFromCallStack(stack1.join(EOL)); + assert.strictEqual(extensionId, 'ms-toolsai.vscode-jupyter-powertoys'); + assert.strictEqual(displayName, 'ms-toolsai.vscode-jupyter-powertoys'); + }); + test('Identify from callstack on remote server', () => { + when(mockedVSCodeNamespaces.extensions.all).thenReturn(extensions2 as any); + when(mockedVSCodeNamespaces.extensions.getExtension(anything())).thenCall(function (id: string) { + return extensions2.find((e) => e.id === id); + }); + const { displayName, extensionId } = new Extensions([]).determineExtensionFromCallStack(stack2.join(EOL)); + assert.strictEqual(extensionId, 'ms-toolsai.vscode-jupyter-powertoys'); + assert.strictEqual(displayName, 'ms-toolsai.vscode-jupyter-powertoys'); + }); +}); diff --git a/src/platform/common/application/extensions.node.ts b/src/platform/common/application/extensions.node.ts index a73e20fac76..7e5f104d2e2 100644 --- a/src/platform/common/application/extensions.node.ts +++ b/src/platform/common/application/extensions.node.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { injectable } from 'inversify'; -import { extensions } from 'vscode'; -import { IExtensions } from '../types'; +import { inject, injectable } from 'inversify'; +import { extensions, type Extension } from 'vscode'; +import { IDisposableRegistry, IExtensions } from '../types'; import { DataScience } from '../utils/localize'; import { parseStack } from '../../errors'; import { JVSC_EXTENSION_ID, unknownExtensionId } from '../constants'; @@ -14,6 +14,14 @@ import { traceError } from '../../logging'; */ @injectable() export class Extensions implements IExtensions { + private _extensions: readonly Extension[] = []; + private get extensions() { + return this._extensions; + } + constructor(@inject(IDisposableRegistry) disposables: IDisposableRegistry) { + disposables.push(extensions.onDidChange(() => (this._extensions = extensions.all))); + this._extensions = extensions.all; + } public determineExtensionFromCallStack(stack?: string): { extensionId: string; displayName: string } { stack = stack || new Error().stack; try { @@ -26,11 +34,37 @@ export class Extensions implements IExtensions { .split('\n') .map((f) => { const result = /\((.*)\)/.exec(f); - if (result) { - return result[1]; + const filenameWithPositions = result ? result[1] : undefined; + try { + const filename = /\((.*)\:\d*\:\d*\)/.exec(f); + if (!filename) { + return filenameWithPositions; + } + if (!filenameWithPositions) { + return filename[1]; + } + if (filenameWithPositions.startsWith(filename[1])) { + return filename[1]; + } + } catch { + // } + return filenameWithPositions; }) .filter((item) => item && !item.toLowerCase().startsWith(jupyterExtRoot)) as string[]; + const folderParts = jupyterExtRoot.split(/[\\/]/); + const indexOfJupyterExtFolder = folderParts.findIndex((item) => item.startsWith(JVSC_EXTENSION_ID)); + const extensionFolderName = + indexOfJupyterExtFolder === -1 ? undefined : folderParts[indexOfJupyterExtFolder - 1]; + + // We're just after the extensions folder. + let extensionPathFromFrames = frames.find((frame) => frame.includes(JVSC_EXTENSION_ID)); + if (extensionPathFromFrames) { + extensionPathFromFrames = extensionPathFromFrames.substring( + 0, + extensionPathFromFrames.indexOf(JVSC_EXTENSION_ID) - 1 + ); + } parseStack(new Error('Ex')).forEach((item) => { const fileName = item.getFileName(); if (fileName && !fileName.toLowerCase().startsWith(jupyterExtRoot)) { @@ -38,7 +72,7 @@ export class Extensions implements IExtensions { } }); for (const frame of frames) { - const matchingExt = extensions.all.find( + const matchingExt = this.extensions.find( (ext) => ext.id !== JVSC_EXTENSION_ID && (frame.toLowerCase().startsWith(ext.extensionUri.fsPath.toLowerCase()) || @@ -48,7 +82,28 @@ export class Extensions implements IExtensions { return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName }; } } + if (!extensionFolderName || !extensionPathFromFrames) { + return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; + } + // Possible Jupyter extension root is ~/.vscode-server-insiders/extensions/ms-toolsai.jupyter-2024.3.0 + // But call stack has paths such as ~/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js + for (const frame of frames.filter( + (f) => f.startsWith(extensionPathFromFrames) && !f.includes(JVSC_EXTENSION_ID) + )) { + let extensionIdInFrame = frame + .substring(extensionPathFromFrames.length) + .substring(1) + .split(/[\\/]/)[0]; + if (extensionIdInFrame.includes('-')) { + extensionIdInFrame = extensionIdInFrame.substring(0, extensionIdInFrame.lastIndexOf('-')); + } + const matchingExt = this.extensions.find((ext) => ext.id === extensionIdInFrame); + if (matchingExt) { + return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName }; + } + } } + traceError(`Unable to determine the caller of the extension API for trace stack.`, stack); return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; } catch (ex) { traceError(`Unable to determine the caller of the extension API for trace stack.`, stack); From 7d5b8c8c6986928bda1d17465899c7d01c8139ea Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 5 Mar 2024 20:56:47 +1100 Subject: [PATCH 2/3] misc --- .../common/application/extensions.node.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/platform/common/application/extensions.node.ts b/src/platform/common/application/extensions.node.ts index 7e5f104d2e2..dc563d5f99e 100644 --- a/src/platform/common/application/extensions.node.ts +++ b/src/platform/common/application/extensions.node.ts @@ -57,14 +57,6 @@ export class Extensions implements IExtensions { const extensionFolderName = indexOfJupyterExtFolder === -1 ? undefined : folderParts[indexOfJupyterExtFolder - 1]; - // We're just after the extensions folder. - let extensionPathFromFrames = frames.find((frame) => frame.includes(JVSC_EXTENSION_ID)); - if (extensionPathFromFrames) { - extensionPathFromFrames = extensionPathFromFrames.substring( - 0, - extensionPathFromFrames.indexOf(JVSC_EXTENSION_ID) - 1 - ); - } parseStack(new Error('Ex')).forEach((item) => { const fileName = item.getFileName(); if (fileName && !fileName.toLowerCase().startsWith(jupyterExtRoot)) { @@ -82,14 +74,23 @@ export class Extensions implements IExtensions { return { extensionId: matchingExt.id, displayName: matchingExt.packageJSON.displayName }; } } + // We're just after the extensions folder. + let extensionPathFromFrames = frames.find((frame) => frame.includes(JVSC_EXTENSION_ID)); + if (extensionPathFromFrames) { + extensionPathFromFrames = extensionPathFromFrames.substring( + 0, + extensionPathFromFrames.indexOf(JVSC_EXTENSION_ID) - 1 + ); + } + if (!extensionFolderName || !extensionPathFromFrames) { return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; } // Possible Jupyter extension root is ~/.vscode-server-insiders/extensions/ms-toolsai.jupyter-2024.3.0 // But call stack has paths such as ~/.vscode-insiders/extensions/ms-toolsai.vscode-jupyter-powertoys-0.1.0/out/main.js - for (const frame of frames.filter( - (f) => f.startsWith(extensionPathFromFrames) && !f.includes(JVSC_EXTENSION_ID) - )) { + for (const frame of frames.filter((f) => { + return f.startsWith(extensionPathFromFrames!) && !f.includes(JVSC_EXTENSION_ID); + })) { let extensionIdInFrame = frame .substring(extensionPathFromFrames.length) .substring(1) From 016064d7ea1fe8152283c45e7678c90c11d7bd25 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 6 Mar 2024 12:10:19 +1100 Subject: [PATCH 3/3] Logging in case of failures --- src/platform/common/application/extensions.node.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/platform/common/application/extensions.node.ts b/src/platform/common/application/extensions.node.ts index dc563d5f99e..ecd116a27fe 100644 --- a/src/platform/common/application/extensions.node.ts +++ b/src/platform/common/application/extensions.node.ts @@ -25,12 +25,10 @@ export class Extensions implements IExtensions { public determineExtensionFromCallStack(stack?: string): { extensionId: string; displayName: string } { stack = stack || new Error().stack; try { + const jupyterExtRoot = extensions.getExtension(JVSC_EXTENSION_ID)!.extensionUri.toString().toLowerCase(); + let frames: string[] = []; if (stack) { - const jupyterExtRoot = extensions - .getExtension(JVSC_EXTENSION_ID)! - .extensionUri.toString() - .toLowerCase(); - const frames = stack + frames = stack .split('\n') .map((f) => { const result = /\((.*)\)/.exec(f); @@ -104,7 +102,9 @@ export class Extensions implements IExtensions { } } } - traceError(`Unable to determine the caller of the extension API for trace stack.`, stack); + traceError(`Unable to determine the caller of the extension API for trace stack`, stack); + traceError(`Jupyter Root`, jupyterExtRoot); + traceError(`Frames`, frames); return { extensionId: unknownExtensionId, displayName: DataScience.unknownPackage }; } catch (ex) { traceError(`Unable to determine the caller of the extension API for trace stack.`, stack);