diff --git a/news/1 Enhancements/3763.md b/news/1 Enhancements/3763.md new file mode 100644 index 000000000000..cc59d7c8d3ad --- /dev/null +++ b/news/1 Enhancements/3763.md @@ -0,0 +1 @@ +Add ability to select remote Jupyter kernel on remote jupyter hosts - @hochshi diff --git a/news/1 Enhancements/7014.md b/news/1 Enhancements/7014.md new file mode 100644 index 000000000000..e0b01c98da37 --- /dev/null +++ b/news/1 Enhancements/7014.md @@ -0,0 +1 @@ +Add ability to reconnect to existing Jupyter kernel on remote jupyter hosts - @kdkavanagh diff --git a/package.json b/package.json index c022da8be5d3..acfa045262fa 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,8 @@ "onCommand:python.datascience.showhistorypane", "onCommand:python.datascience.importnotebook", "onCommand:python.datascience.selectjupyteruri", + "onCommand:python.dataScience.jupyterserverkernelid", + "onCommand:python.dataScience.jupyterserverallowkernelshutdown", "onCommand:python.datascience.exportfileasnotebook", "onCommand:python.datascience.exportfileandoutputasnotebook", "onCommand:python.enableSourceMapSupport" @@ -1359,12 +1361,36 @@ "description": "Number of times to attempt to connect to the Jupyter Notebook", "scope": "resource" }, + "python.dataScience.jupyterServers": { + "type": "array", + "default": [], + "description": "Jupter servers.", + "scope": "resource" + }, "python.dataScience.jupyterServerURI": { "type": "string", "default": "local", "description": "Select the Jupyter server URI to connect to. Select 'local' to launch a new Juypter server on the local machine.", "scope": "resource" }, + "python.dataScience.jupyterServerKernelId": { + "type": "string", + "default": "", + "description": "Select the Jupyter server kernel UUID to connect to. Leave blank to start a new kernel", + "scope": "resource" + }, + "python.dataScience.jupyterServerKernelSpec": { + "type": "object", + "default": "", + "description": "Specify kernel spec to connect to. Leave blank to start a new kernel", + "scope": "resource" + }, + "python.dataScience.jupyterServerAllowKernelShutdown": { + "type": "boolean", + "default": true, + "description": "Shutdown the Jupyter kernel when finished.", + "scope": "resource" + }, "python.dataScience.notebookFileRoot": { "type": "string", "default": "${workspaceFolder}", diff --git a/package.nls.json b/package.nls.json index 1d794158e855..03fc435f4501 100644 --- a/package.nls.json +++ b/package.nls.json @@ -174,6 +174,10 @@ "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", "DataScience.jupyterSelectURIInvalidURI": "Invalid URI specified", + "DataScience.jupyterServerReconnectKernel": "Select Jupyer kernel", + "DataScience.jupyterServerReconnectKernelStartNew": "Start new kernel on Jupyter server", + "DataScience.jupyterServerKernelAutoShutdown": "Automatically shutdown kernel when closed", + "DataScience.jupyterServerKernelLeaveRunning": "Leave kernel running when closed", "DataScience.jupyterSelectPasswordPrompt": "Enter your notebook password", "DataScience.jupyterNotebookFailure": "Jupyter notebook failed to launch. \r\n{0}", "DataScience.jupyterNotebookConnectFailed": "Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}", diff --git a/src/client/common/types.ts b/src/client/common/types.ts index bf2354d29f9f..fa66b1345eb7 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -5,6 +5,7 @@ import { Socket } from 'net'; import { Request as RequestResult } from 'request'; import { ConfigurationTarget, DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode'; +import { IJupyterKernelSpec, IJupyterServer } from '../datascience/types'; import { CommandsWithoutArgs } from './application/commands'; import { ExtensionChannels } from './insidersBuild/types'; import { EnvironmentVariables } from './variables/types'; @@ -303,6 +304,10 @@ export interface IDataScienceSettings { jupyterInterruptTimeout: number; jupyterLaunchTimeout: number; jupyterLaunchRetries: number; + jupyterServerAllowKernelShutdown: boolean; + jupyterServerKernelId: string; + jupyterServerKernelSpec: IJupyterKernelSpec | undefined; + jupyterServers: IJupyterServer[] | undefined; jupyterServerURI: string; notebookFileRoot: string; changeDirOnImportExport: boolean; diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index cf28e8d0244f..8d6f8958d03d 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -176,6 +176,10 @@ export namespace DataScience { export const jupyterSelectURISpecifyURI = localize('DataScience.jupyterSelectURISpecifyURI', 'Type in the URI for the Jupyter server'); export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of a Jupyter server'); export const jupyterSelectURIInvalidURI = localize('DataScience.jupyterSelectURIInvalidURI', 'Invalid URI specified'); + export const jupyterServerReconnectKernelLocal = localize('DataScience.jupyterServerReconnectKernelLocal', 'Select Jupyer Kernel'); + export const jupyterServerReconnectKernelStartNewLocal = localize('DataScience.jupyterServerReconnectKernelStartNewLocal', 'Start new kernel on Jupyter server'); + export const jupyterServerKernelAutoShutdownLocal = localize('DataScience.jupyterServerKernelAutoShutdownLocal', 'Automatically shutdown Kernel when closed'); + export const jupyterServerKernelLeaveRunningLocal = localize('DataScience.jupyterServerKernelLeaveRunningLocal', 'Leave Kernel running when closed'); export const jupyterSelectPasswordPrompt = localize('DataScience.jupyterSelectPasswordPrompt', 'Enter your notebook password'); export const jupyterNotebookFailure = localize('DataScience.jupyterNotebookFailure', 'Jupyter notebook failed to launch. \r\n{0}'); export const jupyterNotebookConnectFailed = localize('DataScience.jupyterNotebookConnectFailed', 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'); diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 216f37fea021..c9bc41936473 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -112,6 +112,8 @@ export enum Telemetry { ExpandAll = 'DATASCIENCE.EXPAND_ALL', CollapseAll = 'DATASCIENCE.COLLAPSE_ALL', SelectJupyterURI = 'DATASCIENCE.SELECT_JUPYTER_URI', + JupyterKernelSpecified = 'DATASCIENCE.JUPYTER_KERNEL_SPECIFIED', + JupyterKernelAutoShutdown = 'DATASCIENCE.JUPYTER_AUTO_SHUTDOWN', SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL', SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED', Interrupt = 'DATASCIENCE.INTERRUPT', diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts index 55e5579ad43a..98130d362e10 100644 --- a/src/client/datascience/datascience.ts +++ b/src/client/datascience/datascience.ts @@ -3,6 +3,7 @@ 'use strict'; import '../common/extensions'; +import { Kernel } from '@jupyterlab/services'; import { JSONObject } from '@phosphor/coreutils'; import { inject, injectable } from 'inversify'; import { URL } from 'url'; @@ -11,7 +12,7 @@ import * as vscode from 'vscode'; import { IApplicationShell, ICommandManager, IDebugService, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; import { ContextKey } from '../common/contextKey'; -import { traceError } from '../common/logger'; +import { traceError, traceInfo } from '../common/logger'; import { BANNER_NAME_DS_SURVEY, IConfigurationService, @@ -26,7 +27,43 @@ import { IServiceContainer } from '../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; import { hasCells } from './cellFactory'; import { Commands, EditorContexts, Settings, Telemetry } from './constants'; -import { ICodeWatcher, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener } from './types'; +import { JupyterExecutionBase } from './jupyter/jupyterExecution'; +import { + ICodeWatcher, + IConnection, + IDataScience, + IDataScienceCodeLensProvider, + IDataScienceCommandListener, + IJupyterKernelSpec, + IJupyterSessionManager, + IJupyterShutdown, + IKernelQuickPickItem, + IJupyterServer, + IJupyterServerQuickPickItem +} from './types'; +// import { hostname } from 'os'; +// import { Uri } from 'monaco-editor'; +// import { Icons } from '../testing/common/constants'; +// import { Dictionary } from 'lodash'; + +const newKernel = { + label: localize.DataScience.jupyterServerReconnectKernelStartNewLocal(), + picked: true, + kernelId: '', + name: '' +}; + +const localJupyter: IJupyterServerQuickPickItem = { + label: localize.DataScience.jupyterSelectURILaunchLocal(), + hostName: 'local', + uri: '' +}; + +const specifyJupyter: IJupyterServerQuickPickItem = { + label: localize.DataScience.jupyterSelectURISpecifyURI(), + hostName: '', + uri: '' +}; @injectable() export class DataScience implements IDataScience { @@ -44,7 +81,8 @@ export class DataScience implements IDataScience { @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IApplicationShell) private appShell: IApplicationShell, @inject(IDebugService) private debugService: IDebugService, - @inject(IWorkspaceService) private workspace: IWorkspaceService + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IJupyterSessionManager) private sessionManager: IJupyterSessionManager ) { this.commandListeners = this.serviceContainer.getAll(IDataScienceCommandListener); this.dataScienceSurveyBanner = this.serviceContainer.get(IPythonExtensionBanner, BANNER_NAME_DS_SURVEY); @@ -217,19 +255,53 @@ export class DataScience implements IDataScience { @captureTelemetry(Telemetry.SelectJupyterURI) public async selectJupyterURI(): Promise { - const quickPickOptions = [localize.DataScience.jupyterSelectURILaunchLocal(), localize.DataScience.jupyterSelectURISpecifyURI()]; - const selection = await this.appShell.showQuickPick(quickPickOptions, { ignoreFocusOut: true }); + const settings = this.configuration.getSettings(); + const jupyterServers: IJupyterServer[] | undefined = settings.datascience.jupyterServers; + let optionsArr: IJupyterServerQuickPickItem[] = []; + if (jupyterServers) { + optionsArr = jupyterServers.map(server => { + traceInfo(`Found server ${server.hostName}, with uri ${server.uri}`); + return { + label: server.hostName, + hostName: server.hostName, + uri: server.uri + }; + }); + } + optionsArr.unshift(localJupyter); + optionsArr.push(specifyJupyter); + + // const quickPickOptions = [localize.DataScience.jupyterSelectURILaunchLocal(), localize.DataScience.jupyterSelectURISpecifyURI()]; + const selection = await this.appShell.showQuickPick(optionsArr, { ignoreFocusOut: true }); + if (!selection) { + return; + } + + let connInfo: IConnection | undefined; + switch (selection) { - case localize.DataScience.jupyterSelectURILaunchLocal(): + case localJupyter: return this.setJupyterURIToLocal(); break; - case localize.DataScience.jupyterSelectURISpecifyURI(): - return this.selectJupyterLaunchURI(); + case specifyJupyter: + const userURI = await this.appShell.showInputBox({ + prompt: localize.DataScience.jupyterSelectURIPrompt(), + placeHolder: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe', validateInput: this.validateURI, ignoreFocusOut: true + }); + if (userURI) { + connInfo = await this.createConnectionInfo(userURI, true); + } break; + // return this.selectJupyterLaunchURI(); + // break; default: // If user cancels quick pick we will get undefined as the selection and fall through here + connInfo = await this.createConnectionInfo(selection.uri); break; } + if (connInfo) { + return this.selectJupyterLaunchURI(connInfo); + } } public async debugCell(file: string, startLine: number, startChar: number, endLine: number, endChar: number): Promise { @@ -277,19 +349,125 @@ export class DataScience implements IDataScience { @captureTelemetry(Telemetry.SetJupyterURIToLocal) private async setJupyterURIToLocal(): Promise { await this.configuration.updateSetting('dataScience.jupyterServerURI', Settings.JupyterServerLocalLaunch, undefined, vscode.ConfigurationTarget.Workspace); + await this.configuration.updateSetting('dataScience.jupyterServerKernelId', undefined, undefined, vscode.ConfigurationTarget.Workspace); + await this.configuration.updateSetting('dataScience.jupyterServerKernelSpec', undefined, undefined, vscode.ConfigurationTarget.Workspace); + await this.configuration.updateSetting('dataScience.jupyterServerAllowKernelShutdown', undefined, undefined, vscode.ConfigurationTarget.Workspace); + } + + private async getJupyterKernels(connInfo: IConnection): Promise { + const runningKernels: Kernel.IModel[] = await this.sessionManager.getActiveKernels(connInfo); + const arr: IKernelQuickPickItem[] = runningKernels.map(runningKernel => { + traceInfo(`Found running kernel ${runningKernel.id}, running since ${runningKernel.last_activity}`); + const localLastActivity = runningKernel.last_activity ? new Date(runningKernel.last_activity.toString()).toLocaleString() : '?'; + return { + label: `Kernel ${runningKernel.name} - ${runningKernel.id}`, + detail: `Running since ${localLastActivity}, ${runningKernel.connections} existing connections`, + kernelId: runningKernel.id, + name: runningKernel.name + }; + }); + arr.unshift(newKernel); + return arr; + } + + private shutdownOptions(): IJupyterShutdown[] { + const autoShutdown: IJupyterShutdown = { + label: localize.DataScience.jupyterServerKernelAutoShutdownLocal(), + keepRunning: false, + picked: true + }; + const leaveRunning: IJupyterShutdown = { + label: localize.DataScience.jupyterServerKernelLeaveRunningLocal(), + keepRunning: true, + picked: true + }; + return [autoShutdown, leaveRunning]; + } + + private async getKernelSelection(connInfo: IConnection): Promise { + const kernelOptions: IKernelQuickPickItem[] = await this.getJupyterKernels(connInfo); + + return this.appShell.showQuickPick(kernelOptions, { + ignoreFocusOut: true, + placeHolder: localize.DataScience.jupyterServerReconnectKernelLocal() + }); } - @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) - private async selectJupyterLaunchURI(): Promise { - // First get the proposed URI from the user - const userURI = await this.appShell.showInputBox({ - prompt: localize.DataScience.jupyterSelectURIPrompt(), - placeHolder: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe', validateInput: this.validateURI, ignoreFocusOut: true + private async getNewKernelSelection(kernelSpecs: IJupyterKernelSpec[]): Promise { + const availArr: IKernelQuickPickItem[] = kernelSpecs.map(availableKernel => { + traceInfo(`Found available kernel ${availableKernel.name}`); + return { + label: `Kernel ${availableKernel.name}`, + detail: '', + kernelId: '', + name: availableKernel.name + }; + }); + + return this.appShell.showQuickPick(availArr, { + ignoreFocusOut: true }); + } + + private async createConnectionInfo(uri: string, addToSettings: boolean = false): Promise { + await this.configuration.updateSetting('dataScience.jupyterServerURI', uri, undefined, vscode.ConfigurationTarget.Workspace); + const connInfo: IConnection = JupyterExecutionBase.createRemoteConnectionInfo(uri, this.configuration); + if (addToSettings) { + const settings = this.configuration.getSettings(); + let jupyterServers: IJupyterServer[] = [{ + hostName: connInfo.hostName, + uri: uri + }]; + const savedjupyterServers = settings.datascience.jupyterServers; + if (savedjupyterServers) { + jupyterServers = jupyterServers.concat(savedjupyterServers); + } + await this.configuration.updateSetting('dataScience.jupyterServers', jupyterServers, undefined, vscode.ConfigurationTarget.Workspace); + } + return connInfo; + } + + @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) + private async selectJupyterLaunchURI(connInfo: IConnection): Promise { + + let kernelUUID: string | undefined = ''; + let kernelName: string | undefined = ''; + let kernelSpec: IJupyterKernelSpec | undefined; + let allowShutdown = true; + + let kernelSelection: IKernelQuickPickItem | undefined = await this.getKernelSelection(connInfo); + + const shutdownSelection = await this.appShell.showQuickPick(this.shutdownOptions(), { ignoreFocusOut: true }); + + let kernelSpecs: IJupyterKernelSpec[] = await this.sessionManager.getActiveKernelSpecs(connInfo); + + if (kernelSelection && kernelSelection === newKernel) { + traceInfo('Will create a new kernel for connection'); + kernelSelection = await this.getNewKernelSelection(kernelSpecs); + } + sendTelemetryEvent(Telemetry.JupyterKernelAutoShutdown, undefined, { autoShutdownEnabled: allowShutdown }); + + if (kernelSelection) { + if (kernelSelection !== newKernel) { + traceInfo(`Will connect to existing kernel ${kernelSelection.kernelId}`); + sendTelemetryEvent(Telemetry.JupyterKernelSpecified); + } + kernelUUID = kernelSelection.kernelId ? kernelSelection.kernelId : undefined; + kernelName = kernelSelection.name ? kernelSelection.name : undefined; + kernelSpecs = kernelSpecs.filter(spec => spec.name === kernelName); + kernelSpec = kernelSpecs.length === 1 ? kernelSpecs[0] : undefined; + if (kernelSpec) { + kernelSpec.id = kernelUUID; + } + } - if (userURI) { - await this.configuration.updateSetting('dataScience.jupyterServerURI', userURI, undefined, vscode.ConfigurationTarget.Workspace); + if (shutdownSelection && shutdownSelection.keepRunning) { + traceInfo('Session will not be shutdown on close'); + allowShutdown = false; } + await this.configuration.updateSetting('dataScience.jupyterServerKernelId', kernelUUID, undefined, vscode.ConfigurationTarget.Workspace); + await this.configuration.updateSetting('dataScience.jupyterServerKernelSpec', kernelSpec, undefined, vscode.ConfigurationTarget.Workspace); + await this.configuration.updateSetting('dataScience.jupyterServerAllowKernelShutdown', allowShutdown, undefined, vscode.ConfigurationTarget.Workspace); } @captureTelemetry(Telemetry.AddCellBelow) diff --git a/src/client/datascience/interactive-window/interactiveWindowProvider.ts b/src/client/datascience/interactive-window/interactiveWindowProvider.ts index f546bbd799d8..482924265e01 100644 --- a/src/client/datascience/interactive-window/interactiveWindowProvider.ts +++ b/src/client/datascience/interactive-window/interactiveWindowProvider.ts @@ -23,19 +23,19 @@ interface ISyncData { @injectable() export class InteractiveWindowProvider implements IInteractiveWindowProvider, IAsyncDisposable { - private activeInteractiveWindow : IInteractiveWindow | undefined; - private postOffice : PostOffice; + private activeInteractiveWindow: IInteractiveWindow | undefined; + private postOffice: PostOffice; private id: string; - private pendingSyncs : Map = new Map(); + private pendingSyncs: Map = new Map(); private executedCode: EventEmitter = new EventEmitter(); private activeInteractiveWindowExecuteHandler: Disposable | undefined; constructor( @inject(ILiveShareApi) liveShare: ILiveShareApi, @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(IConfigurationService) private configService: IConfigurationService - ) { + ) { asyncRegistry.push(this); // Create a post office so we can make sure interactive windows are created at the same time @@ -53,15 +53,15 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA this.id = uuid(); } - public getActive() : IInteractiveWindow | undefined { + public getActive(): IInteractiveWindow | undefined { return this.activeInteractiveWindow; } - public get onExecutedCode() : Event { + public get onExecutedCode(): Event { return this.executedCode.event; } - public async getOrCreateActive() : Promise { + public async getOrCreateActive(): Promise { if (!this.activeInteractiveWindow) { await this.create(); } @@ -77,7 +77,7 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA throw new Error(localize.DataScience.pythonInteractiveCreateFailed()); } - public async getNotebookOptions() : Promise { + public async getNotebookOptions(): Promise { // Find the settings that we are going to launch our server with const settings = this.configService.getSettings(); let serverURI: string | undefined = settings.datascience.jupyterServerURI; @@ -96,11 +96,11 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA }; } - public dispose() : Promise { + public dispose(): Promise { return this.postOffice.dispose(); } - private async create() : Promise { + private async create(): Promise { // Set it as soon as we create it. The .ctor for the interactive window // may cause a subclass to talk to the IInteractiveWindowProvider to get the active interactive window. this.activeInteractiveWindow = this.serviceContainer.get(IInteractiveWindow); @@ -162,7 +162,7 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA } } - private async synchronizeCreate() : Promise { + private async synchronizeCreate(): Promise { // Create a new pending wait if necessary if (this.postOffice.peerCount > 0 || this.postOffice.role === vsls.Role.Guest) { const key = uuid(); diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 4bf699a9167a..d39a001d6286 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -49,6 +49,10 @@ enum ModuleExistsResult { export class JupyterExecutionBase implements IJupyterExecution { + public get sessionChanged(): Event { + return this.eventEmitter.event; + } + private processServicePromise: Promise; private commands: Record = {}; private jupyterPath: string | undefined; @@ -86,8 +90,29 @@ export class JupyterExecutionBase implements IJupyterExecution { } } - public get sessionChanged(): Event { - return this.eventEmitter.event; + public static createRemoteConnectionInfo = (uri: string, configuration: IConfigurationService): IConnection => { + let url: URL; + try { + url = new URL(uri); + } catch (err) { + // This should already have been parsed when set, so just throw if it's not right here + throw err; + } + const settings = configuration.getSettings(); + const allowUnauthorized = settings.datascience.allowUnauthorizedRemoteConnection ? settings.datascience.allowUnauthorizedRemoteConnection : false; + const allowShutdown: boolean | undefined = settings.datascience.jupyterServerAllowKernelShutdown; + + return { + allowUnauthorized, + baseUrl: `${url.protocol}//${url.host}${url.pathname}`, + token: `${url.searchParams.get('token')}`, + hostName: url.hostname, + localLaunch: false, + localProcExitCode: undefined, + disconnected: (_l) => { return { dispose: noop }; }, + dispose: noop, + allowShutdown: allowShutdown + }; } public dispose(): Promise { @@ -291,8 +316,9 @@ export class JupyterExecutionBase implements IJupyterExecution { } } else { // If we have a URI spec up a connection info for it - connection = this.createRemoteConnectionInfo(options.uri); - kernelSpec = undefined; + connection = JupyterExecutionBase.createRemoteConnectionInfo(options.uri, this.configuration); + const settings = this.configuration.getSettings(); + kernelSpec = settings.datascience.jupyterServerKernelSpec; } // If we don't have a kernel spec yet, check using our current connection @@ -310,29 +336,6 @@ export class JupyterExecutionBase implements IJupyterExecution { return { connection, kernelSpec }; } - private createRemoteConnectionInfo = (uri: string): IConnection => { - let url: URL; - try { - url = new URL(uri); - } catch (err) { - // This should already have been parsed when set, so just throw if it's not right here - throw err; - } - const settings = this.configuration.getSettings(); - const allowUnauthorized = settings.datascience.allowUnauthorizedRemoteConnection ? settings.datascience.allowUnauthorizedRemoteConnection : false; - - return { - allowUnauthorized, - baseUrl: `${url.protocol}//${url.host}${url.pathname}`, - token: `${url.searchParams.get('token')}`, - hostName: url.hostname, - localLaunch: false, - localProcExitCode: undefined, - disconnected: (_l) => { return { dispose: noop }; }, - dispose: noop - }; - } - // tslint:disable-next-line: max-func-body-length @captureTelemetry(Telemetry.StartJupyter) private async startNotebookServer(useDefaultConfig: boolean, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { diff --git a/src/client/datascience/jupyter/jupyterKernelSpec.ts b/src/client/datascience/jupyter/jupyterKernelSpec.ts index 9881db8671c7..57eb5b87702d 100644 --- a/src/client/datascience/jupyter/jupyterKernelSpec.ts +++ b/src/client/datascience/jupyter/jupyterKernelSpec.ts @@ -15,7 +15,8 @@ export class JupyterKernelSpec implements IJupyterKernelSpec { public language: string; public path: string; public specFile: string | undefined; - constructor(specModel : Kernel.ISpecModel, file?: string) { + public id: string | undefined; + constructor(specModel: Kernel.ISpecModel, file?: string) { this.name = specModel.name; this.language = specModel.language; this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : ''; diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts index 515739ed95df..9d3bb5f02273 100644 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -47,15 +47,21 @@ export class JupyterSession implements IJupyterSession { private connected: boolean = false; private jupyterPasswordConnect: IJupyterPasswordConnect; private oldSessions: Session.ISession[] = []; + // private allowShutdown: boolean; + // private desiredKernelId: string | undefined; constructor( connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, jupyterPasswordConnect: IJupyterPasswordConnect + // desiredKernelId: string | undefined, + // allowShutdown: boolean ) { this.connInfo = connInfo; this.kernelSpec = kernelSpec; this.jupyterPasswordConnect = jupyterPasswordConnect; + // this.allowShutdown = allowShutdown; + // this.desiredKernelId = desiredKernelId; } public dispose(): Promise { @@ -219,17 +225,29 @@ export class JupyterSession implements IJupyterSession { private async createSession(serverSettings: ServerConnection.ISettings, contentsManager: ContentsManager, cancelToken?: CancellationToken): Promise { - // Create a temporary notebook for this session. - this.notebookFiles.push(await contentsManager.newUntitled({ type: 'notebook' })); + // Create a temporary notebook for this session. Give it a unique name, so there is no possiblity of us reusing a session lingering the jupyter server + // (sessions in jupyter are indexed by path, regardless if the file exists of not and regardless of the session name, so deleting the file later doesn't save us) + // See https://github.com/jupyter/notebook/blob/5.7.x/notebook/services/sessions/handlers.py#L65 + const sessionUUID = uuid(); + const nbFile = await contentsManager.newUntitled({ type: 'notebook' }); + this.notebookFiles.push(await contentsManager.rename(nbFile.path, `.vscode-jupyter-session-${sessionUUID}.ipynb`)); - // Create our session options using this temporary notebook and our connection info const options: Session.IOptions = { path: this.notebookFiles[this.notebookFiles.length - 1].path, kernelName: this.kernelSpec ? this.kernelSpec.name : '', - name: uuid(), // This is crucial to distinguish this session from any other. + name: sessionUUID, // This is crucial to distinguish this session from any other. serverSettings: serverSettings }; + if (this.kernelSpec && this.kernelSpec.id) { + options.kernelId = this.kernelSpec.id; + traceInfo(`Connecting to existing kernel ${this.kernelSpec.id}`); + } else { + traceInfo('Creating new kernel for connection'); + } + + // Create our session options using this temporary notebook and our connection info + return Cancellation.race(() => this.sessionManager!.startNew(options), cancelToken); } @@ -347,10 +365,14 @@ export class JupyterSession implements IJupyterSession { }); } } - await waitForPromise(session.shutdown(), 1000); + if (this.connInfo && this.connInfo.allowShutdown) { + await waitForPromise(session.shutdown(), 1000); + } } else { - // Shutdown may fail if the process has been killed - await waitForPromise(session.shutdown(), 1000); + if (this.connInfo && this.connInfo.allowShutdown) { + // Shutdown may fail if the process has been killed + await waitForPromise(session.shutdown(), 1000); + } } } catch { noop(); diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts index b36053bb1418..1210d4b5ee71 100644 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { ServerConnection, SessionManager } from '@jupyterlab/services'; +import { Kernel, ServerConnection, SessionManager } from '@jupyterlab/services'; import { inject, injectable } from 'inversify'; import { CancellationToken } from 'vscode-jsonrpc'; +// import { IConfigurationService } from '../../common/types'; import { IConnection, IJupyterKernelSpec, IJupyterPasswordConnect, IJupyterSession, IJupyterSessionManager } from '../types'; import { JupyterKernelSpec } from './jupyterKernelSpec'; import { JupyterSession } from './jupyterSession'; @@ -13,10 +14,15 @@ import { JupyterSession } from './jupyterSession'; export class JupyterSessionManager implements IJupyterSessionManager { constructor( @inject(IJupyterPasswordConnect) private jupyterPasswordConnect: IJupyterPasswordConnect - ) {} + // @inject(IConfigurationService) private readonly configurationService: IConfigurationService + ) { } - public async startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken) : Promise { + public async startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise { // Create a new session and attempt to connect to it + // const settings = this.configurationService.getSettings(); + // const allowShutdown = settings.datascience.jupyterServerAllowKernelShutdown; + // const kernelId = settings.datascience.jupyterServerKernelId; + // const session = new JupyterSession(connInfo, kernelSpec, this.jupyterPasswordConnect, kernelId, allowShutdown); const session = new JupyterSession(connInfo, kernelSpec, this.jupyterPasswordConnect); try { await session.connect(cancelToken); @@ -28,19 +34,15 @@ export class JupyterSessionManager implements IJupyterSessionManager { return session; } - public async getActiveKernelSpecs(connection: IConnection) : Promise { - let sessionManager: SessionManager | undefined ; + public getActiveKernels(connection: IConnection): Promise { + return Kernel.listRunning(this.makeServerSettings(connection)); + } + + public async getActiveKernelSpecs(connection: IConnection): Promise { + let sessionManager: SessionManager | undefined; try { // Use our connection to create a session manager - const serverSettings = ServerConnection.makeSettings( - { - baseUrl: connection.baseUrl, - token: connection.token, - pageUrl: '', - // A web socket is required to allow token authentication (what if there is no token authentication?) - wsUrl: connection.baseUrl.replace('http', 'ws'), - init: { cache: 'no-store', credentials: 'same-origin' } - }); + const serverSettings = this.makeServerSettings(connection); sessionManager = new SessionManager({ serverSettings: serverSettings }); // Ask the session manager to refresh its list of kernel specs. @@ -65,4 +67,16 @@ export class JupyterSessionManager implements IJupyterSessionManager { } + private makeServerSettings(connection: IConnection): ServerConnection.ISettings { + return ServerConnection.makeSettings( + { + baseUrl: connection.baseUrl, + token: connection.token, + pageUrl: '', + // A web socket is required to allow token authentication (what if there is no token authentication?) + wsUrl: connection.baseUrl.replace('http', 'ws'), + init: { cache: 'no-store', credentials: 'same-origin' } + }); + } + } diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts index 517eaa655569..9f2361142150 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts @@ -3,20 +3,26 @@ 'use strict'; import { CancellationToken } from 'vscode-jsonrpc'; +import { Kernel } from '@jupyterlab/services'; import { noop } from '../../../../test/core'; import { IConnection, IJupyterKernelSpec, IJupyterSession, IJupyterSessionManager } from '../../types'; export class GuestJupyterSessionManager implements IJupyterSessionManager { - public constructor(private realSessionManager : IJupyterSessionManager) { + public constructor(private realSessionManager: IJupyterSessionManager) { noop(); } - public startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken) : Promise { + public startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise { return this.realSessionManager.startNew(connInfo, kernelSpec, cancelToken); } - public async getActiveKernelSpecs(_connection: IConnection) : Promise { + public async getActiveKernels(_connection: IConnection): Promise { + // Don't return any kernels in guest mode. They're only needed for the host side + return Promise.resolve([]); + } + + public async getActiveKernelSpecs(_connection: IConnection): Promise { // Don't return any kernel specs in guest mode. They're only needed for the host side return Promise.resolve([]); } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 14591afabafe..dd431308c1dd 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -5,7 +5,7 @@ import { nbformat } from '@jupyterlab/coreutils'; import { Kernel, KernelMessage } from '@jupyterlab/services/lib/kernel'; import { JSONObject } from '@phosphor/coreutils'; import { Observable } from 'rxjs/Observable'; -import { CancellationToken, CodeLens, CodeLensProvider, DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, Disposable, Event, Range, TextDocument, TextEditor } from 'vscode'; +import { CancellationToken, CodeLens, CodeLensProvider, DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, Disposable, Event, QuickPickItem, Range, TextDocument, TextEditor } from 'vscode'; import { ICommandManager } from '../common/application/types'; import { ExecutionResult, ObservableExecutionResult, SpawnOptions } from '../common/process/types'; @@ -34,6 +34,7 @@ export interface IConnection extends Disposable { localProcExitCode: number | undefined; disconnected: Event; allowUnauthorized?: boolean; + allowShutdown?: boolean; } export enum InterruptResult { @@ -148,12 +149,35 @@ export const IJupyterSessionManager = Symbol('IJupyterSessionManager'); export interface IJupyterSessionManager { startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise; getActiveKernelSpecs(connInfo: IConnection): Promise; + getActiveKernels(connInfo: IConnection): Promise; } export interface IJupyterKernelSpec extends IAsyncDisposable { name: string | undefined; language: string | undefined; path: string | undefined; + id: string | undefined; +} + +export interface IKernelQuickPickItem extends QuickPickItem { + name: string | undefined; + kernelId: string | undefined; +} + +export interface IJupyterServerQuickPickItem extends QuickPickItem { + hostName: string | undefined; + uri: string; +} + +export interface IJupyterShutdown { + label: string; + picked: boolean; + keepRunning: boolean; +} + +export interface IJupyterServer { + hostName: string; + uri: string; } export const INotebookImporter = Symbol('INotebookImporter'); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 9e70f61d7c9d..f3ca42733a03 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1272,6 +1272,8 @@ export interface IEventNamePropertyMapping { [Telemetry.SelfCertsMessageClose]: never | undefined; [Telemetry.SelfCertsMessageEnabled]: never | undefined; [Telemetry.SelectJupyterURI]: never | undefined; + [Telemetry.JupyterKernelSpecified]: never | undefined; + [Telemetry.JupyterKernelAutoShutdown]: { autoShutdownEnabled: boolean }; [Telemetry.SetJupyterURIToLocal]: never | undefined; [Telemetry.SetJupyterURIToUserSpecified]: never | undefined; [Telemetry.ShiftEnterBannerShown]: never | undefined; diff --git a/src/datascience-ui/react-common/settingsReactSide.ts b/src/datascience-ui/react-common/settingsReactSide.ts index e7c81bff10c6..a9959e8202ed 100644 --- a/src/datascience-ui/react-common/settingsReactSide.ts +++ b/src/datascience-ui/react-common/settingsReactSide.ts @@ -36,6 +36,10 @@ function load() { jupyterLaunchTimeout: 10, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/color.test.ts b/src/test/datascience/color.test.ts index 1088dd868f46..697ecda3e8ca 100644 --- a/src/test/datascience/color.test.ts +++ b/src/test/datascience/color.test.ts @@ -53,6 +53,10 @@ suite('Theme colors', () => { jupyterLaunchTimeout: 20000, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 4aec10054698..163c9bdca7c0 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -407,6 +407,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { jupyterLaunchTimeout: 20000, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 716246c00b1c..cfcddb9a1aae 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -74,6 +74,10 @@ suite('DataScience Code Watcher Unit Tests', () => { jupyterLaunchTimeout: 20000, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index c4c3d60738c3..63942882c382 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -540,6 +540,10 @@ suite('Jupyter Execution', async () => { jupyterLaunchTimeout: 10, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts index 76653d3b7e34..679e4557911a 100644 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -133,6 +133,10 @@ suite('Interactive window command listener', async () => { jupyterLaunchTimeout: 10, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: '', changeDirOnImportExport: true, notebookFileRoot: 'WORKSPACE', diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx index c018277e6091..a1fba53218a7 100644 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ b/src/test/datascience/interactiveWindowTestHelpers.tsx @@ -307,6 +307,10 @@ export function defaultDataScienceSettings(): IDataScienceSettings { jupyterLaunchTimeout: 10, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', + jupyterServerKernelSpec: undefined, + jupyterServers: undefined, jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true, diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index a8b967665899..bf45dc4cd8ee 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -12,6 +12,7 @@ import * as uuid from 'uuid/v4'; import { EventEmitter } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; +import { Kernel } from '@jupyterlab/services'; import { Cancellation } from '../../client/common/cancellation'; import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory, Output } from '../../client/common/process/types'; import { IAsyncDisposableRegistry, IConfigurationService } from '../../client/common/types'; @@ -236,6 +237,10 @@ export class MockJupyterManager implements IJupyterSessionManager { } } + public getActiveKernels(_connection: IConnection): Promise { + return Promise.resolve([]); + } + public getActiveKernelSpecs(_connection: IConnection): Promise { return Promise.resolve([]); } diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts index c4152c58dc6d..5d6bc7b4ef2b 100644 --- a/src/test/datascience/reactHelpers.ts +++ b/src/test/datascience/reactHelpers.ts @@ -349,6 +349,8 @@ export function setUpDomEnvironment() { jupyterLaunchTimeout: 10, jupyterLaunchRetries: 3, enabled: true, + jupyterServerAllowKernelShutdown: true, + jupyterServerKernelId: '', jupyterServerURI: 'local', notebookFileRoot: 'WORKSPACE', changeDirOnImportExport: true,