Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/3763.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to select remote Jupyter kernel on remote jupyter hosts - @hochshi
1 change: 1 addition & 0 deletions news/1 Enhancements/7014.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to reconnect to existing Jupyter kernel on remote jupyter hosts - @kdkavanagh
26 changes: 26 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}",
Expand Down
4 changes: 4 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}');
Expand Down
2 changes: 2 additions & 0 deletions src/client/datascience/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
210 changes: 194 additions & 16 deletions src/client/datascience/datascience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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>(IDataScienceCommandListener);
this.dataScienceSurveyBanner = this.serviceContainer.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_DS_SURVEY);
Expand Down Expand Up @@ -217,19 +255,53 @@ export class DataScience implements IDataScience {

@captureTelemetry(Telemetry.SelectJupyterURI)
public async selectJupyterURI(): Promise<void> {
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<void> {
Expand Down Expand Up @@ -277,19 +349,125 @@ export class DataScience implements IDataScience {
@captureTelemetry(Telemetry.SetJupyterURIToLocal)
private async setJupyterURIToLocal(): Promise<void> {
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<IKernelQuickPickItem[]> {
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<IKernelQuickPickItem | undefined> {
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<void> {
// 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<IKernelQuickPickItem | undefined> {
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<IConnection> {
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<void> {

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)
Expand Down
Loading