From 2873f5664fecfde0c05305792829f9fca823911e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 17 May 2023 22:27:23 +1000 Subject: [PATCH 1/4] Wip --- src/extension.node.ts | 2 + src/extension.web.ts | 2 + src/kernels/common/commonFinder.ts | 24 -- .../jupyter/connection/jupyterConnection.ts | 2 +- .../connection/jupyterConnection.unit.test.ts | 4 +- .../jupyter/connection/serverSelector.ts | 339 +---------------- .../jupyter/connection/serverUriStorage.ts | 145 +------- .../jupyter/finder/remoteKernelFinder.ts | 6 +- .../finder/remoteKernelFinder.unit.test.ts | 3 - .../finder/remoteKernelFinderController.ts | 6 - .../session/serverSelector.unit.test.ts | 341 ------------------ src/kernels/jupyter/types.ts | 7 - .../contributedKerneFinder.node.unit.test.ts | 3 +- .../finder/localKernelSpecFinderBase.node.ts | 18 +- .../controllers/controllerRegistration.ts | 2 +- .../controllerRegistration.unit.test.ts | 7 - .../remoteKernelControllerWatcher.ts | 6 +- src/platform/common/cache.ts | 27 ++ .../userServerUrlProvider.ts | 6 - .../notebook/controllerDefaultService.ts | 9 +- .../notebook/controllerPreferredService.ts | 13 +- src/test/datascience/notebook/helper.ts | 10 +- .../notebook/kernelSelection.vscode.test.ts | 1 - .../remoteNotebookEditor.vscode.test.ts | 4 - 24 files changed, 73 insertions(+), 914 deletions(-) delete mode 100644 src/kernels/jupyter/session/serverSelector.unit.test.ts create mode 100644 src/platform/common/cache.ts diff --git a/src/extension.node.ts b/src/extension.node.ts index ae5630af947..823e092bc9e 100644 --- a/src/extension.node.ts +++ b/src/extension.node.ts @@ -95,6 +95,7 @@ import { IInterpreterPackages } from './platform/interpreter/types'; import { homedir, platform, arch, userInfo } from 'os'; import { getUserHomeDir } from './platform/common/utils/platform.node'; import { homePath } from './platform/common/platform/fs-paths.node'; +import { removeOldCachedItems } from './platform/common/cache'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -110,6 +111,7 @@ let activatedServiceContainer: IServiceContainer | undefined; export async function activate(context: IExtensionContext): Promise { context.subscriptions.push({ dispose: () => (Exiting.isExiting = true) }); try { + removeOldCachedItems(context.globalState).then(noop, noop); let api: IExtensionApi; let ready: Promise; let serviceContainer: IServiceContainer; diff --git a/src/extension.web.ts b/src/extension.web.ts index b1343d6a781..8b59a5b5b2f 100644 --- a/src/extension.web.ts +++ b/src/extension.web.ts @@ -100,6 +100,7 @@ import { ServiceManager } from './platform/ioc/serviceManager'; import { OutputChannelLogger } from './platform/logging/outputChannelLogger'; import { ConsoleLogger } from './platform/logging/consoleLogger'; import { initializeGlobals as initializeTelemetryGlobals } from './platform/telemetry/telemetry'; +import { removeOldCachedItems } from './platform/common/cache'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -115,6 +116,7 @@ let activatedServiceContainer: IServiceContainer | undefined; export async function activate(context: IExtensionContext): Promise { context.subscriptions.push({ dispose: () => (Exiting.isExiting = true) }); try { + removeOldCachedItems(context.globalState).then(noop, noop); let api: IExtensionApi; let ready: Promise; let serviceContainer: IServiceContainer; diff --git a/src/kernels/common/commonFinder.ts b/src/kernels/common/commonFinder.ts index 11f284c58c3..29018f0b3ca 100644 --- a/src/kernels/common/commonFinder.ts +++ b/src/kernels/common/commonFinder.ts @@ -1,29 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Memento } from 'vscode'; -import { noop } from '../../platform/common/utils/misc'; - // Two cache keys so we can get local and remote separately export const RemoteKernelSpecsCacheKey = 'JUPYTER_REMOTE_KERNELSPECS_V4'; - -export async function removeOldCachedItems(globalState: Memento): Promise { - await Promise.all( - [ - 'JUPYTER_LOCAL_KERNELSPECS', - 'JUPYTER_LOCAL_KERNELSPECS_V1', - 'JUPYTER_LOCAL_KERNELSPECS_V2', - 'JUPYTER_LOCAL_KERNELSPECS_V3', - 'JUPYTER_REMOTE_KERNELSPECS', - 'JUPYTER_REMOTE_KERNELSPECS_V1', - 'JUPYTER_REMOTE_KERNELSPECS_V2', - 'JUPYTER_REMOTE_KERNELSPECS_V3', - 'JUPYTER_LOCAL_KERNELSPECS_V4', - 'LOCAL_KERNEL_SPECS_CACHE_KEY_V_2022_10', - 'LOCAL_KERNEL_PYTHON_AND_RELATED_SPECS_CACHE_KEY_V_2022_10' - ] - .filter((key) => RemoteKernelSpecsCacheKey !== key) // Exclude latest cache key - .filter((key) => globalState.get(key, undefined) !== undefined) - .map((key) => globalState.update(key, undefined).then(noop, noop)) - ); -} diff --git a/src/kernels/jupyter/connection/jupyterConnection.ts b/src/kernels/jupyter/connection/jupyterConnection.ts index 339cc545e62..6522ddbdddc 100644 --- a/src/kernels/jupyter/connection/jupyterConnection.ts +++ b/src/kernels/jupyter/connection/jupyterConnection.ts @@ -41,7 +41,7 @@ export class JupyterConnection implements IExtensionSyncActivationService { disposables.push(this); } public activate() { - this.serverUriStorage.onDidChangeConnectionType( + this.serverUriStorage.onDidChangeUri( () => // When server URI changes, clear our pending URI timeouts this.clearTimeouts(), diff --git a/src/kernels/jupyter/connection/jupyterConnection.unit.test.ts b/src/kernels/jupyter/connection/jupyterConnection.unit.test.ts index 7cd97283983..105f8af217d 100644 --- a/src/kernels/jupyter/connection/jupyterConnection.unit.test.ts +++ b/src/kernels/jupyter/connection/jupyterConnection.unit.test.ts @@ -56,7 +56,7 @@ suite('Jupyter Connection', async () => { const serverConnectionChangeEvent = new EventEmitter(); disposables.push(serverConnectionChangeEvent); - when(serverUriStorage.onDidChangeConnectionType).thenReturn(serverConnectionChangeEvent.event); + when(serverUriStorage.onDidChangeUri).thenReturn(serverConnectionChangeEvent.event); jupyterConnection.activate(); }); @@ -65,7 +65,7 @@ suite('Jupyter Connection', async () => { }); test('Ensure event handler is added', () => { - verify(serverUriStorage.onDidChangeConnectionType).once(); + verify(serverUriStorage.onDidChangeUri).once(); }); test('Validation will result in fetching kernels and kernelspecs', async () => { const uri = 'http://localhost:8888/?token=1234'; diff --git a/src/kernels/jupyter/connection/serverSelector.ts b/src/kernels/jupyter/connection/serverSelector.ts index 83c523a003f..b8f70b48c25 100644 --- a/src/kernels/jupyter/connection/serverSelector.ts +++ b/src/kernels/jupyter/connection/serverSelector.ts @@ -4,56 +4,21 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import { inject, injectable } from 'inversify'; -import { EventEmitter, QuickPickItem, ThemeIcon, Uri } from 'vscode'; -import { IApplicationShell, IClipboard, IWorkspaceService } from '../../../platform/common/application/types'; -import { traceDecoratorError, traceError, traceWarning } from '../../../platform/logging'; +import { IApplicationShell, IWorkspaceService } from '../../../platform/common/application/types'; +import { traceWarning } from '../../../platform/logging'; import { DataScience } from '../../../platform/common/utils/localize'; -import { - IMultiStepInputFactory, - IMultiStepInput, - InputStep, - IQuickPickParameters, - InputFlowAction -} from '../../../platform/common/utils/multiStepInput'; -import { capturePerfTelemetry, sendTelemetryEvent } from '../../../telemetry'; +import { sendTelemetryEvent } from '../../../telemetry'; import { Telemetry } from '../../../telemetry'; -import { - IJupyterUriProvider, - IJupyterUriProviderRegistration, - IJupyterServerUriStorage, - JupyterServerUriHandle, - IJupyterServerUriEntry -} from '../types'; +import { IJupyterServerUriStorage } from '../types'; import { IDataScienceErrorHandler } from '../../errors/types'; -import { IConfigurationService, IDisposableRegistry, IsWebExtension } from '../../../platform/common/types'; -import { - handleExpiredCertsError, - handleSelfCertsError, - computeServerId, - generateUriFromRemoteProvider -} from '../jupyterUtils'; +import { IConfigurationService, IDisposableRegistry } from '../../../platform/common/types'; +import { handleExpiredCertsError, handleSelfCertsError, computeServerId } from '../jupyterUtils'; import { JupyterConnection } from './jupyterConnection'; import { JupyterSelfCertsError } from '../../../platform/errors/jupyterSelfCertsError'; import { RemoteJupyterServerConnectionError } from '../../../platform/errors/remoteJupyterServerConnectionError'; import { JupyterSelfCertsExpiredError } from '../../../platform/errors/jupyterSelfCertsExpiredError'; import { JupyterInvalidPasswordError } from '../../errors/jupyterInvalidPassword'; -interface ISelectUriQuickPickItem extends QuickPickItem { - newChoice?: boolean; - provider?: IJupyterUriProvider; - url?: string; -} - -interface IJupyterServerSelector { - selectJupyterURI( - commandSource: SelectJupyterUriCommandSource, - existingMultiStep?: IMultiStepInput<{}> - ): Promise | void>; - - setJupyterURIToLocal(): Promise; - setJupyterURIToRemote(userURI: string | undefined, ignoreValidation?: boolean, displayName?: string): Promise; -} - export type SelectJupyterUriCommandSource = | 'nonUser' | 'toolbar' @@ -119,96 +84,17 @@ export async function validateSelectJupyterURI( */ @injectable() export class JupyterServerSelector { - private impl: IJupyterServerSelector; constructor( - @inject(IClipboard) private readonly clipboard: IClipboard, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IJupyterUriProviderRegistration) - private readonly extraUriProviders: IJupyterUriProviderRegistration, @inject(IJupyterServerUriStorage) private readonly serverUriStorage: IJupyterServerUriStorage, @inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, @inject(IConfigurationService) private readonly configService: IConfigurationService, @inject(JupyterConnection) private readonly jupyterConnection: JupyterConnection, - @inject(IsWebExtension) private readonly isWebExtension: boolean, @inject(IWorkspaceService) readonly workspaceService: IWorkspaceService, @inject(IDisposableRegistry) readonly disposableRegistry: IDisposableRegistry - ) { - this.createImpl(); - } - - public selectJupyterURI( - commandSource: SelectJupyterUriCommandSource = 'nonUser', - existingMultiStep?: IMultiStepInput<{}> - ): Promise | void> { - return this.impl.selectJupyterURI(commandSource, existingMultiStep); - } - - public setJupyterURIToLocal(): Promise { - return this.impl.setJupyterURIToLocal(); - } - - public setJupyterURIToRemote( - userURI: string | undefined, - ignoreValidation?: boolean, - displayName?: string - ): Promise { - return this.impl.setJupyterURIToRemote(userURI, ignoreValidation, displayName); - } - - private createImpl() { - this.impl = new JupyterServerSelector_Insiders( - this.clipboard, - this.multiStepFactory, - this.extraUriProviders, - this.serverUriStorage, - this.errorHandler, - this.applicationShell, - this.configService, - this.jupyterConnection, - this.isWebExtension - ); - } -} - -/** - * Inisders version of the JupyterServerSelector. - */ -class JupyterServerSelector_Insiders implements IJupyterServerSelector { - private readonly localLabel = `$(zap) ${DataScience.jupyterSelectURINoneLabel}`; - private readonly newLabel = `$(server) ${DataScience.jupyterSelectURINewLabel}`; - private readonly remoteLabel = `$(server) ${DataScience.jupyterSelectURIRemoteLabel}`; - constructor( - private readonly clipboard: IClipboard, - private readonly multiStepFactory: IMultiStepInputFactory, - private extraUriProviders: IJupyterUriProviderRegistration, - private readonly serverUriStorage: IJupyterServerUriStorage, - private readonly errorHandler: IDataScienceErrorHandler, - private readonly applicationShell: IApplicationShell, - private readonly configService: IConfigurationService, - @inject(JupyterConnection) private readonly jupyterConnection: JupyterConnection, - @inject(IsWebExtension) private readonly isWebExtension: boolean ) {} - @capturePerfTelemetry(Telemetry.SelectJupyterURI) - @traceDecoratorError('Failed to select Jupyter Uri') - public selectJupyterURI( - commandSource: SelectJupyterUriCommandSource = 'nonUser' - ): Promise | void> { - const allowLocal = commandSource !== 'nonUser'; - sendTelemetryEvent(Telemetry.SetJupyterURIUIDisplayed, undefined, { - commandSource - }); - const multiStep = this.multiStepFactory.create<{}>(); - return multiStep.run(this.startSelectingURI.bind(this, allowLocal), {}); - } - - @capturePerfTelemetry(Telemetry.SetJupyterURIToLocal) - public async setJupyterURIToLocal(): Promise { - await this.serverUriStorage.setUriToLocal(); - } - public async setJupyterURIToRemote( userURI: string, ignoreValidation?: boolean, @@ -250,217 +136,4 @@ class JupyterServerSelector_Insiders implements IJupyterServerSelector { azure: userURI.toLowerCase().includes('azure') }); } - - private async startSelectingURI( - allowLocal: boolean, - input: IMultiStepInput<{}>, - _state: {} - ): Promise | void> { - // First step, show a quick pick to choose either the remote or the local. - // newChoice element will be set if the user picked 'enter a new server' - - // Get the list of items and show what the current value is - const remoteUri = await this.serverUriStorage.getRemoteUri(); - // filter out the builtin providers which are only now used in the MRU quick pick. - const items = (await this.getUriPickList(allowLocal, remoteUri)).filter( - (item) => !item.provider?.id.startsWith('_builtin') - ); - const activeItem = items.find((i) => i.url === remoteUri || (i.label === this.localLabel && !remoteUri)); - const currentValue = !remoteUri ? DataScience.jupyterSelectURINoneLabel : activeItem?.label; - const placeholder = currentValue // This will show at the top (current value really) - ? DataScience.jupyterSelectURIQuickPickCurrent(currentValue) - : DataScience.jupyterSelectURIQuickPickPlaceholder; - - let pendingUpdatesToUri = Promise.resolve(); - const onDidChangeItems = new EventEmitter(); - const item = await input.showQuickPick>({ - placeholder, - items, - activeItem, - title: allowLocal - ? DataScience.jupyterSelectURIQuickPickTitleOld - : DataScience.jupyterSelectURIQuickPickTitleRemoteOnly, - onDidTriggerItemButton: (e) => { - const url = e.item.url; - if (url && e.button.tooltip === DataScience.removeRemoteJupyterServerEntryInQuickPick) { - pendingUpdatesToUri = pendingUpdatesToUri.then(() => - this.serverUriStorage.removeUri(url).catch((ex) => traceError('Failed to update Uri list', ex)) - ); - items.splice(items.indexOf(e.item), 1); - onDidChangeItems.fire(items.concat([])); - } - }, - onDidChangeItems: onDidChangeItems.event - }); - await pendingUpdatesToUri.catch((ex) => traceError('Failed to update Uri list', ex)); - if (item.label === this.localLabel) { - await this.setJupyterURIToLocal(); - } else if (!item.newChoice && !item.provider) { - await this.setJupyterURIToRemote(item.url || item.label, false, item.label); - } else if (!item.provider) { - return this.selectRemoteURI.bind(this); - } else { - return this.selectProviderURI.bind(this, item.provider, item); - } - } - - private async selectProviderURI( - provider: IJupyterUriProvider, - item: ISelectUriQuickPickItem, - _input: IMultiStepInput<{}>, - _state: {} - ): Promise | void> { - if (!provider.handleQuickPick) { - return; - } - const result = await provider.handleQuickPick(item, true); - if (result === 'back') { - throw InputFlowAction.back; - } - if (result) { - await this.handleProviderQuickPick(provider.id, result); - } - } - private async handleProviderQuickPick(id: string, result: JupyterServerUriHandle | undefined) { - if (result) { - const uri = generateUriFromRemoteProvider(id, result); - await this.setJupyterURIToRemote(uri); - } - } - private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { - let initialValue = ''; - try { - const text = await this.clipboard.readText().catch(() => ''); - const parsedUri = Uri.parse(text.trim(), true); - // Only display http/https uris. - initialValue = text && parsedUri && parsedUri.scheme.toLowerCase().startsWith('http') ? text : ''; - } catch { - // We can ignore errors. - } - // Ask the user to enter a URI to connect to. - const uri = await input.showInputBox({ - title: DataScience.jupyterSelectURIPrompt, - value: initialValue || '', - validate: this.validateSelectJupyterURI, - prompt: '' - }); - - // Offer the user a change to pick a display name for the server - // Leaving it blank will use the URI as the display name - const newDisplayName = await this.applicationShell.showInputBox({ - title: DataScience.jupyterRenameServer - }); - - if (uri) { - await this.setJupyterURIToRemote(uri, true, newDisplayName || uri); - } - } - - public validateSelectJupyterURI = async (inputText: string): Promise => { - inputText = inputText.trim(); - try { - new URL(inputText); - } catch { - return DataScience.jupyterSelectURIInvalidURI; - } - - // Double check http - if (!inputText.toLowerCase().startsWith('http')) { - return DataScience.validationErrorMessageForRemoteUrlProtocolNeedsToBeHttpOrHttps; - } - // Double check this server can be connected to. Might need a password, might need a allowUnauthorized - try { - await this.jupyterConnection.validateRemoteUri(inputText); - } catch (err) { - traceWarning('Uri verification error', err); - if (JupyterSelfCertsError.isSelfCertsError(err)) { - sendTelemetryEvent(Telemetry.ConnectRemoteSelfCertFailedJupyter); - const handled = await handleSelfCertsError(this.applicationShell, this.configService, err.message); - if (!handled) { - return DataScience.jupyterSelfCertFailErrorMessageOnly; - } - } else if (JupyterSelfCertsExpiredError.isSelfCertsExpiredError(err)) { - sendTelemetryEvent(Telemetry.ConnectRemoteSelfCertFailedJupyter); - const handled = await handleExpiredCertsError(this.applicationShell, this.configService, err.message); - if (!handled) { - return DataScience.jupyterSelfCertExpiredErrorMessageOnly; - } - } else { - // Return the general connection error to show in the validation box - // Replace any Urls in the error message with markdown link. - const urlRegex = /(https?:\/\/[^\s]+)/g; - const errorMessage = (err.message || err.toString()).replace( - urlRegex, - (url: string) => `[${url}](${url})` - ); - return ( - this.isWebExtension || true - ? DataScience.remoteJupyterConnectionFailedWithoutServerWithErrorWeb - : DataScience.remoteJupyterConnectionFailedWithoutServerWithError - )(errorMessage); - } - } - }; - - private async getUriPickList( - allowLocal: boolean, - currentRemoteUri?: IJupyterServerUriEntry - ): Promise { - // Ask our providers to stick on items - let providerItems: ISelectUriQuickPickItem[] = []; - const providers = await this.extraUriProviders.getProviders(); - if (providers) { - for (const p of providers) { - if (p.getQuickPickEntryItems && p.handleQuickPick) { - const items = await p.getQuickPickEntryItems(); - const newProviderItems = items.map((i) => { - return { ...i, newChoice: false, provider: p }; - }); - providerItems = providerItems.concat(newProviderItems); - } - } - } - - // Always have 'local' and 'add new' - let items: ISelectUriQuickPickItem[] = []; - if (allowLocal) { - items.push({ label: this.localLabel, detail: DataScience.jupyterSelectURINoneDetail, newChoice: false }); - items = items.concat(providerItems); - items.push({ label: this.newLabel, detail: DataScience.jupyterSelectURINewDetail, newChoice: true }); - } else { - items = items.concat(providerItems); - items.push({ - label: this.remoteLabel, - detail: DataScience.jupyterSelectURIRemoteDetail, - newChoice: true - }); - } - - // Get our list of recent server connections and display that as well - const savedURIList = await this.serverUriStorage.getSavedUriList(); - savedURIList.forEach((uriItem) => { - if (uriItem.uri && uriItem.isValidated) { - const uriDate = new Date(uriItem.time); - const isSelected = currentRemoteUri?.uri === uriItem.uri; - items.push({ - label: uriItem.displayName || uriItem.uri, - detail: DataScience.jupyterSelectURIMRUDetail(uriDate), - // If our display name is not the same as the URI, render the uri as description - description: uriItem.displayName !== uriItem.uri ? uriItem.uri : undefined, - newChoice: false, - url: uriItem.uri, - buttons: isSelected - ? [] // Cannot delete the current Uri (you can only switch to local). - : [ - { - iconPath: new ThemeIcon('trash'), - tooltip: DataScience.removeRemoteJupyterServerEntryInQuickPick - } - ] - }); - } - }); - - return items; - } } diff --git a/src/kernels/jupyter/connection/serverUriStorage.ts b/src/kernels/jupyter/connection/serverUriStorage.ts index 1d160c67826..f6058ce59a4 100644 --- a/src/kernels/jupyter/connection/serverUriStorage.ts +++ b/src/kernels/jupyter/connection/serverUriStorage.ts @@ -17,23 +17,16 @@ import { IsWebExtension, IConfigurationService } from '../../../platform/common/types'; -import { noop } from '../../../platform/common/utils/misc'; -import { traceError, traceInfoIfCI, traceVerbose } from '../../../platform/logging'; +import { traceInfoIfCI, traceVerbose } from '../../../platform/logging'; import { computeServerId, extractJupyterServerHandleAndId } from '../jupyterUtils'; import { IJupyterServerUriEntry, IJupyterServerUriStorage, IJupyterUriProviderRegistration } from '../types'; -export const mementoKeyToIndicateIfConnectingToLocalKernelsOnly = 'connectToLocalKernelsOnly'; -export const currentServerHashKey = 'currentServerHash'; - /** * Class for storing Jupyter Server URI values */ @injectable() export class JupyterServerUriStorage implements IJupyterServerUriStorage { private lastSavedList?: Promise; - private currentUriPromise: Promise | undefined; - private _currentServerId: string | undefined; - private _localOnly: boolean = false; private _onDidChangeUri = new EventEmitter(); public get onDidChangeUri() { return this._onDidChangeUri.event; @@ -46,15 +39,9 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { public get onDidAddUri() { return this._onDidAddUri.event; } - public get currentServerId(): string | undefined { - return this._currentServerId; - } public get onDidChangeConnectionType(): Event { return this._onDidChangeUri.event; } - public get isLocalLaunch(): boolean { - return this._localOnly; - } constructor( @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICryptoUtils) private readonly crypto: ICryptoUtils, @@ -65,23 +52,7 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { @inject(IConfigurationService) readonly configService: IConfigurationService, @inject(IJupyterUriProviderRegistration) private readonly jupyterPickerRegistration: IJupyterUriProviderRegistration - ) { - // Remember if local only - traceInfoIfCI(`JupyterServerUriStorage: isWebExtension: ${isWebExtension}`); - traceInfoIfCI( - `Global memento: ${this.globalMemento.get( - mementoKeyToIndicateIfConnectingToLocalKernelsOnly, - true - )}` - ); - this._localOnly = isWebExtension - ? false - : this.globalMemento.get(mementoKeyToIndicateIfConnectingToLocalKernelsOnly, true); - this._currentServerId = this.globalMemento.get(currentServerHashKey, undefined); - - // Cache our current state so we don't keep asking for it from the encrypted storage - this.getUri().catch(noop); - } + ) {} public async addServerToUriList(serverId: string, time: number) { // Start with saved list. const uriList = await this.getSavedUriList(); @@ -97,7 +68,7 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { await this.addToUriList(existingEntry.uri, time, existingEntry.displayName || ''); } - public async addToUriList(uri: string, time: number, displayName: string) { + private async addToUriList(uri: string, time: number, displayName: string) { // Uri list is saved partially in the global memento and partially in encrypted storage // Start with saved list. @@ -123,28 +94,17 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { const entry = { uri, time, serverId, displayName: displayName || uri, isValidated: true }; editedList.push(entry); - if (this.currentUriPromise) { - const currentUri = await this.currentUriPromise; - if (currentUri && currentUri.uri === uri) { - this.currentUriPromise = Promise.resolve(entry); - } - } - // Signal that we added in the entry this._onDidAddUri.fire(entry); return this.updateMemento(editedList); } public async removeUri(uri: string) { - const activeUri = await this.getUri(); // Start with saved list. const uriList = await this.getSavedUriList(); const editedList = uriList.filter((f) => f.uri !== uri); await this.updateMemento(editedList); - if (activeUri?.uri === uri) { - await this.setUriToLocal(); - } const removedItem = uriList.find((f) => f.uri === uri); if (removedItem) { this._onDidRemoveUris.fire([removedItem]); @@ -220,7 +180,7 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { }; if (uri === Settings.JupyterServerLocalLaunch) { - return server; + return; } try { @@ -247,7 +207,7 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { ); traceVerbose(`Found ${result.length} saved URIs, ${JSON.stringify(result)}`); - return result; + return result.filter((item) => !!item) as IJupyterServerUriEntry[]; } } return []; @@ -274,105 +234,22 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage { }) ); } - public getUri(): Promise { - if (!this.currentUriPromise) { - this.currentUriPromise = this.getUriInternal(); - } - - return this.currentUriPromise; - } - public async getRemoteUri(): Promise { - try { - const uri = await this.getUri(); - traceInfoIfCI(`getRemoteUri: ${uri?.uri}`); - if (uri?.uri === Settings.JupyterServerLocalLaunch) { - return; - } - return uri; - } catch (e) { - traceError(`Exception getting uri: ${e}`); - return; - } - } public async getUriForServer(id: string): Promise { const savedList = await this.getSavedUriList(); const uriItem = savedList.find((item) => item.serverId === id); return uriItem; } - public async setUriToLocal(): Promise { - traceInfoIfCI(`setUriToLocal`); - await this.setUri(Settings.JupyterServerLocalLaunch, undefined); - } public async setUriToRemote(uri: string, displayName: string): Promise { - // Make sure to add to the saved list before we set the uri. Otherwise - // handlers for the URI changing will use the saved list to make sure the - // server id matches - await this.addToUriList(uri, Date.now(), displayName); - await this.setUri(uri, displayName); - } - - public async setUriToNone(): Promise { - traceInfoIfCI(`setUriToNone`); - return this.setUri(undefined, undefined); - } - - public async setUri(uri: string | undefined, displayName: string | undefined) { - // Set the URI as our current state - this._currentServerId = uri ? await computeServerId(uri) : undefined; - const entry: IJupyterServerUriEntry | undefined = - uri && this._currentServerId - ? { - uri, - time: Date.now(), - serverId: this._currentServerId, - displayName: displayName || uri, - isValidated: true - } - : undefined; - - this.currentUriPromise = Promise.resolve(entry); traceInfoIfCI(`setUri: ${uri}`); - this._localOnly = (uri === Settings.JupyterServerLocalLaunch || uri === undefined) && !this.isWebExtension; - this._onDidChangeUri.fire(); // Needs to happen as soon as we change so that dependencies update synchronously - - // No update the async parts - await this.globalMemento.update(mementoKeyToIndicateIfConnectingToLocalKernelsOnly, this._localOnly); - await this.globalMemento.update(currentServerHashKey, this._currentServerId); - - if (!this._localOnly && uri) { - // disaplay name is wrong here - await this.addToUriList(uri, Date.now(), displayName ?? uri); - // Save in the storage (unique account per workspace) - const key = await this.getUriAccountKey(); - await this.encryptedStorage.store(Settings.JupyterServerRemoteLaunchService, key, uri); - } - } - private async getUriInternal(): Promise { - const savedList = await this.getSavedUriList(); - if (this.isLocalLaunch) { - return ( - savedList.find((item) => item.uri === Settings.JupyterServerLocalLaunch) ?? { - uri: Settings.JupyterServerLocalLaunch, - time: Date.now(), - serverId: '', - displayName: 'local', - isValidated: true - } - ); - } else { - // Should be stored in encrypted storage based on the workspace - const key = await this.getUriAccountKey(); - const storedUri = await this.encryptedStorage.retrieve(Settings.JupyterServerRemoteLaunchService, key); - - // Update server id if not already set - if (!this._currentServerId && storedUri) { - this._currentServerId = await computeServerId(storedUri); - } + // display name is wrong here + await this.addToUriList(uri, Date.now(), displayName ?? uri); + this._onDidChangeUri.fire(); // Needs to happen as soon as we change so that dependencies update synchronously - return savedList.find((item) => item.serverId === this._currentServerId); - } + // Save in the storage (unique account per workspace) + const key = await this.getUriAccountKey(); + await this.encryptedStorage.store(Settings.JupyterServerRemoteLaunchService, key, uri); } /** diff --git a/src/kernels/jupyter/finder/remoteKernelFinder.ts b/src/kernels/jupyter/finder/remoteKernelFinder.ts index b3045841524..2355d4feb63 100644 --- a/src/kernels/jupyter/finder/remoteKernelFinder.ts +++ b/src/kernels/jupyter/finder/remoteKernelFinder.ts @@ -30,7 +30,6 @@ import { isArray } from '../../../platform/common/utils/sysTypes'; import { areObjectsWithUrisTheSame, noop } from '../../../platform/common/utils/misc'; import { IApplicationEnvironment } from '../../../platform/common/application/types'; import { KernelFinder } from '../../kernelFinder'; -import { removeOldCachedItems } from '../../common/commonFinder'; import { ContributedKernelFinderKind } from '../../internalTypes'; import { disposeAllDisposables } from '../../../platform/common/helpers'; import { PromiseMonitor } from '../../../platform/common/utils/promises'; @@ -422,10 +421,7 @@ export class RemoteKernelFinder implements IRemoteKernelFinder, IDisposable { const key = this.cacheKey; this.cache = values; const serialized = values.map((item) => item.toJSON()); - await Promise.all([ - removeOldCachedItems(this.globalState), - this.globalState.update(key, { kernels: serialized, extensionVersion: this.env.extensionVersion }) - ]); + await this.globalState.update(key, { kernels: serialized, extensionVersion: this.env.extensionVersion }); if (added.length || updated.length || removed.length) { this._onDidChangeKernels.fire({ added, updated, removed }); diff --git a/src/kernels/jupyter/finder/remoteKernelFinder.unit.test.ts b/src/kernels/jupyter/finder/remoteKernelFinder.unit.test.ts index 925777faec7..c051aefeb9c 100644 --- a/src/kernels/jupyter/finder/remoteKernelFinder.unit.test.ts +++ b/src/kernels/jupyter/finder/remoteKernelFinder.unit.test.ts @@ -136,9 +136,6 @@ suite(`Remote Kernel Finder`, () => { serverId: connInfo.baseUrl, isValidated: true }; - when(serverUriStorage.getUri()).thenResolve(serverEntry); - when(serverUriStorage.getRemoteUri()).thenResolve(serverEntry); - when(serverUriStorage.isLocalLaunch).thenReturn(false); const onDidChangeEvent = new EventEmitter(); disposables.push(onDidChangeEvent); when(serverUriStorage.onDidChangeConnectionType).thenReturn(onDidChangeEvent.event); diff --git a/src/kernels/jupyter/finder/remoteKernelFinderController.ts b/src/kernels/jupyter/finder/remoteKernelFinderController.ts index e698611db9f..a612bbd6e16 100644 --- a/src/kernels/jupyter/finder/remoteKernelFinderController.ts +++ b/src/kernels/jupyter/finder/remoteKernelFinderController.ts @@ -19,7 +19,6 @@ import { IExtensionSyncActivationService } from '../../../platform/activation/ty import { RemoteKernelFinder } from './remoteKernelFinder'; import { ContributedKernelFinderKind } from '../../internalTypes'; import { RemoteKernelSpecsCacheKey } from '../../common/commonFinder'; -import { Settings } from '../../../platform/common/constants'; import { JupyterConnection } from '../connection/jupyterConnection'; @injectable() @@ -64,11 +63,6 @@ export class RemoteKernelFinderController implements IExtensionSyncActivationSer return; } - if (serverUri.uri === Settings.JupyterServerLocalLaunch) { - // 'local' uri is not a remote server. - return; - } - if (!this.serverFinderMapping.has(serverUri.serverId)) { const finder = new RemoteKernelFinder( `${ContributedKernelFinderKind.Remote}-${serverUri.serverId}`, diff --git a/src/kernels/jupyter/session/serverSelector.unit.test.ts b/src/kernels/jupyter/session/serverSelector.unit.test.ts deleted file mode 100644 index e20c261abd4..00000000000 --- a/src/kernels/jupyter/session/serverSelector.unit.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { assert } from 'chai'; -import { anyString, anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; - -import * as os from 'os'; -import * as sinon from 'sinon'; -import { ConfigurationChangeEvent, EventEmitter, QuickPickItem } from 'vscode'; -import { DataScienceErrorHandler } from '../../../kernels/errors/kernelErrorHandler'; -import { JupyterConnection } from '../../../kernels/jupyter/connection/jupyterConnection'; -import { JupyterUriProviderRegistration } from '../../../kernels/jupyter/connection/jupyterUriProviderRegistration'; -import { JupyterServerSelector } from '../../../kernels/jupyter/connection/serverSelector'; -import { - JupyterServerUriStorage, - mementoKeyToIndicateIfConnectingToLocalKernelsOnly -} from '../../../kernels/jupyter/connection/serverUriStorage'; -import { ApplicationEnvironment } from '../../../platform/common/application/applicationEnvironment.node'; -import { ApplicationShell } from '../../../platform/common/application/applicationShell'; -import { ClipboardService } from '../../../platform/common/application/clipboard'; -import { IApplicationShell, IClipboard } from '../../../platform/common/application/types'; -import { WorkspaceService } from '../../../platform/common/application/workspace.node'; -import { JupyterSettings } from '../../../platform/common/configSettings'; -import { ConfigurationService } from '../../../platform/common/configuration/service.node'; -import { Settings } from '../../../platform/common/constants'; -import { CryptoUtils } from '../../../platform/common/crypto'; -import { disposeAllDisposables } from '../../../platform/common/helpers'; -import { IConfigurationService, IDisposable, IWatchableJupyterSettings } from '../../../platform/common/types'; -import { DataScience } from '../../../platform/common/utils/localize'; -import { noop } from '../../../platform/common/utils/misc'; -import { MultiStepInputFactory } from '../../../platform/common/utils/multiStepInput'; -import { MockEncryptedStorage } from '../../../test/datascience/mockEncryptedStorage'; -import { MockInputBox } from '../../../test/datascience/mockInputBox'; -import { MockQuickPick } from '../../../test/datascience/mockQuickPick'; -import { MockMemento } from '../../../test/mocks/mementos'; - -/* eslint-disable , @typescript-eslint/no-explicit-any */ -suite(`Jupyter Server URI Selector`, () => { - let quickPick: MockQuickPick | undefined; - let clipboard: IClipboard; - let connection: JupyterConnection; - let applicationShell: IApplicationShell; - let configService: IConfigurationService; - let settings: IWatchableJupyterSettings; - let onDidChangeSettings: sinon.SinonStub; - const disposables: IDisposable[] = []; - function createDataScienceObject( - quickPickSelection: string, - inputSelection: string, - hasFolders: boolean - ): { selector: JupyterServerSelector; storage: JupyterServerUriStorage } { - clipboard = mock(ClipboardService); - configService = mock(ConfigurationService); - applicationShell = mock(ApplicationShell); - const applicationEnv = mock(ApplicationEnvironment); - const workspaceService = mock(WorkspaceService); - const picker = mock(JupyterUriProviderRegistration); - const crypto = mock(CryptoUtils); - settings = mock(JupyterSettings); - when(crypto.createHash(anyString(), anyString())).thenCall((a1, _a2) => a1); - quickPick = new MockQuickPick(quickPickSelection); - const input = new MockInputBox(inputSelection); - when(applicationShell.createQuickPick()).thenReturn(quickPick!); - when(applicationShell.createInputBox()).thenReturn(input); - when(applicationEnv.machineId).thenReturn(os.hostname()); - const multiStepFactory = new MultiStepInputFactory(instance(applicationShell)); - when(workspaceService.getWorkspaceFolderIdentifier(anything())).thenReturn('1'); - when(workspaceService.hasWorkspaceFolders).thenReturn(hasFolders); - const configChangeEvent = new EventEmitter(); - when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); - const encryptedStorage = new MockEncryptedStorage(); - connection = mock(); - when(connection.createConnectionInfo(anything())).thenResolve({ displayName: '' } as any); - const handler = mock(DataScienceErrorHandler); - when(connection.validateRemoteUri(anything())).thenResolve(); - when(configService.updateSetting(anything(), anything(), anything(), anything())).thenResolve(); - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(configService.getSettings()).thenReturn(instance(settings)); - onDidChangeSettings = sinon.stub(); - when(settings.onDidChange).thenReturn(onDidChangeSettings); - const memento = new MockMemento(); - // local launch false - memento.update(mementoKeyToIndicateIfConnectingToLocalKernelsOnly, false).then(noop, noop); - const jupyterUriProviderRegistration = mock(JupyterUriProviderRegistration); - const storage = new JupyterServerUriStorage( - instance(workspaceService), - instance(crypto), - encryptedStorage, - instance(applicationEnv), - new MockMemento(), - false, - instance(configService), - instance(jupyterUriProviderRegistration) - ); - const selector = new JupyterServerSelector( - instance(clipboard), - multiStepFactory, - instance(picker), - storage, - instance(handler), - instance(applicationShell), - instance(configService), - instance(connection), - false, - instance(workspaceService), - disposables - ); - return { selector, storage }; - } - - teardown(() => { - sinon.restore(); - disposeAllDisposables(disposables); - }); - - suite('Original', () => { - test('Local pick server uri', async () => { - const { selector, storage } = createDataScienceObject('$(zap) Default', '', true); - await selector.selectJupyterURI('commandPalette'); - let value = await storage.getUri(); - assert.equal(value?.uri, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); - - // Try a second time. - await selector.selectJupyterURI('commandPalette'); - value = await storage.getUri(); - assert.equal(value?.uri, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); - - // Verify active items - assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); - }); - - test('Local pick server uri with no workspace', async () => { - const { selector, storage } = createDataScienceObject('$(zap) Default', '', false); - await selector.selectJupyterURI('commandPalette'); - let value = await storage.getUri(); - assert.equal(value?.uri, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); - - // Try a second time. - await selector.selectJupyterURI('commandPalette'); - value = await storage.getUri(); - assert.equal(value?.uri, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); - - // Verify active items - assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); - }); - - test('Quick pick MRU tests', async () => { - const { selector, storage } = createDataScienceObject('$(zap) Default', '', true); - console.log('Step1'); - await selector.selectJupyterURI('commandPalette'); - // Verify initial default items - assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); - - // Add in a new server - const serverA1 = { uri: 'ServerA', time: 1, date: new Date(1) }; - console.log('Step2'); - await storage.addToUriList(serverA1.uri, serverA1.time, serverA1.uri); - - console.log('Step3'); - await selector.selectJupyterURI('commandPalette'); - assert.equal(quickPick?.items.length, 3, 'Wrong number of items in the quick pick'); - quickPickCheck(quickPick?.items[2], serverA1); - - // Add in a second server, the newer server should be higher in the list due to newer time - const serverB1 = { uri: 'ServerB', time: 2, date: new Date(2) }; - console.log('Step4'); - await storage.addToUriList(serverB1.uri, serverB1.time, serverB1.uri); - console.log('Step5'); - await selector.selectJupyterURI('commandPalette'); - assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); - quickPickCheck(quickPick?.items[2], serverB1); - quickPickCheck(quickPick?.items[3], serverA1); - - // Reconnect to server A with a new time, it should now be higher in the list - const serverA3 = { uri: 'ServerA', time: 3, date: new Date(3) }; - console.log('Step6'); - await storage.addToUriList(serverA3.uri, serverA3.time, serverA3.uri); - console.log('Step7'); - await selector.selectJupyterURI('commandPalette'); - assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); - quickPickCheck(quickPick?.items[3], serverB1); - quickPickCheck(quickPick?.items[2], serverA1); - - // Verify that we stick to our settings limit - for (let i = 0; i < Settings.JupyterServerUriListMax + 10; i = i + 1) { - console.log(`Step8 ${i} of ${Settings.JupyterServerUriListMax + 10}`); - await storage.addToUriList(i.toString(), i, i.toString()); - } - - console.log('Step9'); - await selector.selectJupyterURI('commandPalette'); - // Need a plus 2 here for the two default items - assert.equal( - quickPick?.items.length, - Settings.JupyterServerUriListMax + 2, - 'Wrong number of items in the quick pick' - ); - }); - - function quickPickCheck(item: QuickPickItem | undefined, expected: { uri: string; time: Number; date: Date }) { - assert.isOk(item, 'Quick pick item not defined'); - if (item) { - assert.equal(item.label, expected.uri, 'Wrong URI value in quick pick'); - assert.equal( - item.detail, - DataScience.jupyterSelectURIMRUDetail(expected.date), - 'Wrong detail value in quick pick' - ); - } - } - - test('Remote server uri', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'http://localhost:1111', true); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal( - value?.uri, - 'http://localhost:1111', - 'Already running should end up with the user inputed value' - ); - }); - test('Remote server uri no workspace', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'http://localhost:1111', false); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal( - value?.uri, - 'http://localhost:1111', - 'Already running should end up with the user inputed value' - ); - }); - - test('Remote server uri no local', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'http://localhost:1111', true); - await selector.selectJupyterURI('nonUser'); - const value = await storage.getUri(); - assert.equal( - value?.uri, - 'http://localhost:1111', - 'Already running should end up with the user inputed value' - ); - }); - - test('Remote server uri (reload VSCode if there is a change in settings)', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'http://localhost:1111', true); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal( - value?.uri, - 'http://localhost:1111', - 'Already running should end up with the user inputed value' - ); - }); - - test('Remote server uri (do not reload VSCode if there is no change in settings)', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'http://localhost:1111', true); - await storage.setUri('http://localhost:1111', undefined); - - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal( - value?.uri, - 'http://localhost:1111', - 'Already running should end up with the user inputed value' - ); - }); - - test('Invalid server uri', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'httx://localhost:1111', true); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.notEqual(value?.uri, 'httx://localhost:1111', 'Already running should validate'); - assert.equal(value?.uri, 'local', 'Validation failed'); - }); - - test('Server is validated', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'https://localhost:1111', 'Validation failed'); - verify(connection.validateRemoteUri('https://localhost:1111')).atLeast(1); - }); - - test('Remote authorization is asked when ssl cert is invalid and works', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - when(connection.validateRemoteUri(anyString())).thenReject(new Error('reason: self signed certificate')); - when( - applicationShell.showErrorMessage(anything(), deepEqual({ modal: true }), anything(), anything()) - ).thenCall((_m, _opt, c1, _c2) => { - return Promise.resolve(c1); - }); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'https://localhost:1111', 'Validation failed'); - verify(connection.validateRemoteUri('https://localhost:1111')).atLeast(1); - }); - test('Remote authorization is asked when ssl cert has expired is invalid and works', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - when(connection.validateRemoteUri(anyString())).thenReject(new Error('reason: certificate has expired')); - when( - applicationShell.showErrorMessage(anything(), deepEqual({ modal: true }), anything(), anything()) - ).thenCall((_m, _opt, c1, _c2) => { - return Promise.resolve(c1); - }); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'https://localhost:1111', 'Validation failed'); - verify(connection.validateRemoteUri('https://localhost:1111')).atLeast(1); - }); - - test('Remote authorization is asked for usage of self signed ssl cert and skipped', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - when(connection.validateRemoteUri(anyString())).thenReject(new Error('reason: self signed certificate')); - when(applicationShell.showErrorMessage(anything(), anything(), anything())).thenCall((_m, _c1, c2) => { - return Promise.resolve(c2); - }); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'local', 'Should not be a remote URI'); - verify(connection.validateRemoteUri('https://localhost:1111')).once(); - }); - - test('Fails to connect to remote jupyter server, hence remote jupyter server is not used', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - when(connection.validateRemoteUri(anyString())).thenReject(new Error('Failed to connect to remote server')); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'local', 'Should not be a remote URI'); - verify(connection.validateRemoteUri('https://localhost:1111')).once(); - }); - - test('Remote authorization is asked and skipped for a different error', async () => { - const { selector, storage } = createDataScienceObject('$(server) Existing', 'https://localhost:1111', true); - when(connection.validateRemoteUri(anyString())).thenReject(new Error('different error')); - await selector.selectJupyterURI('commandPalette'); - const value = await storage.getUri(); - assert.equal(value?.uri, 'local', 'Should not be a remote URI'); - verify(connection.validateRemoteUri('https://localhost:1111')).once(); - }); - }); -}); diff --git a/src/kernels/jupyter/types.ts b/src/kernels/jupyter/types.ts index 12e44a53d9f..e78e86f565f 100644 --- a/src/kernels/jupyter/types.ts +++ b/src/kernels/jupyter/types.ts @@ -270,13 +270,9 @@ export interface IJupyterServerUriEntry { export const IJupyterServerUriStorage = Symbol('IJupyterServerUriStorage'); export interface IJupyterServerUriStorage { - isLocalLaunch: boolean; - onDidChangeConnectionType: Event; - readonly currentServerId: string | undefined; readonly onDidChangeUri: Event; readonly onDidRemoveUris: Event; readonly onDidAddUri: Event; - addToUriList(uri: string, time: number, displayName: string): Promise; /** * Adds a server to the MRU list. * Similar to `addToUriList` however one does not need to pass the `Uri` nor the `displayName`. @@ -286,11 +282,8 @@ export interface IJupyterServerUriStorage { getSavedUriList(): Promise; removeUri(uri: string): Promise; clearUriList(): Promise; - getRemoteUri(): Promise; getUriForServer(id: string): Promise; - setUriToLocal(): Promise; setUriToRemote(uri: string, displayName: string): Promise; - setUriToNone(): Promise; } export interface IBackupFile { diff --git a/src/kernels/raw/finder/contributedKerneFinder.node.unit.test.ts b/src/kernels/raw/finder/contributedKerneFinder.node.unit.test.ts index 4ad252a68ed..9d213e55403 100644 --- a/src/kernels/raw/finder/contributedKerneFinder.node.unit.test.ts +++ b/src/kernels/raw/finder/contributedKerneFinder.node.unit.test.ts @@ -244,10 +244,9 @@ import { IPythonExecutionService, IPythonExecutionFactory } from '../../../platf when(memento.update('JUPYTER_GLOBAL_KERNELSPECS_V2', anything())).thenResolve(); const uriStorage = mock(); - when(uriStorage.isLocalLaunch).thenReturn(true); const onDidChangeEvent = new EventEmitter(); disposables.push(onDidChangeEvent); - when(uriStorage.onDidChangeConnectionType).thenReturn(onDidChangeEvent.event); + when(uriStorage.onDidChangeUri).thenReturn(onDidChangeEvent.event); const extensions = mock(); const trustedKernels = mock(); diff --git a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts index 42d217e8c34..7784ff0aa90 100644 --- a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts +++ b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts @@ -23,7 +23,6 @@ import { } from '../../../kernels/types'; import { JupyterKernelSpec } from '../../jupyter/jupyterKernelSpec'; import { getComparisonKey } from '../../../platform/vscode-path/resources'; -import { removeOldCachedItems } from '../../common/commonFinder'; import { PromiseMonitor } from '../../../platform/common/utils/promises'; import { disposeAllDisposables } from '../../../platform/common/helpers'; import { JupyterPaths } from './jupyterPaths.node'; @@ -279,16 +278,13 @@ export abstract class LocalKernelSpecFinderBase< } protected async writeToMementoCache(values: T[], cacheKey: string) { - await Promise.all([ - removeOldCachedItems(this.memento), - this.memento.update( - cacheKey, - JSON.stringify({ - kernels: values.map((item) => item.toJSON()), - extensionVersion: this.env.extensionVersion - }) - ) - ]); + await this.memento.update( + cacheKey, + JSON.stringify({ + kernels: values.map((item) => item.toJSON()), + extensionVersion: this.env.extensionVersion + }) + ); } protected async isValidCachedKernel(kernel: LocalKernelConnectionMetadata): Promise { switch (kernel.kind) { diff --git a/src/notebooks/controllers/controllerRegistration.ts b/src/notebooks/controllers/controllerRegistration.ts index 1efecd4ee75..77170a30163 100644 --- a/src/notebooks/controllers/controllerRegistration.ts +++ b/src/notebooks/controllers/controllerRegistration.ts @@ -103,7 +103,7 @@ export class ControllerRegistration implements IControllerRegistration, IExtensi this.disposables ); this.pythonEnvFilter.onDidChange(this.onDidChangeFilter, this, this.disposables); - this.serverUriStorage.onDidChangeConnectionType(this.onDidChangeFilter, this, this.disposables); + this.serverUriStorage.onDidChangeUri(this.onDidChangeFilter, this, this.disposables); this.serverUriStorage.onDidChangeUri(this.onDidChangeUri, this, this.disposables); this.serverUriStorage.onDidRemoveUris(this.onDidRemoveUris, this, this.disposables); diff --git a/src/notebooks/controllers/controllerRegistration.unit.test.ts b/src/notebooks/controllers/controllerRegistration.unit.test.ts index a3d947d3df5..dfc41f3e44d 100644 --- a/src/notebooks/controllers/controllerRegistration.unit.test.ts +++ b/src/notebooks/controllers/controllerRegistration.unit.test.ts @@ -105,7 +105,6 @@ suite('Controller Registration', () => { removed: IContributedKernelFinder[]; }>; let onDidChangeFilter: EventEmitter; - let onDidChangeConnectionType: EventEmitter; let onDidChangeUri: EventEmitter; let onDidRemoveUris: EventEmitter; let onDidChangeInterpreter: EventEmitter; @@ -169,8 +168,6 @@ suite('Controller Registration', () => { disposables.push(onDidChangeRegistrations); onDidChangeFilter = new EventEmitter(); disposables.push(onDidChangeFilter); - onDidChangeConnectionType = new EventEmitter(); - disposables.push(onDidChangeConnectionType); onDidChangeUri = new EventEmitter(); disposables.push(onDidChangeUri); onDidRemoveUris = new EventEmitter(); @@ -195,7 +192,6 @@ suite('Controller Registration', () => { when(kernelFinder.onDidChangeKernels).thenReturn(onDidChangeKernels.event); when(kernelFinder.onDidChangeRegistrations).thenReturn(onDidChangeRegistrations.event); when(kernelFilter.onDidChange).thenReturn(onDidChangeFilter.event); - when(serverUriStorage.onDidChangeConnectionType).thenReturn(onDidChangeConnectionType.event); when(serverUriStorage.onDidChangeUri).thenReturn(onDidChangeUri.event); when(serverUriStorage.onDidRemoveUris).thenReturn(onDidRemoveUris.event); when(interpreters.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); @@ -283,7 +279,6 @@ suite('Controller Registration', () => { condaPythonConnection, javaKernelConnection ]); - when(serverUriStorage.isLocalLaunch).thenReturn(true); const controller = mock(); (instance(controller) as any).then = undefined; when(controller.connection).thenReturn(instance(mock())); @@ -313,7 +308,6 @@ suite('Controller Registration', () => { condaPythonConnection, javaKernelConnection ]); - when(serverUriStorage.isLocalLaunch).thenReturn(true); // const controller = mock(); // (instance(controller) as any).then = undefined; // when(controller.connection).thenReturn(instance(mock())); @@ -403,7 +397,6 @@ suite('Controller Registration', () => { condaPythonConnection, javaKernelConnection ]); - when(serverUriStorage.isLocalLaunch).thenReturn(true); const controller = mock(); (instance(controller) as any).then = undefined; when(controller.connection).thenReturn(instance(mock())); diff --git a/src/notebooks/controllers/remoteKernelControllerWatcher.ts b/src/notebooks/controllers/remoteKernelControllerWatcher.ts index 19bcd09b8e0..4bd7fc09a9b 100644 --- a/src/notebooks/controllers/remoteKernelControllerWatcher.ts +++ b/src/notebooks/controllers/remoteKernelControllerWatcher.ts @@ -10,7 +10,6 @@ import { } from '../../kernels/jupyter/types'; import { isLocalConnection } from '../../kernels/types'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; -import { Settings } from '../../platform/common/constants'; import { IDisposableRegistry } from '../../platform/common/types'; import { noop } from '../../platform/common/utils/misc'; import { traceError, traceWarning } from '../../platform/logging'; @@ -53,9 +52,6 @@ export class RemoteKernelControllerWatcher implements IExtensionSyncActivationSe const registeredHandles: string[] = []; await Promise.all( uris.map(async (item) => { - if (item.uri === Settings.JupyterServerLocalLaunch) { - return; - } // Check if this url is associated with a provider. const info = extractJupyterServerHandleAndId(item.uri); if (!info || info.id !== provider.id) { @@ -76,7 +72,7 @@ export class RemoteKernelControllerWatcher implements IExtensionSyncActivationSe if (!handles.includes(info.handle)) { // Looks like the 3rd party provider has updated its handles and this server is no longer available. await this.uriStorage.removeUri(item.uri); - } else if (!item.isValidated && item.serverId === this.uriStorage.currentServerId) { + } else if (!item.isValidated) { await this.uriStorage.setUriToRemote(item.uri, item.displayName ?? item.uri).catch(noop); } }) diff --git a/src/platform/common/cache.ts b/src/platform/common/cache.ts new file mode 100644 index 00000000000..ba421a5d8d9 --- /dev/null +++ b/src/platform/common/cache.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Memento } from 'vscode'; +import { noop } from './utils/misc'; + +export async function removeOldCachedItems(globalState: Memento): Promise { + await Promise.all( + [ + 'currentServerHash', + 'connectToLocalKernelsOnly', + 'JUPYTER_LOCAL_KERNELSPECS', + 'JUPYTER_LOCAL_KERNELSPECS_V1', + 'JUPYTER_LOCAL_KERNELSPECS_V2', + 'JUPYTER_LOCAL_KERNELSPECS_V3', + 'JUPYTER_REMOTE_KERNELSPECS', + 'JUPYTER_REMOTE_KERNELSPECS_V1', + 'JUPYTER_REMOTE_KERNELSPECS_V2', + 'JUPYTER_REMOTE_KERNELSPECS_V3', + 'JUPYTER_LOCAL_KERNELSPECS_V4', + 'LOCAL_KERNEL_SPECS_CACHE_KEY_V_2022_10', + 'LOCAL_KERNEL_PYTHON_AND_RELATED_SPECS_CACHE_KEY_V_2022_10' + ] + .filter((key) => globalState.get(key, undefined) !== undefined) + .map((key) => globalState.update(key, undefined).then(noop, noop)) + ); +} diff --git a/src/standalone/userJupyterServer/userServerUrlProvider.ts b/src/standalone/userJupyterServer/userServerUrlProvider.ts index e30f9c10606..0363fad89a9 100644 --- a/src/standalone/userJupyterServer/userServerUrlProvider.ts +++ b/src/standalone/userJupyterServer/userServerUrlProvider.ts @@ -91,12 +91,6 @@ export class UserJupyterServerUrlProvider implements IExtensionSyncActivationSer const existingServers = await this.serverUriStorage.getSavedUriList(); const migratedServers = []; for (const server of existingServers) { - // server.uri is never 'local' - // we should check if the server is from uri provider - if (server.uri === Settings.JupyterServerLocalLaunch) { - continue; - } - const info = extractJupyterServerHandleAndId(server.uri); if (info) { continue; diff --git a/src/test/datascience/notebook/controllerDefaultService.ts b/src/test/datascience/notebook/controllerDefaultService.ts index 47afd1296ad..c44efeba2e3 100644 --- a/src/test/datascience/notebook/controllerDefaultService.ts +++ b/src/test/datascience/notebook/controllerDefaultService.ts @@ -4,7 +4,6 @@ import { NotebookDocument } from 'vscode'; import { isPythonNotebook } from '../../../kernels/helpers'; import { PreferredRemoteKernelIdProvider } from '../../../kernels/jupyter/connection/preferredRemoteKernelIdProvider'; -import { IJupyterServerUriStorage } from '../../../kernels/jupyter/types'; import { IVSCodeNotebook } from '../../../platform/common/application/types'; import { InteractiveWindowView, JupyterNotebookView, PYTHON_LANGUAGE } from '../../../platform/common/constants'; import { IDisposableRegistry, IsWebExtension, Resource } from '../../../platform/common/types'; @@ -15,20 +14,17 @@ import { isEqual } from '../../../platform/vscode-path/resources'; import { createActiveInterpreterController } from '../../../notebooks/controllers/helpers'; import { IControllerRegistration, IVSCodeNotebookController } from '../../../notebooks/controllers/types'; import { IServiceContainer } from '../../../platform/ioc/types'; +import { IS_REMOTE_NATIVE_TEST } from '../../constants'; /** * Determines the 'default' kernel for a notebook. Default is what kernel should be used if there's no metadata in a notebook. */ export class ControllerDefaultService { - private get isLocalLaunch(): boolean { - return this.serverUriStorage.isLocalLaunch; - } constructor( private readonly registration: IControllerRegistration, private readonly interpreters: IInterpreterService, private readonly notebook: IVSCodeNotebook, readonly disposables: IDisposableRegistry, - private readonly serverUriStorage: IJupyterServerUriStorage, private readonly preferredRemoteFinder: PreferredRemoteKernelIdProvider, private readonly isWeb: boolean ) {} @@ -40,7 +36,6 @@ export class ControllerDefaultService { serviceContainer.get(IInterpreterService), serviceContainer.get(IVSCodeNotebook), serviceContainer.get(IDisposableRegistry), - serviceContainer.get(IJupyterServerUriStorage), serviceContainer.get(PreferredRemoteKernelIdProvider), serviceContainer.get(IsWebExtension, IsWebExtension) ); @@ -51,7 +46,7 @@ export class ControllerDefaultService { resource: Resource, viewType: typeof JupyterNotebookView | typeof InteractiveWindowView ): Promise { - if (this.isLocalLaunch) { + if (!IS_REMOTE_NATIVE_TEST()) { traceInfoIfCI('CreateActiveInterpreterController'); return createActiveInterpreterController(viewType, resource, this.interpreters, this.registration); } else { diff --git a/src/test/datascience/notebook/controllerPreferredService.ts b/src/test/datascience/notebook/controllerPreferredService.ts index f555e78856e..9848ac91c2f 100644 --- a/src/test/datascience/notebook/controllerPreferredService.ts +++ b/src/test/datascience/notebook/controllerPreferredService.ts @@ -11,7 +11,6 @@ import { workspace } from 'vscode'; import { getKernelConnectionLanguage, getLanguageInNotebookMetadata, isPythonNotebook } from '../../../kernels/helpers'; -import { IJupyterServerUriStorage } from '../../../kernels/jupyter/types'; import { trackKernelResourceInformation } from '../../../kernels/telemetry/helper'; import { KernelConnectionMetadata, isLocalConnection } from '../../../kernels/types'; import { @@ -48,6 +47,7 @@ import { IServiceContainer } from '../../../platform/ioc/types'; import { KernelRankingHelper, findKernelSpecMatchingInterpreter } from './kernelRankingHelper'; import { PreferredRemoteKernelIdProvider } from '../../../kernels/jupyter/connection/preferredRemoteKernelIdProvider'; import { ControllerDefaultService } from './controllerDefaultService'; +import { IS_REMOTE_NATIVE_TEST } from '../../constants'; /** * Computes and tracks the preferred kernel for a notebook. @@ -56,9 +56,6 @@ import { ControllerDefaultService } from './controllerDefaultService'; export class ControllerPreferredService { private preferredControllers = new WeakMap(); private preferredCancelTokens = new WeakMap(); - private get isLocalLaunch(): boolean { - return this.serverUriStorage.isLocalLaunch; - } private disposables = new Set(); constructor( private readonly registration: IControllerRegistration, @@ -66,7 +63,6 @@ export class ControllerPreferredService { private readonly interpreters: IInterpreterService, private readonly notebook: IVSCodeNotebook, private readonly extensionChecker: IPythonExtensionChecker, - private readonly serverUriStorage: IJupyterServerUriStorage, private readonly kernelRankHelper: KernelRankingHelper, private readonly isWebExtension: boolean ) {} @@ -79,7 +75,6 @@ export class ControllerPreferredService { serviceContainer.get(IInterpreterService), serviceContainer.get(IVSCodeNotebook), serviceContainer.get(IPythonExtensionChecker), - serviceContainer.get(IJupyterServerUriStorage), new KernelRankingHelper( serviceContainer.get(PreferredRemoteKernelIdProvider) ), @@ -155,7 +150,11 @@ export class ControllerPreferredService { const isPythonNbOrInteractiveWindow = (isEmptyMetadata ? isPythonNotebookLanguage : isPythonNotebook(notebookMetadata)) || resourceType === 'interactive'; - if (document.notebookType === JupyterNotebookView && !this.isLocalLaunch && isPythonNbOrInteractiveWindow) { + if ( + document.notebookType === JupyterNotebookView && + IS_REMOTE_NATIVE_TEST() && + isPythonNbOrInteractiveWindow + ) { const defaultPythonController = await this.defaultService.computeDefaultController( document.uri, document.notebookType diff --git a/src/test/datascience/notebook/helper.ts b/src/test/datascience/notebook/helper.ts index 50dd711fb14..74c26023152 100644 --- a/src/test/datascience/notebook/helper.ts +++ b/src/test/datascience/notebook/helper.ts @@ -294,7 +294,6 @@ export async function createEmptyPythonNotebook( traceInfoIfCI('Creating an empty notebook'); const { serviceContainer } = await getServices(); const vscodeNotebook = serviceContainer.get(IVSCodeNotebook); - const serverUriStorage = serviceContainer.get(IJupyterServerUriStorage); // Don't use same file (due to dirty handling, we might save in dirty.) // Coz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. const nbFile = await createTemporaryNotebook( @@ -312,7 +311,7 @@ export async function createEmptyPythonNotebook( await waitForKernelToGetAutoSelected( vscodeNotebook.activeNotebookEditor!, PYTHON_LANGUAGE, - !serverUriStorage.isLocalLaunch + IS_REMOTE_NATIVE_TEST() ); await verifySelectedControllerIsRemoteForRemoteTests(); } @@ -345,15 +344,12 @@ async function shutdownRemoteKernels() { const jupyterConnection = api.serviceContainer.get(JupyterConnection); const jupyterSessionManagerFactory = api.serviceContainer.get(IJupyterSessionManagerFactory); - const uri = await serverUriStorage.getRemoteUri(); - if (!uri) { - return; - } const cancelToken = new CancellationTokenSource(); let sessionManager: IJupyterSessionManager | undefined; try { + const uris = await serverUriStorage.getSavedUriList(); const connection = await jupyterConnection.createConnectionInfo({ - serverId: serverUriStorage.currentServerId! + serverId: uris[0].serverId }); if (connection.type !== 'jupyter') { return; diff --git a/src/test/datascience/notebook/kernelSelection.vscode.test.ts b/src/test/datascience/notebook/kernelSelection.vscode.test.ts index 4c9443b0417..b46ef481b50 100644 --- a/src/test/datascience/notebook/kernelSelection.vscode.test.ts +++ b/src/test/datascience/notebook/kernelSelection.vscode.test.ts @@ -154,7 +154,6 @@ suite('Kernel Selection @kernelPicker', function () { ]); await startJupyterServer(); - jupyterServerUri = (await serverUriStorage.getRemoteUri())?.uri; sinon.restore(); }); diff --git a/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts b/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts index 6f8c192f3de..0fe811fff71 100644 --- a/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts +++ b/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts @@ -128,7 +128,6 @@ suite('Remote Kernel Execution', function () { const uri = await JupyterServer.instance.startSecondJupyterWithToken(); const uriString = decodeURIComponent(uri.toString()); traceInfo(`Another Jupyter started and listening at ${uriString}`); - await jupyterServerSelector.setJupyterURIToLocal(); await jupyterServerSelector.setJupyterURIToRemote(uriString); // Opening a notebook will trigger the refresh of the kernel list. @@ -153,9 +152,6 @@ suite('Remote Kernel Execution', function () { ); }); test('Local Kernel state is not lost when connecting to remote @kernelPicker', async function () { - // After resetting connection to local only, verify all remote connections are no longer available. - await jupyterServerSelector.setJupyterURIToLocal(); - const activeInterpreter = await interpreterService.getActiveInterpreter(); traceInfoIfCI(`active interpreter ${activeInterpreter?.uri.path}`); const { notebook } = await createEmptyPythonNotebook(disposables); From ab39b928bb430db35e32bef0ad6b39f196622018 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 17 May 2023 22:30:35 +1000 Subject: [PATCH 2/4] fixes --- src/gdpr.ts | 9 --------- src/platform/common/constants.ts | 1 - src/telemetry.ts | 9 --------- 3 files changed, 19 deletions(-) diff --git a/src/gdpr.ts b/src/gdpr.ts index 81472d98263..ff28ca40385 100644 --- a/src/gdpr.ts +++ b/src/gdpr.ts @@ -945,15 +945,6 @@ "${include}": [ "${F1}" - ] - } - */ -//Telemetry.SetJupyterURIToLocal -/* __GDPR__ - "DATASCIENCE.SET_JUPYTER_URI_LOCAL" : { - "${include}": [ - "${F1}" - ] } */ diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index c2ced81d3fe..9e81cd45474 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -330,7 +330,6 @@ export enum Telemetry { EnterJupyterURI = 'DATASCIENCE.ENTER_JUPYTER_URI', SelectLocalJupyterKernel = 'DATASCIENCE.SELECT_LOCAL_JUPYTER_KERNEL', SelectRemoteJupyterKernel = 'DATASCIENCE.SELECT_REMOTE_JUPYTER_KERNEL', - SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL', SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED', SetJupyterURIUIDisplayed = 'DATASCIENCE.SET_JUPYTER_URI_UI_DISPLAYED', Interrupt = 'DATASCIENCE.INTERRUPT', diff --git a/src/telemetry.ts b/src/telemetry.ts index 50354a9bf3b..9000b18e92d 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -1763,15 +1763,6 @@ export class IEventNamePropertyMapping { } } }; - /** - * Jupyter URI was set to local. - */ - [Telemetry.SetJupyterURIToLocal]: TelemetryEventInfo = { - owner: 'donjayamanne', - feature: ['KernelPicker'], - source: 'N/A', - measures: commonClassificationForDurationProperties() - }; /** * Jupyter URI was valid and set to a remote setting. */ From 9bf57663f3179d9f3b3a30031061cb977729e473 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 17 May 2023 22:34:30 +1000 Subject: [PATCH 3/4] fixes --- .../jupyter/connection/jupyterRemoteCachedKernelValidator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernels/jupyter/connection/jupyterRemoteCachedKernelValidator.ts b/src/kernels/jupyter/connection/jupyterRemoteCachedKernelValidator.ts index f47f783fd2d..73942da344d 100644 --- a/src/kernels/jupyter/connection/jupyterRemoteCachedKernelValidator.ts +++ b/src/kernels/jupyter/connection/jupyterRemoteCachedKernelValidator.ts @@ -55,7 +55,7 @@ export class JupyterRemoteCachedKernelValidator implements IJupyterRemoteCachedK return true; } else { traceWarning( - `Hiding remote kernel ${kernel.id} as the remote Jupyter Server ${item.uri} is no longer available` + `Hiding remote kernel ${kernel.id} for ${provider.id} as the remote Jupyter Server ${item.serverId} is no longer available` ); // 3rd party extensions own these kernels, if these are no longer // available, then just don't display them. From 17f34a0ac6515b90dd19d5dd03201cd98c64ad93 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 17 May 2023 22:37:14 +1000 Subject: [PATCH 4/4] fix tests --- .../controllers/remoteKernelControllerWatcher.unit.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/notebooks/controllers/remoteKernelControllerWatcher.unit.test.ts b/src/notebooks/controllers/remoteKernelControllerWatcher.unit.test.ts index 819b6317cd4..ba2b082308c 100644 --- a/src/notebooks/controllers/remoteKernelControllerWatcher.unit.test.ts +++ b/src/notebooks/controllers/remoteKernelControllerWatcher.unit.test.ts @@ -120,6 +120,7 @@ suite('RemoteKernelControllerWatcher', () => { when(uriStorage.getSavedUriList()).thenResolve([ { time: 1, serverId, uri: remoteUriForProvider1, displayName: 'Something' } ]); + when(uriStorage.setUriToRemote(anything(), anything())).thenResolve(); watcher.activate();