From e30f2352d392b1893b22f37321263ebf4fdc5274 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Oct 2023 09:33:36 +1100 Subject: [PATCH 1/4] Wip --- src/client/api.ts | 17 ++++ src/client/common/constants.ts | 1 + src/client/tensorBoard/serviceRegistry.ts | 2 + src/client/tensorBoard/tensorBoardSession.ts | 9 +- .../tensorBoard/tensorBoardSessionProvider.ts | 2 +- .../tensorboardDependencyChecker.ts | 61 +++++++++++++ .../tensorBoard/tensorboardIntegration.ts | 87 +++++++++++++++++++ src/client/tensorBoard/types.ts | 6 +- 8 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/client/tensorBoard/tensorboardDependencyChecker.ts create mode 100644 src/client/tensorBoard/tensorboardIntegration.ts diff --git a/src/client/api.ts b/src/client/api.ts index 23b2553c93d2..81a5f676cc22 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -21,6 +21,7 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { buildEnvironmentApi } from './environmentApi'; import { ApiForPylance } from './pylanceApi'; import { getTelemetryReporter } from './telemetry'; +import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration'; export function buildApi( ready: Promise, @@ -31,7 +32,14 @@ export function buildApi( const configurationService = serviceContainer.get(IConfigurationService); const interpreterService = serviceContainer.get(IInterpreterService); serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); const api: PythonExtension & { @@ -41,6 +49,12 @@ export function buildApi( jupyter: { registerHooks(): void; }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; } & { /** * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an @@ -92,6 +106,9 @@ export function buildApi( jupyter: { registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(), }, + tensorboard: { + registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(), + }, debug: { async getRemoteLauncherCommand( host: string, diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index cd6d305f624a..6fc743fb8a0a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -23,6 +23,7 @@ export const PYTHON_NOTEBOOKS = [ export const PVSC_EXTENSION_ID = 'ms-python.python'; export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; +export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard'; export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; export type Channel = 'stable' | 'insiders'; diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index 8d16766f70c5..dd193f528eea 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -10,6 +10,7 @@ import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; import { TerminalWatcher } from './terminalWatcher'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); @@ -32,4 +33,5 @@ export function registerTypes(serviceManager: IServiceManager): void { ); serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); } diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 1d24e8c313f7..3bbf55b89c21 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -100,7 +100,10 @@ export class TensorBoardSession { private readonly globalMemento: IPersistentState, private readonly multiStepFactory: IMultiStepInputFactory, private readonly configurationService: IConfigurationService, - ) {} + ) { + this.disposables.push(this.onDidChangeViewStateEventEmitter) + this.disposables.push(this.onDidDisposeEventEmitter) + } public get onDidDispose(): Event { return this.onDidDisposeEventEmitter.event; @@ -189,10 +192,10 @@ export class TensorBoardSession { // to start a TensorBoard session. If the user has a torch import in // any of their open documents, also try to install the torch-tb-plugin // package, but don't block if installing that fails. - private async ensurePrerequisitesAreInstalled() { + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); const interpreter = - (await this.interpreterService.getActiveInterpreter()) || + (await this.interpreterService.getActiveInterpreter(resource)) || (await this.commandManager.executeCommand('python.setInterpreter')); if (!interpreter) { return false; diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index 53878bd543c2..41f630056741 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -23,7 +23,7 @@ import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; -const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; +export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @injectable() export class TensorBoardSessionProvider implements IExtensionSingleActivationService { diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..de6454716c9b --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri, ViewColumn } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { + IInstaller, + IPersistentState, + IPersistentStateFactory, + IConfigurationService, + IDisposable, +} from '../common/types'; +import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; +import { disposeAll } from '../common/utils/resourceLifecycle'; +import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; +import { ITensorboardDependencyChecker } from './types'; + +@injectable() +export class TensorboardDependencyChecker implements ITensorboardDependencyChecker { + private preferredViewGroupMemento: IPersistentState; + + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( + PREFERRED_VIEWGROUP, + ViewColumn.Active, + ); + } + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const disposables: IDisposable[] = []; + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.workspaceService, + this.pythonExecFactory, + this.commandManager, + disposables, + this.applicationShell, + this.preferredViewGroupMemento, + this.multiStepFactory, + this.configurationService, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + disposeAll(disposables); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..fb7d34fb8031 --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,87 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ITensorboardDependencyChecker } from './types'; + +type PythonApiForTensorboardExtension = { + /** + * IEnvironmentActivationService + */ + getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + ): Promise; + ensureDependenciesAreInstalled(resource?: Uri): Promise; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(ITensorboardDependencyChecker) private readonly dependencyChcker: ITensorboardDependencyChecker, + ) {} + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + // Forward python parts + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async ( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + ) => this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + }); + return undefined; + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts index 6e2c274d63f4..7b3514d7d854 100644 --- a/src/client/tensorBoard/types.ts +++ b/src/client/tensorBoard/types.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event } from 'vscode'; +import { Event, Uri } from 'vscode'; export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); export interface ITensorBoardImportTracker { onDidImportTensorBoard: Event; } + +export interface ITensorboardDependencyChecker { + ensureDependenciesAreInstalled(resource?: Uri): Promise; +} From da544c80cd6cdf58623359b9aeb65d2ca0f0543c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Oct 2023 09:35:51 +1100 Subject: [PATCH 2/4] Misc --- src/client/tensorBoard/tensorboardIntegration.ts | 4 ---- src/client/tensorBoard/types.ts | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts index fb7d34fb8031..a68c6d18a4f0 100644 --- a/src/client/tensorBoard/tensorboardIntegration.ts +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -14,9 +14,6 @@ import { PythonEnvironment } from '../pythonEnvironments/info'; import { ITensorboardDependencyChecker } from './types'; type PythonApiForTensorboardExtension = { - /** - * IEnvironmentActivationService - */ getActivatedEnvironmentVariables( resource: Resource, interpreter?: PythonEnvironment, @@ -48,7 +45,6 @@ export class TensorboardExtensionIntegration { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); return undefined; } - // Forward python parts tensorboardExtensionApi.registerPythonApi({ getActivatedEnvironmentVariables: async ( resource: Resource, diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts index 7b3514d7d854..a11659015da8 100644 --- a/src/client/tensorBoard/types.ts +++ b/src/client/tensorBoard/types.ts @@ -8,6 +8,7 @@ export interface ITensorBoardImportTracker { onDidImportTensorBoard: Event; } +export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); export interface ITensorboardDependencyChecker { ensureDependenciesAreInstalled(resource?: Uri): Promise; } From d13d8d934b4d38eff6efe90a5327ad819678b4a9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Oct 2023 13:36:49 +1100 Subject: [PATCH 3/4] Move tensorboard support into separate extension --- package.json | 4 +- .../nbextensionCodeLensProvider.ts | 4 ++ .../tensorBoard/tensorBoardFileWatcher.ts | 4 ++ .../tensorBoardImportCodeLensProvider.ts | 4 ++ src/client/tensorBoard/tensorBoardPrompt.ts | 2 +- .../tensorBoard/tensorBoardSessionProvider.ts | 5 ++ .../tensorBoard/tensorBoardUsageTracker.ts | 4 ++ .../tensorBoard/tensorboarExperiment.ts | 8 +++ .../tensorboardDependencyChecker.ts | 3 +- .../tensorBoard/tensorboardIntegration.ts | 51 +++++++++++++------ src/client/tensorBoard/terminalWatcher.ts | 4 ++ 11 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 src/client/tensorBoard/tensorboarExperiment.ts diff --git a/package.json b/package.json index 0f93441c6113..8308019d0d0f 100644 --- a/package.json +++ b/package.json @@ -1836,7 +1836,7 @@ "category": "Python", "command": "python.launchTensorBoard", "title": "%python.command.python.launchTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", @@ -1844,7 +1844,7 @@ "enablement": "python.hasActiveTensorBoardSession", "icon": "$(refresh)", "title": "%python.command.python.refreshTensorBoard.title%", - "when": "!virtualWorkspace && shellExecutionSupported" + "when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled" }, { "category": "Python", diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts index 6d4c844cd392..7b9a116ee144 100644 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ b/src/client/tensorBoard/nbextensionCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsNotebookExtension } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts index 81c62f1f8de3..dccdb95290ec 100644 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ b/src/client/tensorBoard/tensorBoardFileWatcher.ts @@ -8,6 +8,7 @@ import { IWorkspaceService } from '../common/application/types'; import { IDisposableRegistry } from '../common/types'; import { TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardFileWatcher implements IExtensionSingleActivationService { @@ -24,6 +25,9 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts index cac29b1d7e7a..d6dc8d7e82e5 100644 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts @@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; @injectable() export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { @@ -27,6 +28,9 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } this.activateInternal().ignoreErrors(); } diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts index 1c03a696dc1d..d42101cb51d6 100644 --- a/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -84,7 +84,7 @@ export class TensorBoardPrompt { } } - private isPromptEnabled(): boolean { + public isPromptEnabled(): boolean { return this.state.value; } diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts index 41f630056741..c81059654075 100644 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ b/src/client/tensorBoard/tensorBoardSessionProvider.ts @@ -22,6 +22,7 @@ import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; import { TensorBoardSession } from './tensorBoardSession'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; @@ -58,6 +59,10 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer } public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } + this.disposables.push( this.commandManager.registerCommand( Commands.LaunchTensorBoard, diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts index 99d82949dcfd..7c8ea7b00961 100644 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ b/src/client/tensorBoard/tensorBoardUsageTracker.ts @@ -12,6 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker'; import { TensorBoardEntrypointTrigger } from './constants'; import { containsTensorBoardImport } from './helpers'; import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; const testExecution = isTestExecution(); @@ -28,6 +29,9 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic ) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } if (testExecution) { await this.activateInternal(); } else { diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts new file mode 100644 index 000000000000..25eac8db71da --- /dev/null +++ b/src/client/tensorBoard/tensorboarExperiment.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +export function useNewTensorboardExtension(): boolean { + return !!extensions.getExtension('ms-toolsai.tensorboard'); +} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts index de6454716c9b..5c377e1d2455 100644 --- a/src/client/tensorBoard/tensorboardDependencyChecker.ts +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -17,10 +17,9 @@ import { IInterpreterService } from '../interpreter/contracts'; import { TensorBoardSession } from './tensorBoardSession'; import { disposeAll } from '../common/utils/resourceLifecycle'; import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; -import { ITensorboardDependencyChecker } from './types'; @injectable() -export class TensorboardDependencyChecker implements ITensorboardDependencyChecker { +export class TensorboardDependencyChecker { private preferredViewGroupMemento: IPersistentState; constructor( diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts index a68c6d18a4f0..74f69afab84f 100644 --- a/src/client/tensorBoard/tensorboardIntegration.ts +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -5,21 +5,27 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Extension, Uri } from 'vscode'; +import { Extension, Uri, commands } from 'vscode'; import { IWorkspaceService } from '../common/application/types'; import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; -import { IExtensions, Resource } from '../common/types'; +import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; -import { PythonEnvironment } from '../pythonEnvironments/info'; -import { ITensorboardDependencyChecker } from './types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; type PythonApiForTensorboardExtension = { - getActivatedEnvironmentVariables( - resource: Resource, - interpreter?: PythonEnvironment, - allowExceptions?: boolean, - ): Promise; + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; }; type TensorboardExtensionApi = { @@ -37,26 +43,39 @@ export class TensorboardExtensionIntegration { @inject(IExtensions) private readonly extensions: IExtensions, @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(ITensorboardDependencyChecker) private readonly dependencyChcker: ITensorboardDependencyChecker, - ) {} + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + ) { + this.hideCommands(); + extensions.onDidChange(this.hideCommands, this, disposables); + } public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + this.hideCommands(); if (!this.workspaceService.isTrusted) { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); return undefined; } tensorboardExtensionApi.registerPythonApi({ - getActivatedEnvironmentVariables: async ( - resource: Resource, - interpreter?: PythonEnvironment, - allowExceptions?: boolean, - ) => this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions), + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), ensureDependenciesAreInstalled: async (resource?: Uri): Promise => this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), }); return undefined; } + public hideCommands(): void { + if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { + console.error('TensorBoard extension is installed'); + void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); + } else { + console.error('TensorBoard extension not installed'); + } + } + public async integrateWithTensorboardExtension(): Promise { const api = await this.getExtensionApi(); if (api) { diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts index 5aadc12dc4c0..30ccf7e1726a 100644 --- a/src/client/tensorBoard/terminalWatcher.ts +++ b/src/client/tensorBoard/terminalWatcher.ts @@ -4,6 +4,7 @@ import { IExtensionSingleActivationService } from '../activation/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { useNewTensorboardExtension } from './tensorboarExperiment'; // Every 5 min look, through active terminals to see if any are running `tensorboard` @injectable() @@ -15,6 +16,9 @@ export class TerminalWatcher implements IExtensionSingleActivationService, IDisp constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {} public async activate(): Promise { + if (useNewTensorboardExtension()) { + return; + } const handle = setInterval(() => { // When user runs a command in VSCode terminal, the terminal's name // becomes the program that is currently running. Since tensorboard From efdb9fb0ee56340005970f6723b7c1c7dfb3138e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Oct 2023 14:11:27 +1100 Subject: [PATCH 4/4] Fix tests --- src/client/tensorBoard/tensorBoardSession.ts | 4 +- .../tensorBoardUsageTracker.unit.test.ts | 7 +++ src/test/vscode-mock.ts | 43 ++++++++----------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 3bbf55b89c21..fb54ad6f32e6 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -101,8 +101,8 @@ export class TensorBoardSession { private readonly multiStepFactory: IMultiStepInputFactory, private readonly configurationService: IConfigurationService, ) { - this.disposables.push(this.onDidChangeViewStateEventEmitter) - this.disposables.push(this.onDidDisposeEventEmitter) + this.disposables.push(this.onDidChangeViewStateEventEmitter); + this.disposables.push(this.onDidDisposeEventEmitter); } public get onDidDispose(): Event { diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts index b6efad083a57..ff187dd2afc1 100644 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts @@ -1,9 +1,11 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; +import { anything, reset, when } from 'ts-mockito'; import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; import { MockDocumentManager } from '../mocks/mockDocumentManager'; import { createTensorBoardPromptWithMocks } from './helpers'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('TensorBoard usage tracker', () => { let documentManager: MockDocumentManager; @@ -11,6 +13,11 @@ suite('TensorBoard usage tracker', () => { let prompt: TensorBoardPrompt; let showNativeTensorBoardPrompt: sinon.SinonSpy; + suiteSetup(() => { + reset(mockedVSCodeNamespaces.extensions); + when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); + }); + suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); setup(() => { documentManager = new MockDocumentManager(); prompt = createTensorBoardPromptWithMocks(); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 44518e7575a7..ec44d302d063 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,21 +3,21 @@ 'use strict'; -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +import { anything, instance, mock, when } from 'ts-mockito'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock(name: K): void { - const mockedObj = TypeMoq.Mock.ofType(); - (mockedVSCode as any)[name] = mockedObj.object; + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } @@ -35,15 +35,26 @@ export function initialize() { generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); - generateNotebookMocks(); + generateMock('notebooks'); // Use mock clipboard fo testing purposes. const clipboard = new MockClipboard(); - mockedVSCodeNamespaces.env?.setup((e) => e.clipboard).returns(() => clipboard); - mockedVSCodeNamespaces.env?.setup((e) => e.appName).returns(() => 'Insider'); + when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); + when(mockedVSCodeNamespaces.env!.appName).thenReturn('Insider'); + + // This API is used in src/client/telemetry/telemetry.ts + const extension = mock>(); + const packageJson = mock(); + const contributes = mock(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function (request: any, _parent: any) { @@ -122,21 +133,3 @@ mockedVSCode.LogLevel = vscodeMocks.LogLevel; (mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; (mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; (mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; - -// This API is used in src/client/telemetry/telemetry.ts -const extensions = TypeMoq.Mock.ofType(); -extensions.setup((e) => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType>(); -const packageJson = TypeMoq.Mock.ofType(); -const contributes = TypeMoq.Mock.ofType(); -extension.setup((e) => e.packageJSON).returns(() => packageJson.object); -packageJson.setup((p) => p.contributes).returns(() => contributes.object); -contributes.setup((p) => p.debuggers).returns(() => [{ aiKey: '' }]); -extensions.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); -mockedVSCode.extensions = extensions.object; - -function generateNotebookMocks() { - const mockedObj = TypeMoq.Mock.ofType<{}>(); - (mockedVSCode as any).notebook = mockedObj.object; - (mockedVSCodeNamespaces as any).notebook = mockedObj as any; -}