Skip to content

Commit

Permalink
Display errors in cells when we run into kernel issues (#9297)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored Mar 9, 2022
1 parent 7b38613 commit 98f4b05
Show file tree
Hide file tree
Showing 35 changed files with 427 additions and 246 deletions.
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"CommonSurvey.remindMeLaterLabel": "Remind me later",
"CommonSurvey.yesLabel": "Yes, take survey now",
"CommonSurvey.noLabel": "No, thanks",
"Common.clickHereForMoreInfoWithHtml": "Click <a href='{0}'>here</a> for more info.",
"OutputChannelNames.languageServer": "Python Language Server",
"OutputChannelNames.python": "Python",
"OutputChannelNames.pythonTest": "Python Test Log",
Expand Down Expand Up @@ -261,7 +262,6 @@
"DataScience.restartingKernelHeader": "_Restarting kernel..._",
"DataScience.startedNewKernelHeader": "Started '{0}' kernel",
"DataScience.connectKernelHeader": "Connected to '{0}' kernel",
"DataScience.canceledKernelHeader": "Canceled connection to '{0}' kernel",
"DataScience.executingCodeFailure": "Executing code failed : {0}",
"DataScience.inputWatermark": "Type code here and press shift-enter to run",
"DataScience.deleteButtonTooltip": "Remove cell",
Expand Down
3 changes: 1 addition & 2 deletions src/client/common/errors/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ function isBuiltInModuleOverwritten(
};
}

export async function displayErrorsInCell(cell: NotebookCell, execution: NotebookCellExecution, errorMessage: string) {
export function displayErrorsInCell(cell: NotebookCell, execution: NotebookCellExecution, errorMessage: string) {
if (!errorMessage) {
return;
}
Expand All @@ -593,5 +593,4 @@ export async function displayErrorsInCell(cell: NotebookCell, execution: Noteboo
})
]);
void execution.appendOutput(output);
execution.end(false);
}
21 changes: 21 additions & 0 deletions src/client/common/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import { EOL } from 'os';
import { KernelConnectionMetadata } from '../../datascience/jupyter/kernels/types';

export abstract class BaseError extends Error {
public stdErr?: string;
Expand All @@ -10,6 +11,17 @@ export abstract class BaseError extends Error {
}
}

export abstract class BaseKernelError extends BaseError {
public stdErr?: string;
constructor(
category: ErrorCategory,
message: string,
public readonly kernelConnectionMetadata: KernelConnectionMetadata
) {
super(category, message);
}
}

/**
* Wraps an error with a custom error message, retaining the call stack information.
*/
Expand Down Expand Up @@ -44,6 +56,15 @@ export class WrappedError extends BaseError {
return err;
}
}
export class WrappedKernelError extends WrappedError {
constructor(
message: string,
originalException: Error | undefined,
public readonly kernelConnectionMetadata: KernelConnectionMetadata
) {
super(message, originalException);
}
}

export function getErrorCategory(error?: Error): ErrorCategory {
if (!error) {
Expand Down
9 changes: 4 additions & 5 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export namespace Common {
export const learnMore = localize('Common.learnMore', 'Learn more');
export const and = localize('Common.and', 'and');
export const reportThisIssue = localize('Common.reportThisIssue', 'Report this issue');
export const clickHereForMoreInfoWithHtml = localize(
'Common.clickHereForMoreInfoWithHtml',
"Click <a href='{0}'>here</a> for more info."
);
}

export namespace CommonSurvey {
Expand Down Expand Up @@ -453,7 +457,6 @@ export namespace DataScience {
export const startingNewKernelHeader = localize('DataScience.kernelStarting', '_Connecting to kernel..._');
export const startingNewKernelCustomHeader = localize('DataScience.kernelStartingCustom', '_Connecting to {0}..._');
export const connectKernelHeader = localize('DataScience.connectKernelHeader', 'Connected to {0}');
export const canceledKernelHeader = localize('DataScience.canceledKernelHeader', 'Canceled connection to {0}');

export const jupyterSelectURIPrompt = localize(
'DataScience.jupyterSelectURIPrompt',
Expand Down Expand Up @@ -978,10 +981,6 @@ export namespace DataScience {
'DataScience.ipykernelNotInstalled',
'IPyKernel not installed into interpreter {0}'
);
export const ipykernelNotInstalledBecauseCanceled = localize(
'DataScience.ipykernelNotInstalledBecauseCanceled',
'IPyKernel not installed into interpreter. Installation canceled.'
);
export const needIpykernel6 = localize('DataScience.needIpykernel6', 'Ipykernel setup required for this feature');
export const setup = localize('DataScience.setup', 'Setup');
export const startingRunByLine = localize('DataScience.startingRunByLine', 'Starting Run by Line');
Expand Down
5 changes: 2 additions & 3 deletions src/client/datascience/baseJupyterSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ export abstract class BaseJupyterSession implements IJupyterSession {
if (session.kernel.status == 'idle') {
deferred.resolve(session.kernel.status);
}

const result = await Promise.race([deferred.promise, sleep(timeout)]);
session.kernel.statusChanged?.disconnect(handler);
traceInfo(`Finished waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`);
Expand All @@ -313,12 +312,12 @@ export abstract class BaseJupyterSession implements IJupyterSession {
);
// If we throw an exception, make sure to shutdown the session as it's not usable anymore
this.shutdownSession(session, this.statusHandler, isRestartSession).ignoreErrors();
throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut());
throw new JupyterWaitForIdleError(this.kernelConnectionMetadata);
} finally {
progress?.dispose();
}
} else {
throw new JupyterInvalidKernelError(undefined);
throw new JupyterInvalidKernelError(this.kernelConnectionMetadata);
}
}

Expand Down
161 changes: 151 additions & 10 deletions src/client/datascience/errors/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type * as nbformat from '@jupyterlab/nbformat';
import { inject, injectable } from 'inversify';
import { IApplicationShell, IWorkspaceService } from '../../common/application/types';
import { WrappedError } from '../../common/errors/types';
import { BaseError, BaseKernelError, WrappedError, WrappedKernelError } from '../../common/errors/types';
import { traceWarning } from '../../common/logger';
import { Common, DataScience } from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
Expand All @@ -17,22 +17,34 @@ import {
IKernelDependencyService,
KernelInterpreterDependencyResponse
} from '../types';
import { CancellationError as VscCancellationError, CancellationTokenSource, ConfigurationTarget } from 'vscode';
import {
CancellationError as VscCancellationError,
CancellationTokenSource,
ConfigurationTarget,
workspace
} from 'vscode';
import { CancellationError } from '../../common/cancellation';
import { KernelConnectionTimeoutError } from './kernelConnectionTimeoutError';
import { KernelDiedError } from './kernelDiedError';
import { KernelPortNotUsedTimeoutError } from './kernelPortNotUsedTimeoutError';
import { KernelProcessExitedError } from './kernelProcessExitedError';
import { PythonKernelDiedError } from './pythonKernelDiedError';
import { analyzeKernelErrors, getErrorMessageFromPythonTraceback } from '../../common/errors/errorUtils';
import {
analyzeKernelErrors,
getErrorMessageFromPythonTraceback,
KernelFailureReason
} from '../../common/errors/errorUtils';
import { KernelConnectionMetadata } from '../jupyter/kernels/types';
import { IBrowserService, IConfigurationService, Resource } from '../../common/types';
import { Commands, Telemetry } from '../constants';
import { sendTelemetryEvent } from '../../telemetry';
import { JupyterConnectError } from './jupyterConnectError';
import { JupyterKernelDependencyError } from '../jupyter/kernels/jupyterKernelDependencyError';
import { JupyterInterpreterDependencyResponse } from '../jupyter/interpreter/jupyterInterpreterDependencyService';
import { DisplayOptions } from '../displayOptions';
import { JupyterKernelDependencyError } from './jupyterKernelDependencyError';
import { EnvironmentType } from '../../pythonEnvironments/info';
import { translateProductToModule } from '../../../kernels/installer/moduleInstaller';
import { ProductNames } from '../../../kernels/installer/productNames';
import { Product } from '../../../kernels/installer/types';

@injectable()
export class DataScienceErrorHandler implements IDataScienceErrorHandler {
Expand Down Expand Up @@ -77,12 +89,10 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
} else if (
err instanceof KernelDiedError ||
err instanceof KernelProcessExitedError ||
err instanceof PythonKernelDiedError ||
err instanceof JupyterConnectError
) {
const defaultErrorMessage = getCombinedErrorMessage(
// PythonKernelDiedError has an `errorMessage` property, use that over `err.stdErr` for user facing error messages.
'errorMessage' in err ? err.errorMessage : getErrorMessageFromPythonTraceback(err.stdErr) || err.stdErr
getErrorMessageFromPythonTraceback(err.stdErr) || err.stdErr
);
this.applicationShell.showErrorMessage(defaultErrorMessage).then(noop, noop);
} else {
Expand All @@ -91,7 +101,58 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
this.applicationShell.showErrorMessage(message).then(noop, noop);
}
}

public async getErrorMessageForDisplayInCell(error: Error) {
let message: string = error.message;
error = WrappedError.unwrap(error);
if (error instanceof JupyterKernelDependencyError) {
message = getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || message;
} else if (error instanceof JupyterInstallError) {
message = getJupyterMissingErrorMessageForCell(error) || message;
} else if (error instanceof VscCancellationError || error instanceof CancellationError) {
// Don't show the message for cancellation errors
traceWarning(`Cancelled by user`, error);
return '';
} else if (
error instanceof KernelDiedError &&
(error.kernelConnectionMetadata.kind === 'startUsingLocalKernelSpec' ||
error.kernelConnectionMetadata.kind === 'startUsingPythonInterpreter') &&
error.kernelConnectionMetadata.interpreter &&
!(await this.kernelDependency.areDependenciesInstalled(error.kernelConnectionMetadata, undefined, true))
) {
// We don't look for ipykernel dependencies before we start a kernel, hence
// its possible the kernel failed to start due to missing dependencies.
message = getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || message;
} else if (error instanceof BaseKernelError || error instanceof WrappedKernelError) {
const failureInfo = analyzeKernelErrors(
workspace.workspaceFolders || [],
error,
getDisplayNameOrNameOfKernelConnection(error.kernelConnectionMetadata),
error.kernelConnectionMetadata.interpreter?.sysPrefix
);
if (failureInfo) {
// Special case for ipykernel module missing.
if (
failureInfo.reason === KernelFailureReason.moduleNotFoundFailure &&
['ipykernel_launcher', 'ipykernel'].includes(failureInfo.moduleName)
) {
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || message;
}
const messageParts = [failureInfo.message];
if (failureInfo.moreInfoLink) {
messageParts.push(Common.clickHereForMoreInfoWithHtml().format(failureInfo.moreInfoLink));
}
return messageParts.join('\n');
}
return getCombinedErrorMessage(
getErrorMessageFromPythonTraceback(error.stdErr) || error.stdErr || error.message
);
} else if (error instanceof BaseError) {
return getCombinedErrorMessage(
getErrorMessageFromPythonTraceback(error.stdErr) || error.stdErr || error.message
);
}
return message;
}
public async handleKernelError(
err: Error,
purpose: 'start' | 'restart' | 'interrupt' | 'execution',
Expand All @@ -110,6 +171,30 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
return response === JupyterInterpreterDependencyResponse.ok
? KernelInterpreterDependencyResponse.ok
: KernelInterpreterDependencyResponse.cancel;
} else if (err instanceof JupyterSelfCertsError) {
// On a self cert error, warn the user and ask if they want to change the setting
const enableOption: string = DataScience.jupyterSelfCertEnable();
const closeOption: string = DataScience.jupyterSelfCertClose();
void this.applicationShell
.showErrorMessage(DataScience.jupyterSelfCertFail().format(err.message), enableOption, closeOption)
.then((value) => {
if (value === enableOption) {
sendTelemetryEvent(Telemetry.SelfCertsMessageEnabled);
void this.configuration.updateSetting(
'allowUnauthorizedRemoteConnection',
true,
undefined,
ConfigurationTarget.Workspace
);
} else if (value === closeOption) {
sendTelemetryEvent(Telemetry.SelfCertsMessageClose);
}
});
return KernelInterpreterDependencyResponse.failed;
} else if (err instanceof VscCancellationError || err instanceof CancellationError) {
// Don't show the message for cancellation errors
traceWarning(`Cancelled by user`, err);
return KernelInterpreterDependencyResponse.cancel;
} else if (
(purpose === 'start' || purpose === 'restart') &&
!(await this.kernelDependency.areDependenciesInstalled(kernelConnection, undefined, true))
Expand All @@ -135,11 +220,18 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
);
if (failureInfo) {
void this.showMessageWithMoreInfo(failureInfo?.message, failureInfo?.moreInfoLink);
} else if (err instanceof BaseError) {
const message = getCombinedErrorMessage(
getErrorMessageFromPythonTraceback(err.stdErr) || err.stdErr || err.message
);
void this.showMessageWithMoreInfo(message);
} else {
void this.showMessageWithMoreInfo(err.message);
}
return KernelInterpreterDependencyResponse.failed;
}
}
private async showMessageWithMoreInfo(message: string, moreInfoLink: string | undefined) {
private async showMessageWithMoreInfo(message: string, moreInfoLink?: string) {
if (!message.includes(Commands.ViewJupyterOutput)) {
message = `${message} \n${DataScience.viewJupyterLogForFurtherInfo()}`;
}
Expand Down Expand Up @@ -172,3 +264,52 @@ export function getKernelNotInstalledErrorMessage(notebookMetadata?: nbformat.IN
return DataScience.kernelNotInstalled().format(kernelName);
}
}

function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnectionMetadata) {
if (
kernelConnection.kind === 'connectToLiveKernel' ||
kernelConnection.kind === 'startUsingRemoteKernelSpec' ||
!kernelConnection.interpreter
) {
return;
}
const displayNameOfKernel = kernelConnection.interpreter.displayName || kernelConnection.interpreter.path;
const ipyKernelName = ProductNames.get(Product.ipykernel)!;
const ipyKernelModuleName = translateProductToModule(Product.ipykernel);

let installerCommand = `${kernelConnection.interpreter.path.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --force-reinstall`;
if (kernelConnection.interpreter?.envType === EnvironmentType.Conda) {
if (kernelConnection.interpreter?.envName) {
installerCommand = `conda install -n ${kernelConnection.interpreter?.envName} ${ipyKernelModuleName} --update-deps --force-reinstall`;
} else if (kernelConnection.interpreter?.envPath) {
installerCommand = `conda install -p ${kernelConnection.interpreter?.envPath} ${ipyKernelModuleName} --update-deps --force-reinstall`;
}
} else if (
kernelConnection.interpreter?.envType === EnvironmentType.Global ||
kernelConnection.interpreter?.envType === EnvironmentType.WindowsStore ||
kernelConnection.interpreter?.envType === EnvironmentType.System
) {
installerCommand = `${kernelConnection.interpreter.path.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --user --force-reinstall`;
}
const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter().format(
displayNameOfKernel,
ProductNames.get(Product.ipykernel)!
);
const installationInstructions = DataScience.installPackageInstructions().format(ipyKernelName, installerCommand);
return message + '\n' + installationInstructions;
}
function getJupyterMissingErrorMessageForCell(err: JupyterInstallError) {
const productNames = `${ProductNames.get(Product.jupyter)} ${Common.and()} ${ProductNames.get(Product.notebook)}`;
const moduleNames = [Product.jupyter, Product.notebook].map(translateProductToModule).join(' ');

const installerCommand = `python -m pip install ${moduleNames} -U\nor\nconda install ${moduleNames} -U`;
const installationInstructions = DataScience.installPackageInstructions().format(productNames, installerCommand);

return (
err.message +
'\n' +
installationInstructions +
'\n' +
Common.clickHereForMoreInfoWithHtml().format('https://aka.ms/installJupyterForVSCode')
);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import { BaseError } from '../../common/errors/types';
import { BaseKernelError } from '../../common/errors/types';
import '../../common/extensions';
import * as localize from '../../common/utils/localize';
import { KernelConnectionMetadata } from '../jupyter/kernels/types';

export class JupyterDebuggerNotInstalledError extends BaseError {
constructor(debuggerPkg: string, message?: string) {
export class JupyterDebuggerNotInstalledError extends BaseKernelError {
constructor(debuggerPkg: string, message: string | undefined, kernelConnectionMetadata: KernelConnectionMetadata) {
const errorMessage = message
? message
: localize.DataScience.jupyterDebuggerNotInstalledError().format(debuggerPkg);
super('notinstalled', errorMessage);
super('notinstalled', errorMessage, kernelConnectionMetadata);
}
}
20 changes: 0 additions & 20 deletions src/client/datascience/errors/jupyterDebuggerPortBlockedError.ts

This file was deleted.

Loading

0 comments on commit 98f4b05

Please sign in to comment.