Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify remote Jupyter Sever connections upon connecting to remote kernels #9941

Merged
merged 15 commits into from
May 9, 2022
1 change: 1 addition & 0 deletions news/2 Fixes/8043.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate remote Jupyter Server connections when attempting to start a kernel.
1 change: 1 addition & 0 deletions news/2 Fixes/9167.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Notify failures in connection to remote Jupyter Server only when connecting to those kernels.
10 changes: 8 additions & 2 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"DataScience.openExportFileYes": "Yes",
"DataScience.openExportFileNo": "No",
"DataScience.failedExportMessage": "Export failed.",
"DataScience.validationErrorMessageForRemoteUrlProtocolNeedsToBeHttpOrHttps": "Has to be http(s)",
"DataScience.exportFailedGeneralMessage": "Please check the 'Jupyter' [output](command:jupyter.viewOutput) panel for further details.",
"DataScience.exportToPDFDependencyMessage": "If you have not installed xelatex (TeX) you will need to do so before you can export to PDF, for further instructions go to https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex. \r\nTo avoid installing xelatex (TeX) you might want to try exporting to HTML and using your browsers \"Print to PDF\" feature.",
"DataScience.insecureSessionMessage": "Connecting over HTTP without a token may be an insecure connection. Do you want to connect to a possibly insecure server?",
Expand Down Expand Up @@ -183,10 +184,14 @@
"DataScience.jupyterNotSupported": "Jupyter cannot be started. Error attempting to locate jupyter: {0}",
"DataScience.jupyterNotSupportedBecauseOfEnvironment": "Activating {0} to run Jupyter failed with {1}.",
"DataScience.jupyterNbConvertNotSupported": "Importing notebooks requires Jupyter nbconvert to be installed.",
"DataScience.removeRemoteJupyterConnectionButtonText": "Forget Connection",
"DataScience.changeRemoteJupyterConnectionButtonText": "Manage Connections",
"DataScience.jupyterLaunchNoURL": "Failed to find the URL of the launched Jupyter notebook server",
"DataScience.jupyterLaunchTimedOut": "The Jupyter notebook server failed to launch in time",
"DataScience.jupyterSelfCertFail": "The security certificate used by server {0} was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?",
"DataScience.jupyterExpiredCertFail": "The security certificate used by server {0} has expired.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?",
"DataScience.jupyterSelfCertFailErrorMessageOnly": "The security certificate used by server was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.",
"DataScience.jupyterSelfCertExpiredErrorMessageOnly":"The security certificate used by server has expired.\r\nThis may indicate an attempt to steal your information.",
"DataScience.jupyterSelfCertEnable": "Yes, connect anyways",
"DataScience.jupyterSelfCertClose": "No, close the connection",
"DataScience.jupyterServerCrashed": "Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}",
Expand Down Expand Up @@ -283,8 +288,9 @@
"DataScience.jupyterSelectPasswordPrompt": "Enter your password",
"DataScience.removeRemoteJupyterServerEntryInQuickPick": "Remove",
"DataScience.jupyterNotebookFailure": "Jupyter notebook failed to launch. \r\n{0}",
"DataScience.jupyterNotebookConnectFailed": "Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}",
"DataScience.jupyterNotebookRemoteConnectFailed": "Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}",
"DataScience.remoteJupyterConnectionFailedWithServer": "Failed to connect to the remote Jupyter Server {0}. Verify the server is running and reachable.",
"DataScience.remoteJupyterConnectionFailedWithServerWithError":"Failed to connect to the remote Jupyter Server {0}. Verify the server is running and reachable. ({1}).",
"DataScience.remoteJupyterConnectionFailedWithoutServerWithError": "Connection failure. Verify the server is running and reachable. ({0}).",
"DataScience.jupyterNotebookRemoteConnectSelfCertsFailed": "Failed to connect to remote Jupyter notebook.\r\nSpecified server is using self signed certs. Enable Allow Unauthorized Remote Connection setting to connect anyways\r\n{0}\r\n{1}",
"DataScience.jupyterRemoteConnectFailedModalMessage": "Failed to connect to the remote Jupyter Server. View Jupyter log for further details.",
"DataScience.changeJupyterRemoteConnection": "Change Jupyter Server connection.",
Expand Down
1 change: 0 additions & 1 deletion package.nls.nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"DataScience.pythonVersionHeader": "Python versie:",
"DataScience.pythonRestartHeader": "Kernel herstart:",
"DataScience.jupyterNotebookFailure": "Jupyter-notebook kon niet starten. \r\n{0}",
"DataScience.jupyterNotebookConnectFailed": "Verbinden met Jupiter-notebook is niet gelukt. \r\n{0}\r\n{1}",
"DataScience.notebookVersionFormat": "Jupyter-notebook versie: {0}",
"DataScience.jupyterKernelSpecNotFound": "Kan geen Jupyter-kernel-spec aanmaken en er zijn er geen beschikbaar voor gebruik",
"DataScience.interruptKernel": "IPython-kernel onderbreken",
Expand Down
2 changes: 0 additions & 2 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,6 @@
"DataScience.jupyterSelectUserPrompt": "请输入用户名",
"DataScience.jupyterSelectPasswordPrompt": "请输入密码",
"DataScience.jupyterNotebookFailure": "Jupyter 笔记本启动失败。\r\n{0}",
"DataScience.jupyterNotebookConnectFailed": "连接 Jupyter 笔记本失败。\r\n{0}\r\n{1}",
"DataScience.jupyterNotebookRemoteConnectFailed": "连接远程 Jupyter 笔记本失败。\r\n请检查 Jupyter 服务器 URI 设置是否指定了有效的运行服务器。\r\n{0}\r\n{1}",
"DataScience.jupyterNotebookRemoteConnectSelfCertsFailed": "连接远程 Jupyter 笔记本失败。\r\n指定的服务器正在使用自签名的证书。启用 Allow Unauthorized Remote Connection 设置以连接\r\n{0}\r\n{1}",
"DataScience.notebookVersionFormat": "Jupyter 笔记本版本:{0}",
"DataScience.jupyterKernelSpecNotFound": "无法创建 Jupyter kernel spec,没有可用的内核",
Expand Down
3 changes: 2 additions & 1 deletion src/kernels/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,8 @@ export function removeNotebookSuffixAddedByExtension(notebookPath: string) {
.substring(notebookPath.lastIndexOf(jvscIdentifier) + jvscIdentifier.length)
.search(guidRegEx) !== -1
) {
return notebookPath.substring(0, notebookPath.lastIndexOf(jvscIdentifier));
const nbFile = notebookPath.substring(0, notebookPath.lastIndexOf(jvscIdentifier));
return nbFile.toLowerCase().endsWith('.ipynb') ? nbFile : `${nbFile}.ipynb`;
}
}
return notebookPath;
Expand Down
4 changes: 2 additions & 2 deletions src/kernels/jupyter/commands/serverSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ICommandManager } from '../../../platform/common/application/types';
import { Commands } from '../../../platform/common/constants';
import { IDisposable } from '../../../platform/common/types';
import { traceInfo } from '../../../platform/logging';
import { JupyterServerSelector } from '../serverSelector';
import { JupyterServerSelector, SelectJupyterUriCommandSource } from '../serverSelector';
import { IJupyterServerUriStorage } from '../types';

@injectable()
Expand All @@ -36,7 +36,7 @@ export class JupyterServerSelectorCommand implements IDisposable {

private async selectJupyterUri(
local: boolean = true,
source: Uri | 'nativeNotebookStatusBar' | 'commandPalette' | 'toolbar' = 'commandPalette',
source: Uri | SelectJupyterUriCommandSource = 'commandPalette',
notebook: NotebookDocument | undefined
): Promise<undefined | string> {
if (source instanceof Uri) {
Expand Down
19 changes: 7 additions & 12 deletions src/kernels/jupyter/jupyterConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,25 @@ export class JupyterConnection implements IExtensionSyncActivationService {
this.pendingTimeouts.forEach((t) => clearTimeout(t as any));
this.pendingTimeouts = [];
}
public createConnectionInfo(uri: string) {
public async createConnectionInfo(uri: string) {
// Prepare our map of server URIs
await this.updateServerUri(uri);
return createRemoteConnectionInfo(uri, this.getServerUri.bind(this));
}
public async validateRemoteUri(uri: string): Promise<void> {
// Prepare our map of server URIs (needed in order to retrieve the uri during the connection)
await this.updateServerUri(uri);

// Create an active connection.
return this.validateRemoteConnection(await createRemoteConnectionInfo(uri, this.getServerUri.bind(this)));
return this.validateRemoteConnection(await this.createConnectionInfo(uri));
}

public async validateRemoteConnection(connection: IJupyterConnection): Promise<void> {
private async validateRemoteConnection(connection: IJupyterConnection): Promise<void> {
let sessionManager: IJupyterSessionManager | undefined = undefined;
try {
// Attempt to list the running kernels. It will return empty if there are none, but will
// throw if can't connect.
sessionManager = await this.jupyterSessionManagerFactory.create(connection, false);
await Promise.all([sessionManager.getRunningKernels(), sessionManager.getKernelSpecs]);

await Promise.all([sessionManager.getRunningKernels(), sessionManager.getKernelSpecs()]);
// We should throw an exception if any of that fails.
} finally {
if (connection) {
connection.dispose();
}
connection.dispose();
if (sessionManager) {
void sessionManager.dispose();
}
Expand Down
1 change: 1 addition & 0 deletions src/kernels/jupyter/jupyterUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function handleSelfCertsError(
const closeOption: string = DataScience.jupyterSelfCertClose();
const value = await appShell.showErrorMessage(
DataScience.jupyterSelfCertFail().format(message),
{ modal: true },
enableOption,
closeOption
);
Expand Down
24 changes: 8 additions & 16 deletions src/kernels/jupyter/launcher/jupyterExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as uuid from 'uuid/v4';
import { CancellationToken, Uri } from 'vscode';
import { IWorkspaceService } from '../../../platform/common/application/types';
import { Cancellation } from '../../../platform/common/cancellation';
import { WrappedError } from '../../../platform/errors/types';
import { traceInfo } from '../../../platform/logging';
import { IDisposableRegistry, IConfigurationService, Resource } from '../../../platform/common/types';
import { DataScience } from '../../../platform/common/utils/localize';
Expand All @@ -26,8 +25,10 @@ import {
INotebookServerFactory
} from '../types';
import { IJupyterSubCommandExecutionService } from '../types.node';
import { JupyterExpiredCertsError } from '../../../platform/errors/jupyterExpiredCertsError';
import { JupyterConnection } from '../jupyterConnection';
import { RemoteJupyterServerConnectionError } from '../../../platform/errors/remoteJupyterServerConnectionError';
import { LocalJupyterServerConnectionError } from '../../../platform/errors/localJupyterServerConnectionError';
import { JupyterSelfCertsExpiredError } from '../../../platform/errors/jupyterSelfCertsExpiredError';

const LocalHosts = ['localhost', '127.0.0.1', '::1'];

Expand Down Expand Up @@ -153,24 +154,18 @@ export class JupyterExecutionBase implements IJupyterExecution {
sendTelemetryEvent(Telemetry.ConnectRemoteFailedJupyter, undefined, undefined, err, true);

// Check for the self signed certs error specifically
if (err.message.indexOf('reason: self signed certificate') >= 0) {
if (JupyterSelfCertsError.isSelfCertsError(err)) {
sendTelemetryEvent(Telemetry.ConnectRemoteSelfCertFailedJupyter);
throw new JupyterSelfCertsError(connection.baseUrl);
} else if (err.message.indexOf('reason: certificate has expired') >= 0) {
} else if (JupyterSelfCertsExpiredError.isSelfCertsExpiredError(err)) {
sendTelemetryEvent(Telemetry.ConnectRemoteExpiredCertFailedJupyter);
throw new JupyterExpiredCertsError(connection.baseUrl);
throw new JupyterSelfCertsExpiredError(connection.baseUrl);
} else {
throw WrappedError.from(
DataScience.jupyterNotebookRemoteConnectFailed().format(connection.baseUrl, err),
err
);
throw new RemoteJupyterServerConnectionError(connection.baseUrl, options.serverId, err);
}
} else {
sendTelemetryEvent(Telemetry.ConnectFailedJupyter, undefined, undefined, err, true);
throw WrappedError.from(
DataScience.jupyterNotebookConnectFailed().format(connection.baseUrl, err),
err
);
throw new LocalJupyterServerConnectionError(err);
}
} else {
throw err;
Expand Down Expand Up @@ -214,9 +209,6 @@ export class JupyterExecutionBase implements IJupyterExecution {
cancelToken
);
} else {
// Prepare our map of server URIs
await this.jupyterConnection.updateServerUri(options.uri);

// If we have a URI spec up a connection info for it
return this.jupyterConnection.createConnectionInfo(options.uri);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cerateConnectionInfo will ensure the server Uri has been updated, calling code need to know about such internals

}
Expand Down
17 changes: 17 additions & 0 deletions src/kernels/jupyter/launcher/liveshare/hostJupyterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { Cancellation } from '../../../../platform/common/cancellation';
import { getDisplayPath } from '../../../../platform/common/platform/fs-paths';
import { INotebookServer } from '../../types';
import { Uri } from 'vscode';
import { RemoteJupyterServerConnectionError } from '../../../../platform/errors/remoteJupyterServerConnectionError';
/* eslint-disable @typescript-eslint/no-explicit-any */

export class HostJupyterServer implements INotebookServer {
Expand Down Expand Up @@ -150,6 +151,22 @@ export class HostJupyterServer implements INotebookServer {
if (!this.sessionManager || this.isDisposed) {
throw new SessionDisposedError();
}
if (
this.sessionManager &&
!this.isDisposed &&
(kernelConnection.kind === 'connectToLiveRemoteKernel' ||
kernelConnection.kind === 'startUsingRemoteKernelSpec')
) {
try {
await Promise.all([this.sessionManager.getRunningKernels(), this.sessionManager.getKernelSpecs()]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure we try to fetch the kernels (validate the remote jupyter server connection) before creating a session.
This should also fix #8043

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as the current issue #9167

} catch (ex) {
traceError(
'Failed to fetch running kernels from remote server, connection may be outdated or remote server may be unreachable',
ex
);
throw new RemoteJupyterServerConnectionError(kernelConnection.baseUrl, kernelConnection.serverId, ex);
}
}
const stopWatch = new StopWatch();
// Create a session and return it.
try {
Expand Down
1 change: 1 addition & 0 deletions src/kernels/jupyter/launcher/liveshare/serverCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class ServerCache implements IAsyncDisposable {
};
}
return {
serverId: options.serverId,
uri: options.uri,
resource: options?.resource,
ui: options.ui,
Expand Down
1 change: 1 addition & 0 deletions src/kernels/jupyter/launcher/notebookServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export class NotebookServerProvider implements IJupyterServerProvider {

return {
uri,
serverId: options.serverId,
resource: options.resource,
ui: this.ui,
localJupyter: false
Expand Down
13 changes: 11 additions & 2 deletions src/kernels/jupyter/launcher/serverUriStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage {
public get onDidChangeUri() {
return this._onDidChangeUri.event;
}
private _onDidRemoveUri = new EventEmitter<string>();
public get onDidRemoveUri() {
return this._onDidRemoveUri.event;
}
constructor(
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(ICryptoUtils) private readonly crypto: ICryptoUtils,
Expand All @@ -45,18 +49,23 @@ export class JupyterServerUriStorage implements IJupyterServerUriStorage {
return f.uri !== uri && i < Settings.JupyterServerUriListMax - 1;
});

// Add this entry into the liast
// Add this entry into the last.
editedList.push({ uri, time, displayName: displayName || uri });

return this.updateMemento(editedList);
}
public async removeUri(uri: string) {
const activeUri = await this.getUri();
// Start with saved list.
const uriList = await this.getSavedUriList();

// Remove this uri if already found (going to add again with a new time)
const editedList = uriList.filter((f) => f.uri !== uri);
return this.updateMemento(editedList);
await this.updateMemento(editedList);
if (activeUri === uri) {
await this.setUri(Settings.JupyterServerLocalLaunch);
}
this._onDidRemoveUri.fire(uri);
}
private async updateMemento(editedList: { uri: string; time: number; displayName?: string | undefined }[]) {
// Sort based on time. Newest time first
Expand Down
2 changes: 1 addition & 1 deletion src/kernels/jupyter/remoteKernelFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class RemoteKernelFinder implements IRemoteKernelFinder {
): Promise<KernelConnectionMetadata[]> {
// Get a jupyter session manager to talk to
let sessionManager: IJupyterSessionManager | undefined;

// This should only be used when doing remote.
if (connInfo.type === 'jupyter') {
try {
Expand Down Expand Up @@ -113,6 +112,7 @@ export class RemoteKernelFinder implements IRemoteKernelFinder {
return items;
} catch (ex) {
traceError(`Error fetching remote kernels:`, ex);
throw ex;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we'd swallow exceptions and return an empty list, now we throw the error and higher up when we have errors we just fallback to the cache (this behaviour is documented in the calling code)

} finally {
if (sessionManager) {
await sessionManager.dispose();
Expand Down
Loading