Skip to content
Merged
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
52 changes: 44 additions & 8 deletions src/kernels/deepnote/deepnoteServerStarter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from '../../platform/vscode-path/path';
import { generateUuid } from '../../platform/common/uuid';
import { DeepnoteServerStartupError, DeepnoteServerTimeoutError } from '../../platform/errors/deepnoteKernelErrors';

/**
* Lock file data structure for tracking server ownership
Expand All @@ -42,6 +43,8 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
private readonly sessionId: string = generateUuid();
// Directory for lock files
private readonly lockFileDir: string = path.join(os.tmpdir(), 'vscode-deepnote-locks');
// Track server output for error reporting
private readonly serverOutputByFile: Map<string, { stdout: string; stderr: string }> = new Map();

constructor(
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
Expand Down Expand Up @@ -118,12 +121,9 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension

Cancellation.throwIfCanceled(token);

// Ensure toolkit is installed
// Ensure toolkit is installed (will throw typed errors on failure)
logger.info(`Ensuring deepnote-toolkit is installed for ${fileKey}...`);
const installed = await this.toolkitInstaller.ensureInstalled(interpreter, deepnoteFileUri, token);
if (!installed) {
throw new Error('Failed to install deepnote-toolkit. Please check the output for details.');
}
await this.toolkitInstaller.ensureInstalled(interpreter, deepnoteFileUri, token);

Cancellation.throwIfCanceled(token);

Expand Down Expand Up @@ -197,15 +197,27 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
const disposables: IDisposable[] = [];
this.disposablesByFile.set(fileKey, disposables);

// Initialize output tracking for error reporting
this.serverOutputByFile.set(fileKey, { stdout: '', stderr: '' });

// Monitor server output
serverProcess.out.onDidChange(
(output) => {
const outputTracking = this.serverOutputByFile.get(fileKey);
if (output.source === 'stdout') {
logger.trace(`Deepnote server (${fileKey}): ${output.out}`);
this.outputChannel.appendLine(output.out);
if (outputTracking) {
// Keep last 5000 characters of output for error reporting
outputTracking.stdout = (outputTracking.stdout + output.out).slice(-5000);
}
} else if (output.source === 'stderr') {
logger.warn(`Deepnote server stderr (${fileKey}): ${output.out}`);
this.outputChannel.appendLine(output.out);
if (outputTracking) {
// Keep last 5000 characters of error output for error reporting
outputTracking.stderr = (outputTracking.stderr + output.out).slice(-5000);
}
}
},
this,
Expand All @@ -228,13 +240,35 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
try {
const serverReady = await this.waitForServer(serverInfo, 120000, token);
if (!serverReady) {
const output = this.serverOutputByFile.get(fileKey);
await this.stopServerImpl(deepnoteFileUri);
throw new Error('Deepnote server failed to start within timeout period');

throw new DeepnoteServerTimeoutError(serverInfo.url, 120000, output?.stderr || undefined);
}
} catch (error) {
// Clean up leaked server before rethrowing
// If this is already a DeepnoteKernelError, clean up and rethrow it
if (error instanceof DeepnoteServerTimeoutError || error instanceof DeepnoteServerStartupError) {
await this.stopServerImpl(deepnoteFileUri);
throw error;
}

// Capture output BEFORE cleaning up (stopServerImpl deletes it)
const output = this.serverOutputByFile.get(fileKey);
const capturedStdout = output?.stdout || '';
const capturedStderr = output?.stderr || '';

// Clean up leaked server after capturing output
await this.stopServerImpl(deepnoteFileUri);
throw error;

// Wrap in a generic server startup error with captured output
throw new DeepnoteServerStartupError(
interpreter.uri.fsPath,
port,
'unknown',
capturedStdout,
capturedStderr,
error instanceof Error ? error : undefined
);
}

logger.info(`Deepnote server started successfully at ${url} for ${fileKey}`);
Expand Down Expand Up @@ -283,6 +317,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
serverProcess.proc?.kill();
this.serverProcesses.delete(fileKey);
this.serverInfos.delete(fileKey);
this.serverOutputByFile.delete(fileKey);
this.outputChannel.appendLine(`Deepnote server stopped for ${fileKey}`);

// Clean up lock file after stopping the server
Expand Down Expand Up @@ -403,6 +438,7 @@ export class DeepnoteServerStarter implements IDeepnoteServerStarter, IExtension
this.serverInfos.clear();
this.disposablesByFile.clear();
this.pendingOperations.clear();
this.serverOutputByFile.clear();

logger.info('DeepnoteServerStarter disposed successfully');
}
Expand Down
41 changes: 34 additions & 7 deletions src/kernels/deepnote/deepnoteToolkitInstaller.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IOutputChannel, IExtensionContext } from '../../platform/common/types';
import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants';
import { IFileSystem } from '../../platform/common/platform/types';
import { Cancellation } from '../../platform/common/cancellation';
import { DeepnoteVenvCreationError, DeepnoteToolkitInstallError } from '../../platform/errors/deepnoteKernelErrors';

/**
* Handles installation of the deepnote-toolkit Python package.
Expand All @@ -19,7 +20,7 @@ import { Cancellation } from '../../platform/common/cancellation';
export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
private readonly venvPythonPaths: Map<string, Uri> = new Map();
// Track in-flight installations per venv path to prevent concurrent installs
private readonly pendingInstallations: Map<string, Promise<PythonEnvironment | undefined>> = new Map();
private readonly pendingInstallations: Map<string, Promise<PythonEnvironment>> = new Map();

constructor(
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory,
Expand Down Expand Up @@ -62,7 +63,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
baseInterpreter: PythonEnvironment,
deepnoteFileUri: Uri,
token?: CancellationToken
): Promise<PythonEnvironment | undefined> {
): Promise<PythonEnvironment> {
const venvPath = this.getVenvPath(deepnoteFileUri);
const venvKey = venvPath.fsPath;

Expand Down Expand Up @@ -119,7 +120,7 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
deepnoteFileUri: Uri,
venvPath: Uri,
token?: CancellationToken
): Promise<PythonEnvironment | undefined> {
): Promise<PythonEnvironment> {
try {
Cancellation.throwIfCanceled(token);

Expand Down Expand Up @@ -158,7 +159,12 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
logger.error(`venv stderr: ${venvResult.stderr}`);
}
this.outputChannel.appendLine('Error: Failed to create virtual environment');
return undefined;

throw new DeepnoteVenvCreationError(
baseInterpreter.uri.fsPath,
venvPath.fsPath,
venvResult.stderr || 'Virtual environment was created but Python interpreter not found'
);
}

// Use undefined as resource to get full system environment (including git in PATH)
Expand Down Expand Up @@ -250,12 +256,33 @@ export class DeepnoteToolkitInstaller implements IDeepnoteToolkitInstaller {
} else {
logger.error('deepnote-toolkit installation failed');
this.outputChannel.appendLine('✗ deepnote-toolkit installation failed');
return undefined;

throw new DeepnoteToolkitInstallError(
venvInterpreter.uri.fsPath,
venvPath.fsPath,
DEEPNOTE_TOOLKIT_WHEEL_URL,
installResult.stdout || '',
installResult.stderr || 'Package installation completed but verification failed'
);
}
} catch (ex) {
// If this is already a DeepnoteKernelError, rethrow it without wrapping
if (ex instanceof DeepnoteVenvCreationError || ex instanceof DeepnoteToolkitInstallError) {
throw ex;
}

// Otherwise, log full details and wrap in a generic toolkit install error
logger.error(`Failed to set up deepnote-toolkit: ${ex}`);
this.outputChannel.appendLine(`Error setting up deepnote-toolkit: ${ex}`);
return undefined;
this.outputChannel.appendLine('Failed to set up deepnote-toolkit; see logs for details');

throw new DeepnoteToolkitInstallError(
baseInterpreter.uri.fsPath,
venvPath.fsPath,
DEEPNOTE_TOOLKIT_WHEEL_URL,
'',
ex instanceof Error ? ex.message : String(ex),
ex instanceof Error ? ex : undefined
);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/kernels/deepnote/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ export interface IDeepnoteToolkitInstaller {
* @param baseInterpreter The base Python interpreter to use for creating the venv
* @param deepnoteFileUri The URI of the .deepnote file (used to create a unique venv per file)
* @param token Cancellation token to cancel the operation
* @returns The Python interpreter from the venv if installed successfully, undefined otherwise
* @returns The Python interpreter from the venv
* @throws {DeepnoteVenvCreationError} If venv creation fails
* @throws {DeepnoteToolkitInstallError} If toolkit installation fails
*/
ensureInstalled(
baseInterpreter: PythonEnvironment,
deepnoteFileUri: vscode.Uri,
token?: vscode.CancellationToken
): Promise<PythonEnvironment | undefined>;
): Promise<PythonEnvironment>;

/**
* Gets the venv Python interpreter if toolkit is installed, undefined otherwise.
Expand Down
77 changes: 67 additions & 10 deletions src/notebooks/deepnote/deepnoteKernelAutoSelector.node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { inject, injectable, optional } from 'inversify';
import { inject, injectable, optional, named } from 'inversify';
import {
CancellationToken,
NotebookDocument,
Expand All @@ -13,7 +13,8 @@ import {
NotebookController,
CancellationTokenSource,
Disposable,
l10n
l10n,
env
} from 'vscode';
import { IExtensionSyncActivationService } from '../../platform/activation/types';
import { IDisposableRegistry } from '../../platform/common/types';
Expand Down Expand Up @@ -42,6 +43,9 @@ import { IDeepnoteNotebookManager } from '../types';
import { IDeepnoteRequirementsHelper } from './deepnoteRequirementsHelper.node';
import { DeepnoteProject } from './deepnoteTypes';
import { IKernelProvider, IKernel } from '../../kernels/types';
import { DeepnoteKernelError } from '../../platform/errors/deepnoteKernelErrors';
import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants';
import { IOutputChannel } from '../../platform/common/types';

/**
* Automatically selects and starts Deepnote kernel for .deepnote notebooks
Expand Down Expand Up @@ -78,7 +82,8 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
@inject(IDeepnoteInitNotebookRunner) private readonly initNotebookRunner: IDeepnoteInitNotebookRunner,
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager,
@inject(IKernelProvider) private readonly kernelProvider: IKernelProvider,
@inject(IDeepnoteRequirementsHelper) private readonly requirementsHelper: IDeepnoteRequirementsHelper
@inject(IDeepnoteRequirementsHelper) private readonly requirementsHelper: IDeepnoteRequirementsHelper,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel
) {}

public activate() {
Expand Down Expand Up @@ -129,9 +134,7 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
// Don't await - let it happen in background so notebook opens quickly
void this.ensureKernelSelected(notebook).catch((error) => {
logger.error(`Failed to auto-select Deepnote kernel for ${getDisplayPath(notebook.uri)}`, error);
void window.showErrorMessage(
l10n.t('Failed to load Deepnote kernel. Please check the output for details.')
);
void this.handleKernelSelectionError(error);
});
}

Expand Down Expand Up @@ -342,16 +345,13 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
logger.info(`Using base interpreter: ${getDisplayPath(interpreter.uri)}`);

// Ensure deepnote-toolkit is installed in a venv and get the venv interpreter
// Will throw typed errors on failure (DeepnoteVenvCreationError or DeepnoteToolkitInstallError)
progress.report({ message: l10n.t('Installing Deepnote toolkit...') });
const venvInterpreter = await this.toolkitInstaller.ensureInstalled(
interpreter,
baseFileUri,
progressToken
);
if (!venvInterpreter) {
logger.error('Failed to set up Deepnote toolkit environment');
return; // Exit gracefully
}

logger.info(`Deepnote toolkit venv ready at: ${getDisplayPath(venvInterpreter.uri)}`);

Expand Down Expand Up @@ -553,4 +553,61 @@ export class DeepnoteKernelAutoSelector implements IDeepnoteKernelAutoSelector,
this.loadingControllers.set(notebookKey, loadingController);
logger.info(`Created loading controller for ${notebookKey}`);
}

/**
* Handle kernel selection errors with user-friendly messages and actions
*/
private async handleKernelSelectionError(error: unknown): Promise<void> {
// Handle DeepnoteKernelError types with specific guidance
if (error instanceof DeepnoteKernelError) {
// Log the technical details
logger.error(error.getErrorReport());

// Show user-friendly error with actions
const showOutputAction = l10n.t('Show Output');
const copyErrorAction = l10n.t('Copy Error Details');
const actions: string[] = [showOutputAction, copyErrorAction];

const troubleshootingHeader = l10n.t('Troubleshooting:');
const troubleshootingSteps = error.troubleshootingSteps
.slice(0, 3)
.map((step, i) => `${i + 1}. ${step}`)
.join('\n');

const selectedAction = await window.showErrorMessage(
`${error.userMessage}\n\n${troubleshootingHeader}\n${troubleshootingSteps}`,
{ modal: false },
...actions
);

if (selectedAction === showOutputAction) {
this.outputChannel.show();
} else if (selectedAction === copyErrorAction) {
try {
await env.clipboard.writeText(error.getErrorReport());
void window.showInformationMessage(l10n.t('Error details copied to clipboard'));
} catch (clipboardError) {
logger.error('Failed to copy error details to clipboard', clipboardError);
void window.showErrorMessage(l10n.t('Failed to copy error details to clipboard'));
}
}

return;
}

// Handle generic errors
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Deepnote kernel error: ${errorMessage}`);

const showOutputAction = l10n.t('Show Output');
const selectedAction = await window.showErrorMessage(
l10n.t('Failed to load Deepnote kernel: {0}', errorMessage),
{ modal: false },
showOutputAction
);

if (selectedAction === showOutputAction) {
this.outputChannel.show();
}
}
}
Loading