Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 36 additions & 2 deletions src/kernels/deepnote/deepnoteServerStarter.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 @@ -41,6 +42,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 @@ -175,15 +178,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 @@ -206,13 +221,30 @@ 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
await this.stopServerImpl(deepnoteFileUri);
throw error;

// If this is already a DeepnoteKernelError, rethrow it
if (error instanceof DeepnoteServerTimeoutError || error instanceof DeepnoteServerStartupError) {
throw error;
}

// Otherwise wrap in a generic server startup error
const output = this.serverOutputByFile.get(fileKey);
throw new DeepnoteServerStartupError(
interpreter.uri.fsPath,
port,
'unknown',
output?.stdout || '',
output?.stderr || '',
error instanceof Error ? error : undefined
);
}

logger.info(`Deepnote server started successfully at ${url} for ${fileKey}`);
Expand Down Expand Up @@ -261,6 +293,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 @@ -381,6 +414,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
33 changes: 30 additions & 3 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 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 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;

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
61 changes: 55 additions & 6 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 @@ -553,4 +556,50 @@ 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 actions: string[] = ['Show Output', 'Copy Error Details'];

const selectedAction = await window.showErrorMessage(
`${error.userMessage}\n\nTroubleshooting:\n${error.troubleshootingSteps
.slice(0, 3)
.map((step, i) => `${i + 1}. ${step}`)
.join('\n')}`,
{ modal: false },
...actions
);

if (selectedAction === 'Show Output') {
this.outputChannel.show();
} else if (selectedAction === 'Copy Error Details') {
await env.clipboard.writeText(error.getErrorReport());
void window.showInformationMessage('Error details copied to clipboard');
}

return;
}

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

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

if (selectedAction === 'Show Output') {
this.outputChannel.show();
}
}
}
Loading