Skip to content

Commit

Permalink
Recommend updating traitlets to 5.1.1 if jupyter fails to start (#8307)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored Nov 19, 2021
1 parent 0403bcf commit 8734a9b
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 28 deletions.
1 change: 1 addition & 0 deletions news/2 Fixes/8295.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Recommend installing `traitlets` version `5.1.1` if Jupyter fails to start and the version of `traitlets` is older.
3 changes: 3 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@
"DataScience.failedToStartKernelDueToPyZmqFailure": "The kernel failed to start due to an error with the 'pyzmq' module. Consider re-installing this module.",
"DataScience.failedToStartKernelDueToOldIPython": "The kernel failed to start due to an outdated version of IPython. Consider updating this module to the latest version.",
"DataScience.failedToStartKernelDueToOldIPyKernel": "The kernel failed to start due to an outdated version of IPyKernel. Consider updating this module to the latest version.",
"DataScience.failedToStartJupyter": "Failed to start Jupyter in the environment '{0}'. \nView Jupyter [log](command:jupyter.viewOutput) for further details.",
"DataScience.failedToStartJupyterWithErrorInfo": "Failed to start Jupyter in the environment '{0}'. \n{1} \nView Jupyter [log](command:jupyter.viewOutput) for further details.",
"DataScience.failedToStartJupyterDueToOutdatedTraitlets": "Failed to start Jupyter in the environment '{0}' possibly due to an outdated version of 'traitlets'. \n{1} \nConsider updating the 'traitlets' module to '5.1.1' or later. \nView Jupyter [log](command:jupyter.viewOutput) for further details.",
"DataScience.cannotRunCellKernelIsDead": "Cannot run cells, as the kernel '{0}' is dead.",
"DataScience.waitingForJupyterSessionToBeIdle": "Waiting for Jupyter Session to be idle",
"DataScience.gettingListOfKernelsForLocalConnection": "Fetching Kernels",
Expand Down
47 changes: 45 additions & 2 deletions src/client/common/errors/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,15 @@ export enum KernelFailureReason {
* import win32api
* ImportError: DLL load failed: The specified procedure could not be found.
*/
dllLoadFailure = 'dllLoadFailure'
dllLoadFailure = 'dllLoadFailure',
/**
* Failure to start Jupyter due to some unknown reason.
*/
jupyterStartFailure = 'jupyterStartFailure',
/**
* Failure to start Jupyter due to outdated traitlets.
*/
jupyterStartFailureOutdatedTraitlets = 'jupyterStartFailureOutdatedTraitlets'
}
type BaseFailure<Reason extends KernelFailureReason, MoreInfo = {}> = {
reason: Reason;
Expand Down Expand Up @@ -208,6 +216,15 @@ export type DllLoadFailure = BaseFailure<
moduleName?: string;
}
>;
export type JupyterStartFailure = BaseFailure<
KernelFailureReason.jupyterStartFailure | KernelFailureReason.jupyterStartFailureOutdatedTraitlets,
{
/**
* Python error message displayed.
*/
errorMessage?: string;
}
>;
export type ImportWin32ApiFailure = BaseFailure<KernelFailureReason.importWin32apiFailure>;
export type ZmqModuleFailure = BaseFailure<KernelFailureReason.zmqModuleFailure>;
export type OldIPyKernelFailure = BaseFailure<KernelFailureReason.oldIPyKernelFailure>;
Expand All @@ -222,12 +239,14 @@ export type KernelFailure =
| ImportWin32ApiFailure
| ZmqModuleFailure
| OldIPyKernelFailure
| JupyterStartFailure
| OldIPythonFailure;

export function analyzeKernelErrors(
stdErrOrStackTrace: string,
workspaceFolders: readonly WorkspaceFolder[] = [],
pythonSysPrefix: string = ''
pythonSysPrefix: string = '',
isJupyterStartupError?: boolean
): KernelFailure | undefined {
const lastTwolinesOfError = getLastTwoLinesFromPythonTracebackWithErrorMessage(stdErrOrStackTrace);
const stdErr = stdErrOrStackTrace.toLowerCase();
Expand Down Expand Up @@ -398,6 +417,30 @@ export function analyzeKernelErrors(
};
}
}
if (isJupyterStartupError) {
// Get the last error message in the stack trace.
let errorMessage = stdErrOrStackTrace
.splitLines()
.map((line) => line.trim())
.reverse()
.find((line) => line.toLowerCase().includes('Error: '.toLowerCase()));
// https://github.com/microsoft/vscode-jupyter/issues/8295
const errorMessageDueToOutdatedTraitlets = "AttributeError: 'Namespace' object has no attribute '_flags'";
const telemetrySafeTags = ['jupyter.startup.failure'];
let reason = KernelFailureReason.jupyterStartFailure;
if (stdErr.includes(errorMessageDueToOutdatedTraitlets.toLowerCase())) {
reason = KernelFailureReason.jupyterStartFailureOutdatedTraitlets;
errorMessage = errorMessageDueToOutdatedTraitlets;
telemetrySafeTags.push('outdated.traitlets');
}
if (errorMessage) {
return {
reason,
errorMessage,
telemetrySafeTags
};
}
}
}

function extractModuleAndFileFromImportError(errorLine: string) {
Expand Down
12 changes: 12 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,18 @@ export namespace DataScience {
'DataScience.sessionStartFailedWithKernel',
"Failed to start the Kernel '{0}'. \nView Jupyter [log](command:jupyter.viewOutput) for further details."
);
export const failedToStartJupyter = localize(
'DataScience.failedToStartJupyter',
"Failed to start Jupyter in the environment '{0}'. \nView Jupyter [log](command:jupyter.viewOutput) for further details."
);
export const failedToStartJupyterWithErrorInfo = localize(
'DataScience.failedToStartJupyterWithErrorInfo',
"Failed to start Jupyter in the environment '{0}'. \n{1} \nView Jupyter [log](command:jupyter.viewOutput) for further details."
);
export const failedToStartJupyterDueToOutdatedTraitlets = localize(
'DataScience.failedToStartJupyterDueToOutdatedTraitlets',
"Failed to start Jupyter in the environment '{0}' possibly due to an outdated version of 'traitlets'. \n{1} \nConsider updating the 'traitlets' module to '5.1.1' or later. \nView Jupyter [log](command:jupyter.viewOutput) for further details."
);
export const failedToStartKernel = localize('DataScience.failedToStartKernel', 'Failed to start the Kernel.');
export const failedToRestartKernel = localize('DataScience.failedToRestartKernel', 'Failed to restart the Kernel.');
export const failedToInterruptKernel = localize(
Expand Down
102 changes: 89 additions & 13 deletions src/client/datascience/errors/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,21 @@ import { PythonKernelDiedError } from './pythonKernelDiedError';
import {
analyzeKernelErrors,
getErrorMessageFromPythonTraceback,
KernelFailure,
KernelFailureReason
} from '../../common/errors/errorUtils';
import { IKernelProvider, KernelConnectionMetadata } from '../jupyter/kernels/types';
import { getDisplayPath } from '../../common/platform/fs-paths';
import { IBrowserService, IConfigurationService, Resource } from '../../common/types';
import { Telemetry } from '../constants';
import { Commands, Telemetry } from '../constants';
import { sendTelemetryEvent } from '../../telemetry';
import { DisplayOptions } from '../displayOptions';
import { IServiceContainer } from '../../ioc/types';
import { StopWatch } from '../../common/utils/stopWatch';
import { sleep } from '../../common/utils/async';
import { INotebookControllerManager } from '../notebook/types';
import { JupyterConnectError } from './jupyterConnectError';
import { JupyterInterpreterService } from '../jupyter/interpreter/jupyterInterpreterService';

@injectable()
export class DataScienceErrorHandler implements IDataScienceErrorHandler {
Expand All @@ -58,6 +61,7 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
@inject(IBrowserService) private readonly browser: IBrowserService,
@inject(IConfigurationService) private readonly configuration: IConfigurationService,
@inject(IKernelDependencyService) private readonly kernelDependency: IKernelDependencyService,
@inject(JupyterInterpreterService) private readonly jupyterInterpreter: JupyterInterpreterService,
@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer
) {}
public async handleError(err: Error): Promise<void> {
Expand All @@ -77,6 +81,12 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
purpose,
cellToDisplayErrors,
async (error: BaseError, defaultErrorMessage?: string) => {
const failureInfo = analyzeKernelErrors(
error.stdErr || '',
this.workspace.workspaceFolders,
kernelConnection.interpreter?.sysPrefix,
err instanceof JupyterConnectError
);
if (
err instanceof IpyKernelNotInstalledError &&
err.reason === KernelInterpreterDependencyResponse.uiHidden &&
Expand All @@ -96,13 +106,11 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
)
.finally(() => token.dispose());
return;
} else if (err instanceof JupyterConnectError) {
void this.handleJupyterStartupError(failureInfo, err, kernelConnection, cellToDisplayErrors);
return;
}

const failureInfo = analyzeKernelErrors(
error.stdErr || '',
this.workspace.workspaceFolders,
kernelConnection.interpreter?.sysPrefix
);
switch (failureInfo?.reason) {
case KernelFailureReason.overridingBuiltinModules: {
await this.showMessageWithMoreInfo(
Expand Down Expand Up @@ -219,15 +227,78 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
}
);
}
private async showMessageWithMoreInfo(message: string, moreInfoLink: string, cellToDisplayErrors?: NotebookCell) {
message = `${message} \n${DataScience.viewJupyterLogForFurtherInfo()}`;
void this.displayErrorsInCell(message, cellToDisplayErrors);
await this.applicationShell.showErrorMessage(message, Common.learnMore()).then((selection) => {
if (selection === Common.learnMore()) {
private async showMessageWithMoreInfo(
message: string,
moreInfoLink: string | undefined,
cellToDisplayErrors?: NotebookCell
) {
if (!message.includes(Commands.ViewJupyterOutput)) {
message = `${message} \n${DataScience.viewJupyterLogForFurtherInfo()}`;
}
void this.displayErrorsInCell(message, cellToDisplayErrors, moreInfoLink);
const buttons = moreInfoLink ? [Common.learnMore()] : [];
await this.applicationShell.showErrorMessage(message, ...buttons).then((selection) => {
if (selection === Common.learnMore() && moreInfoLink) {
this.browser.launch(moreInfoLink);
}
});
}
private async handleJupyterStartupError(
failureInfo: KernelFailure | undefined,
error: JupyterConnectError,
kernelConnection: KernelConnectionMetadata,
cellToDisplayErrors?: NotebookCell
) {
const failureInfoFromMessage =
failureInfo ||
analyzeKernelErrors(
error.message,
this.workspace.workspaceFolders,
kernelConnection.interpreter?.sysPrefix,
true
);
// Extract the python error message so we can display that.
let pythonError: string | undefined =
failureInfoFromMessage?.reason === KernelFailureReason.jupyterStartFailure ||
failureInfoFromMessage?.reason === KernelFailureReason.jupyterStartFailureOutdatedTraitlets
? failureInfoFromMessage?.errorMessage
: undefined;
if (!pythonError) {
// Some times the error message is either in the message or the stderr.
pythonError = error.message
.splitLines({ removeEmptyEntries: true, trim: true })
.reverse()
.find((item) => item.toLowerCase().includes('Error: '));
pythonError =
pythonError ||
(error.stdErr || '')
.splitLines({ removeEmptyEntries: true, trim: true })
.reverse()
.find((item) => item.toLowerCase().includes('Error: '));
}
const jupyterInterpreter = await this.jupyterInterpreter.getSelectedInterpreter();
const envDisplayName = jupyterInterpreter
? `${jupyterInterpreter.displayName} (${getDisplayPath(
jupyterInterpreter.path,
this.workspace.workspaceFolders || []
)})`
: '';
if (
jupyterInterpreter &&
failureInfoFromMessage?.reason === KernelFailureReason.jupyterStartFailureOutdatedTraitlets
) {
void this.showMessageWithMoreInfo(
DataScience.failedToStartJupyterDueToOutdatedTraitlets().format(envDisplayName, pythonError || ''),
'https://aka.ms/kernelFailuresJupyterTrailtletsOutdated',
cellToDisplayErrors
);
} else {
const message = pythonError
? DataScience.failedToStartJupyterWithErrorInfo().format(envDisplayName, pythonError)
: DataScience.failedToStartJupyter().format(envDisplayName);
void this.showMessageWithMoreInfo(message, undefined, cellToDisplayErrors);
}
}
private async handleErrorImplementation(
err: Error,
purpose?: 'start' | 'restart' | 'interrupt' | 'execution',
Expand Down Expand Up @@ -279,7 +350,8 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
} else if (
err instanceof KernelDiedError ||
err instanceof KernelProcessExitedError ||
err instanceof PythonKernelDiedError
err instanceof PythonKernelDiedError ||
err instanceof JupyterConnectError
) {
const defaultErrorMessage = getCombinedErrorMessage(
errorPrefix,
Expand All @@ -300,7 +372,7 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
}
traceError('DataScience Error', err);
}
private async displayErrorsInCell(errorMessage: string, cellToDisplayErrors?: NotebookCell) {
private async displayErrorsInCell(errorMessage: string, cellToDisplayErrors?: NotebookCell, moreInfoLink?: string) {
if (!cellToDisplayErrors || !errorMessage) {
return;
}
Expand All @@ -325,13 +397,17 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler {
if (!controller || controller.connection !== associatedkernel.kernelConnectionMetadata) {
return;
}
// If we have markdown links to run a command, turn that into a link.
const regex = /\[(?<name>.*)\]\((?<command>command:\S*)\)/gm;
let matches: RegExpExecArray | undefined | null;
while ((matches = regex.exec(errorMessage)) !== null) {
if (matches.length === 3) {
errorMessage = errorMessage.replace(matches[0], `<a href='${matches[2]}'>${matches[1]}</a>`);
}
}
if (moreInfoLink) {
errorMessage += `\n<a href='${moreInfoLink}'>${Common.learnMore()}</a>`;
}
const execution = controller.controller.createNotebookCellExecution(cellToDisplayErrors);
execution.start();
void execution.clearOutput(cellToDisplayErrors);
Expand Down
8 changes: 5 additions & 3 deletions src/client/datascience/jupyter/jupyterConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ChildProcess } from 'child_process';
import { Subscription } from 'rxjs';
import { CancellationToken, Disposable, Event, EventEmitter } from 'vscode';
import { Cancellation, CancellationError } from '../../common/cancellation';
import { traceInfo, traceWarning } from '../../common/logger';
import { traceError, traceInfo, traceWarning } from '../../common/logger';

import { IFileSystem } from '../../common/platform/types';
import { ObservableExecutionResult, Output } from '../../common/process/types';
Expand Down Expand Up @@ -89,7 +89,7 @@ export class JupyterConnectionWaiter implements IDisposable {
this.output(output.out);
}
},
(e) => this.rejectStartPromise(e.message),
(e) => this.rejectStartPromise(e),
// If the process dies, we can't extract connection information.
() => this.rejectStartPromise(localize.DataScience.jupyterServerCrashed().format(exitCode))
)
Expand Down Expand Up @@ -155,6 +155,7 @@ export class JupyterConnectionWaiter implements IDisposable {
try {
url = new URL(uriString);
} catch (err) {
traceError(`Failed to parse ${uriString}`, err);
// Failed to parse the url either via server infos or the string
this.rejectStartPromise(localize.DataScience.jupyterLaunchNoURL());
return;
Expand Down Expand Up @@ -208,10 +209,11 @@ export class JupyterConnectionWaiter implements IDisposable {
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private rejectStartPromise = (message: string) => {
private rejectStartPromise = (message: string | Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clearTimeout(this.launchTimeout as any);
if (!this.startPromise.resolved) {
message = typeof message === 'string' ? message : message.message;
this.startPromise.reject(
Cancellation.isCanceled(this.cancelToken)
? new CancellationError()
Expand Down
4 changes: 4 additions & 0 deletions src/client/datascience/jupyter/kernels/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { Deferred } from '../../../common/utils/async';
import { getDisplayPath } from '../../../common/platform/fs-paths';
import { WrappedError } from '../../../common/errors/types';
import { DisplayOptions } from '../../displayOptions';
import { JupyterConnectError } from '../../errors/jupyterConnectError';

export class Kernel implements IKernel {
get connection(): INotebookProviderConnection | undefined {
Expand Down Expand Up @@ -381,6 +382,9 @@ export class Kernel implements IKernel {
`failed to create INotebook in kernel, UI Disabled = ${this.startupUI.disableUI}`,
ex
);
if (ex instanceof JupyterConnectError) {
throw ex;
}
// Provide a user friendly message in case `ex` is some error thats not throw by us.
const message = DataScience.sessionStartFailedWithKernel().format(
getDisplayNameOrNameOfKernelConnection(this.kernelConnectionMetadata)
Expand Down
3 changes: 2 additions & 1 deletion src/client/datascience/jupyter/notebookStarter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { IJupyterConnection, IJupyterSubCommandExecutionService } from '../types
import { JupyterConnectionWaiter } from './jupyterConnection';
import { JupyterInstallError } from '../errors/jupyterInstallError';
import { disposeAllDisposables } from '../../common/helpers';
import { JupyterConnectError } from '../errors/jupyterConnectError';

/**
* Responsible for starting a notebook.
Expand Down Expand Up @@ -164,7 +165,7 @@ export class NotebookStarter implements Disposable {
return connection;
} catch (err) {
disposeAllDisposables(disposables);
if (err instanceof CancellationError) {
if (err instanceof CancellationError || err instanceof JupyterConnectError) {
throw err;
}

Expand Down
3 changes: 3 additions & 0 deletions src/client/logging/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ function formatArgument(target: Object, method: MethodName, arg: any, parameterI
// Where possible strip user names from paths, then users will be more likely to provide the logs.
return removeUserPaths(arg.fsPath);
}
if (!arg) {
return arg;
}
const parameterInfos = formattedParameters.get(target)?.get(method);
const info = parameterInfos?.find((info) => info.parameterIndex === parameterIndex);
if (!info) {
Expand Down
Loading

0 comments on commit 8734a9b

Please sign in to comment.