From 251bcc7e60e6afd030a0fd697500fb2330ba8a01 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 21 Oct 2022 12:33:01 -0700 Subject: [PATCH] Implement restart (#11668) * Support debug "restart" microsoft/vscode-jupyter#7670 * More debug refactoring, set up IW for restart * Hook up Restart for rbl, via a custom Restart request implementation. Move commands out of DebuggingManager, and expose methods on that class for use by the rbl controller * Hook up restart for "debug cell" * Clean up dumped cell files in disconnect, so it can be awaited * Fix comments in controller * formatting * Fix PR comments * Wait for debugger to be totally set up before executing cell in IW * Add test for restarting debug. Also fix the restart request handler to avoid cancelling the restart request on accident. --- package.json | 12 - src/commands.ts | 4 +- ...lControllers.ts => debugCellController.ts} | 9 +- .../debugger/jupyter/debugger.ts | 15 + .../debugger/jupyter/debuggingManager.ts | 167 ++++----- src/notebooks/debugger/commandRegistry.ts | 89 +++++ .../debugCellController.ts} | 22 +- .../debugger/controllers/restartController.ts | 70 ++++ .../{ => controllers}/runByLineController.ts | 34 +- src/notebooks/debugger/debugger.ts | 38 +-- src/notebooks/debugger/debuggerVariables.ts | 30 +- src/notebooks/debugger/debuggingManager.ts | 320 ++++++------------ .../debugger/debuggingManagerBase.ts | 110 +++--- src/notebooks/debugger/debuggingTypes.ts | 44 ++- src/notebooks/debugger/helper.ts | 21 +- .../debugger/kernelDebugAdapterBase.ts | 53 ++- src/notebooks/serviceRegistry.node.ts | 59 ++-- src/notebooks/serviceRegistry.web.ts | 59 ++-- src/platform/common/constants.ts | 1 - src/test/datascience/debugger.vscode.test.ts | 58 +++- src/test/datascience/notebook/helper.ts | 12 +- 21 files changed, 682 insertions(+), 545 deletions(-) rename src/interactive-window/debugger/jupyter/{debugCellControllers.ts => debugCellController.ts} (94%) create mode 100644 src/interactive-window/debugger/jupyter/debugger.ts create mode 100644 src/notebooks/debugger/commandRegistry.ts rename src/notebooks/debugger/{debugCellControllers.ts => controllers/debugCellController.ts} (73%) create mode 100644 src/notebooks/debugger/controllers/restartController.ts rename src/notebooks/debugger/{ => controllers}/runByLineController.ts (84%) diff --git a/package.json b/package.json index 723a52ab7f5..f5276c35f7c 100644 --- a/package.json +++ b/package.json @@ -288,13 +288,6 @@ "icon": "$(debug-start)", "enablement": "jupyter.development && notebookType == jupyter-notebook && isWorkspaceTrusted && jupyter.replayLogLoaded && !jupyter.webExtension" }, - { - "command": "jupyter.debugNotebook", - "title": "%jupyter.command.jupyter.debug.title%", - "icon": "$(bug)", - "category": "Jupyter", - "enablement": "notebookKernelCount > 0 && resource not in jupyter.notebookeditor.runByLineDocuments" - }, { "command": "jupyter.filterKernels", "title": "%jupyter.command.jupyter.filterKernels.title%", @@ -1099,11 +1092,6 @@ "title": "%jupyter.commandPalette.jupyter.replayPylanceLog.title%", "when": "jupyter.development && isWorkspaceTrusted" }, - { - "command": "jupyter.debugNotebook", - "title": "%jupyter.command.jupyter.debug.title%", - "when": "false" - }, { "command": "jupyter.interactive.copyCell", "when": "false" diff --git a/src/commands.ts b/src/commands.ts index 8ffc0467548..9ed432c9bb9 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -30,6 +30,7 @@ interface ICommandNameWithoutArgumentTypeMapping { ['workbench.action.showCommands']: []; ['workbench.action.debug.continue']: []; ['workbench.action.debug.stepOver']: []; + ['workbench.action.debug.restart']: []; ['workbench.action.debug.stop']: []; ['workbench.action.reloadWindow']: []; ['workbench.action.closeActiveEditor']: []; @@ -193,11 +194,10 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu NotebookDocument | undefined ]; [DSCommands.SelectNativeJupyterUriFromToolBar]: []; - [DSCommands.DebugNotebook]: []; [DSCommands.RunByLine]: [NotebookCell]; [DSCommands.RunAndDebugCell]: [NotebookCell]; [DSCommands.RunByLineNext]: [NotebookCell]; - [DSCommands.RunByLineStop]: []; + [DSCommands.RunByLineStop]: [NotebookCell]; [DSCommands.ReplayPylanceLog]: [Uri]; [DSCommands.ReplayPylanceLogStep]: []; [DSCommands.InstallPythonExtensionViaKernelPicker]: []; diff --git a/src/interactive-window/debugger/jupyter/debugCellControllers.ts b/src/interactive-window/debugger/jupyter/debugCellController.ts similarity index 94% rename from src/interactive-window/debugger/jupyter/debugCellControllers.ts rename to src/interactive-window/debugger/jupyter/debugCellController.ts index 4d3b2d8cbb4..64e1363669b 100644 --- a/src/interactive-window/debugger/jupyter/debugCellControllers.ts +++ b/src/interactive-window/debugger/jupyter/debugCellController.ts @@ -5,7 +5,7 @@ import { NotebookCell } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; import { IKernel } from '../../../kernels/types'; import { DebuggingTelemetry } from '../../../notebooks/debugger/constants'; -import { isJustMyCodeNotification } from '../../../notebooks/debugger/debugCellControllers'; +import { isJustMyCodeNotification } from '../../../notebooks/debugger/controllers/debugCellController'; import { IDebuggingDelegate, IKernelDebugAdapter } from '../../../notebooks/debugger/debuggingTypes'; import { cellDebugSetup } from '../../../notebooks/debugger/helper'; import { createDeferred } from '../../../platform/common/utils/async'; @@ -18,8 +18,9 @@ import { getInteractiveCellMetadata } from '../../helpers'; * Dumping a cell is how the IPython kernel determines the file path of a cell */ export class DebugCellController implements IDebuggingDelegate { - private readonly _ready = createDeferred(); + private _ready = createDeferred(); public readonly ready = this._ready.promise; + private cellDumpInvoked?: boolean; constructor( private readonly debugAdapter: IKernelDebugAdapter, @@ -45,7 +46,7 @@ export class DebugCellController implements IDebuggingDelegate { } private debugCellDumped?: Promise; - public async willSendRequest(request: DebugProtocol.Request): Promise { + public async willSendRequest(request: DebugProtocol.Request): Promise { const metadata = getInteractiveCellMetadata(this.debugCell); if (request.command === 'setBreakpoints' && metadata && metadata.generatedCode && !this.cellDumpInvoked) { if (!this.debugCellDumped) { @@ -60,5 +61,7 @@ export class DebugCellController implements IDebuggingDelegate { await this.debugCellDumped; this._ready.resolve(); } + + return undefined; } } diff --git a/src/interactive-window/debugger/jupyter/debugger.ts b/src/interactive-window/debugger/jupyter/debugger.ts new file mode 100644 index 00000000000..16ab7ae3818 --- /dev/null +++ b/src/interactive-window/debugger/jupyter/debugger.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +'use strict'; +import { Debugger } from '../../../notebooks/debugger/debugger'; +import { createDeferred } from '../../../platform/common/utils/async'; + +export class IWDebugger extends Debugger { + private readonly _ready = createDeferred(); + public readonly ready = this._ready.promise; + + public resolve() { + this._ready.resolve(); + } +} diff --git a/src/interactive-window/debugger/jupyter/debuggingManager.ts b/src/interactive-window/debugger/jupyter/debuggingManager.ts index b30ca7a08f9..85d79b363b1 100644 --- a/src/interactive-window/debugger/jupyter/debuggingManager.ts +++ b/src/interactive-window/debugger/jupyter/debuggingManager.ts @@ -3,45 +3,45 @@ import { inject, injectable } from 'inversify'; import { - NotebookDocument, + debug, + DebugAdapterDescriptor, DebugAdapterInlineImplementation, DebugSession, - NotebookCell, DebugSessionOptions, - DebugAdapterDescriptor, - NotebookEditor, - debug + NotebookCell, + NotebookDocument, + NotebookEditor } from 'vscode'; +import { IKernelProvider } from '../../../kernels/types'; +import { IControllerLoader, IControllerSelection } from '../../../notebooks/controllers/types'; import { pythonIWKernelDebugAdapter } from '../../../notebooks/debugger/constants'; +import { DebuggingManagerBase } from '../../../notebooks/debugger/debuggingManagerBase'; import { - IDebuggingManager, - KernelDebugMode, - IKernelDebugAdapterConfig, - IDebugLocationTrackerFactory + IDebugLocationTrackerFactory, + IInteractiveWindowDebugConfig, + KernelDebugMode } from '../../../notebooks/debugger/debuggingTypes'; -import { IKernelProvider } from '../../../kernels/types'; -import { IpykernelCheckResult, assertIsDebugConfig } from '../../../notebooks/debugger/helper'; -import { KernelDebugAdapter } from './kernelDebugAdapter'; +import { assertIsInteractiveWindowDebugConfig, IpykernelCheckResult } from '../../../notebooks/debugger/helper'; import { IExtensionSingleActivationService } from '../../../platform/activation/types'; import { - ICommandManager, IApplicationShell, - IVSCodeNotebook, - IDebugService + ICommandManager, + IDebugService, + IVSCodeNotebook } from '../../../platform/common/application/types'; import { IPlatformService } from '../../../platform/common/platform/types'; +import { IConfigurationService } from '../../../platform/common/types'; import { DataScience } from '../../../platform/common/utils/localize'; -import { traceInfoIfCI, traceInfo, traceError } from '../../../platform/logging'; +import { noop } from '../../../platform/common/utils/misc'; +import { IServiceContainer } from '../../../platform/ioc/types'; +import { traceError, traceInfo, traceInfoIfCI } from '../../../platform/logging'; import * as path from '../../../platform/vscode-path/path'; -import { DebugCellController } from './debugCellControllers'; -import { DebuggingManagerBase } from '../../../notebooks/debugger/debuggingManagerBase'; -import { IConfigurationService } from '../../../platform/common/types'; import { IFileGeneratedCodes } from '../../editor-integration/types'; -import { buildSourceMap } from '../helper'; -import { noop } from '../../../platform/common/utils/misc'; import { IInteractiveWindowDebuggingManager } from '../../types'; -import { IControllerLoader, IControllerSelection } from '../../../notebooks/controllers/types'; -import { IServiceContainer } from '../../../platform/ioc/types'; +import { buildSourceMap } from '../helper'; +import { DebugCellController } from './debugCellController'; +import { IWDebugger } from './debugger'; +import { KernelDebugAdapter } from './kernelDebugAdapter'; /** * The DebuggingManager maintains the mapping between notebook documents and debug sessions. @@ -49,7 +49,7 @@ import { IServiceContainer } from '../../../platform/ioc/types'; @injectable() export class InteractiveWindowDebuggingManager extends DebuggingManagerBase - implements IExtensionSingleActivationService, IDebuggingManager, IInteractiveWindowDebuggingManager + implements IExtensionSingleActivationService, IInteractiveWindowDebuggingManager { public constructor( @inject(IKernelProvider) kernelProvider: IKernelProvider, @@ -85,102 +85,86 @@ export class InteractiveWindowDebuggingManager }) ); } + public getDebugMode(_notebook: NotebookDocument): KernelDebugMode | undefined { return KernelDebugMode.InteractiveWindow; } + public async start(editor: NotebookEditor, cell: NotebookCell) { traceInfoIfCI(`Starting debugging IW`); - if (this.notebookInProgress.has(editor.notebook)) { - traceInfo(`Cannot start debugging. Already debugging this notebook`); - return; - } - - if (this.isDebugging(editor.notebook)) { - traceInfo(`Cannot start debugging. Already debugging this notebook document. Toolbar should update`); - return; - } - - const checkIpykernelAndStart = async (allowSelectKernel = true): Promise => { - const ipykernelResult = await this.checkForIpykernel6(editor.notebook); - switch (ipykernelResult) { - case IpykernelCheckResult.NotInstalled: - // User would have been notified about this, nothing more to do. - return; - case IpykernelCheckResult.Outdated: - case IpykernelCheckResult.Unknown: { - this.promptInstallIpykernel6().then(noop, noop); - return; - } - case IpykernelCheckResult.Ok: { - await this.startDebuggingCell(editor.notebook, cell); - return; - } - case IpykernelCheckResult.ControllerNotSelected: { - if (allowSelectKernel) { - await this.commandManager.executeCommand('notebook.selectKernel', { notebookEditor: editor }); - await checkIpykernelAndStart(false); - } - } - } - }; - - try { - this.notebookInProgress.add(editor.notebook); - await checkIpykernelAndStart(); - } catch (e) { - traceInfo(`Error starting debugging: ${e}`); - } finally { - this.notebookInProgress.delete(editor.notebook); + const ipykernelResult = await this.checkIpykernelAndPrompt(cell); + if (ipykernelResult === IpykernelCheckResult.Ok) { + await this.startDebuggingCell(editor.notebook, cell); } } private async startDebuggingCell(doc: NotebookDocument, cell: NotebookCell) { const settings = this.configService.getSettings(doc.uri); - const config: IKernelDebugAdapterConfig = { + const config: IInteractiveWindowDebugConfig = { type: pythonIWKernelDebugAdapter, name: path.basename(doc.uri.toString()), request: 'attach', justMyCode: settings.debugJustMyCode, - __interactiveWindowNotebookUri: doc.uri.toString(), + __notebookUri: doc.uri.toString(), // add a property to the config to know if the session is runByLine __mode: KernelDebugMode.InteractiveWindow, __cellIndex: cell.index }; const opts: DebugSessionOptions = { suppressSaveBeforeStart: true }; - return this.startDebuggingConfig(doc, config, opts); + await this.startDebuggingConfig(config, opts); + const dbgr = this.notebookToDebugger.get(doc); + if (!dbgr) { + traceError('Debugger not found, could not start debugging.'); + return; + } + await (dbgr as IWDebugger).ready; } - protected override async createDebugAdapterDescriptor( - session: DebugSession - ): Promise { - const config = session.configuration; - assertIsDebugConfig(config); - - const activeDoc = config.__interactiveWindowNotebookUri - ? this.vscNotebook.notebookDocuments.find( - (doc) => doc.uri.toString() === config.__interactiveWindowNotebookUri - ) - : this.vscNotebook.activeNotebookEditor?.notebook; - if (!activeDoc || typeof config.__cellIndex !== 'number') { - // This cannot happen. + protected async createDebugAdapterDescriptor(session: DebugSession): Promise { + const config = session.configuration as IInteractiveWindowDebugConfig; + assertIsInteractiveWindowDebugConfig(config); + + const notebook = this.vscNotebook.notebookDocuments.find((doc) => doc.uri.toString() === config.__notebookUri); + if (!notebook || typeof config.__cellIndex !== 'number') { traceError('Invalid debug session for debugging of IW using Jupyter Protocol'); return; } - // TODO we apparently always have a kernel here, clean up typings - const kernel = await this.ensureKernelIsRunning(activeDoc); - const debug = this.getDebuggerByUri(activeDoc); - if (!debug) { + if (this.notebookInProgress.has(notebook)) { + traceInfo(`Cannot start debugging. Already debugging this notebook`); return; } + + if (this.isDebugging(notebook)) { + traceInfo(`Cannot start debugging. Already debugging this notebook document. Toolbar should update`); + return; + } + + const dbgr = new IWDebugger(notebook, config, session); + this.notebookToDebugger.set(notebook, dbgr); + try { + this.notebookInProgress.add(notebook); + return await this.doCreateDebugAdapterDescriptor(config, session, notebook, dbgr); + } finally { + this.notebookInProgress.delete(notebook); + } + } + + private async doCreateDebugAdapterDescriptor( + config: IInteractiveWindowDebugConfig, + session: DebugSession, + notebook: NotebookDocument, + dbgr: IWDebugger + ): Promise { + const kernel = await this.ensureKernelIsRunning(notebook); if (!kernel?.session) { this.appShell.showInformationMessage(DataScience.kernelWasNotStarted()).then(noop, noop); return; } const adapter = new KernelDebugAdapter( session, - debug.document, + notebook, kernel.session, kernel, this.platform, @@ -190,22 +174,21 @@ export class InteractiveWindowDebuggingManager this.disposables.push(adapter.onDidEndSession(this.endSession.bind(this))); - // Wait till we're attached before resolving the session - const cell = activeDoc.cellAt(config.__cellIndex); + const cell = notebook.cellAt(config.__cellIndex); const controller = new DebugCellController(adapter, cell, kernel!); - adapter.setDebuggingDelegate(controller); + adapter.setDebuggingDelegates([controller]); controller.ready - .then(() => debug.resolve(session)) + .then(() => dbgr.resolve()) .catch((ex) => console.error('Failed waiting for controller to be ready', ex)); - this.trackDebugAdapter(activeDoc, adapter); + this.trackDebugAdapter(notebook, adapter); return new DebugAdapterInlineImplementation(adapter); } // TODO: This will likely be needed for mapping breakpoints and such public async updateSourceMaps(notebookEditor: NotebookEditor, hashes: IFileGeneratedCodes[]): Promise { // Make sure that we have an active debugging session at this point - let debugSession = await this.getDebugSession(notebookEditor.notebook); + let debugSession = this.getDebugSession(notebookEditor.notebook); if (debugSession) { traceInfoIfCI(`Sending debug request for source map`); await Promise.all( diff --git a/src/notebooks/debugger/commandRegistry.ts b/src/notebooks/debugger/commandRegistry.ts new file mode 100644 index 00000000000..cd73e8a9387 --- /dev/null +++ b/src/notebooks/debugger/commandRegistry.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { NotebookCell } from 'vscode'; +import { ICommandManager, IVSCodeNotebook } from '../../platform/common/application/types'; + +import { IExtensionSingleActivationService } from '../../platform/activation/types'; +import { Commands } from '../../platform/common/constants'; +import { IDisposable, IDisposableRegistry } from '../../platform/common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { DebuggingTelemetry } from './constants'; +import { INotebookDebuggingManager, KernelDebugMode } from './debuggingTypes'; + +/** + * Class that registers command handlers for interactive window commands. + */ +@injectable() +export class CommandRegistry implements IDisposable, IExtensionSingleActivationService { + constructor( + @inject(INotebookDebuggingManager) private readonly debuggingManager: INotebookDebuggingManager, + @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(ICommandManager) private readonly commandManager: ICommandManager + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.RunByLine, this.runByLine, this)); + this.disposables.push(this.commandManager.registerCommand(Commands.RunByLineNext, this.runByLineNext, this)); + this.disposables.push(this.commandManager.registerCommand(Commands.RunByLineStop, this.runByLineStop, this)); + this.disposables.push( + this.commandManager.registerCommand(Commands.RunAndDebugCell, this.runAndDebugCell, this) + ); + } + + private async runByLine(cell: NotebookCell | undefined) { + sendTelemetryEvent(DebuggingTelemetry.clickedRunByLine); + cell ??= this.getCellFromActiveEditor(); + if (!cell) { + return; + } + + await this.debuggingManager.tryToStartDebugging(KernelDebugMode.RunByLine, cell); + } + + private async runByLineNext(cell: NotebookCell | undefined) { + cell ??= this.getCellFromActiveEditor(); + if (!cell) { + return; + } + + this.debuggingManager.runByLineNext(cell); + } + + private async runByLineStop(cell: NotebookCell | undefined) { + cell ??= this.getCellFromActiveEditor(); + if (!cell) { + return; + } + + this.debuggingManager.runByLineStop(cell); + } + + private async runAndDebugCell(cell: NotebookCell | undefined) { + sendTelemetryEvent(DebuggingTelemetry.clickedRunAndDebugCell); + cell ??= this.getCellFromActiveEditor(); + if (!cell) { + return; + } + + await this.debuggingManager.tryToStartDebugging(KernelDebugMode.Cell, cell); + } + + private getCellFromActiveEditor(): NotebookCell | undefined { + const editor = this.vscNotebook.activeNotebookEditor; + if (editor) { + const range = editor.selections[0]; + if (range) { + return editor.notebook.cellAt(range.start); + } + } + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/notebooks/debugger/debugCellControllers.ts b/src/notebooks/debugger/controllers/debugCellController.ts similarity index 73% rename from src/notebooks/debugger/debugCellControllers.ts rename to src/notebooks/debugger/controllers/debugCellController.ts index 6218440fb6d..e3ada7e3edc 100644 --- a/src/notebooks/debugger/debugCellControllers.ts +++ b/src/notebooks/debugger/controllers/debugCellController.ts @@ -3,14 +3,14 @@ import { NotebookCell } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { IKernel } from '../../kernels/types'; -import { ICommandManager } from '../../platform/common/application/types'; -import { noop } from '../../platform/common/utils/misc'; -import { traceVerbose } from '../../platform/logging'; -import { sendTelemetryEvent } from '../../telemetry'; -import { DebuggingTelemetry } from './constants'; -import { IDebuggingDelegate, IKernelDebugAdapter } from './debuggingTypes'; -import { cellDebugSetup } from './helper'; +import { IKernel } from '../../../kernels/types'; +import { ICommandManager } from '../../../platform/common/application/types'; +import { noop } from '../../../platform/common/utils/misc'; +import { traceVerbose } from '../../../platform/logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { DebuggingTelemetry } from '../constants'; +import { IDebuggingDelegate, IKernelDebugAdapter } from '../debuggingTypes'; +import { cellDebugSetup } from '../helper'; export function isJustMyCodeNotification(msg: string): boolean { return msg.includes('Frame skipped from debugging during step-in'); @@ -44,16 +44,18 @@ export class DebugCellController implements IDebuggingDelegate { return false; } - public async willSendRequest(request: DebugProtocol.Request): Promise { + public async willSendRequest(request: DebugProtocol.Request): Promise { if (request.command === 'configurationDone') { await cellDebugSetup(this.kernel, this.debugAdapter); this.commandManager .executeCommand('notebook.cell.execute', { ranges: [{ start: this.debugCell.index, end: this.debugCell.index + 1 }], - document: this.debugCell.document.uri + document: this.debugCell.notebook.uri }) .then(noop, noop); } + + return undefined; } } diff --git a/src/notebooks/debugger/controllers/restartController.ts b/src/notebooks/debugger/controllers/restartController.ts new file mode 100644 index 00000000000..3ecea1639bf --- /dev/null +++ b/src/notebooks/debugger/controllers/restartController.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { NotebookCell } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IServiceContainer } from '../../../platform/ioc/types'; +import { traceError, traceVerbose } from '../../../platform/logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { DebuggingTelemetry } from '../constants'; +import { IDebuggingDelegate, IKernelDebugAdapter, INotebookDebuggingManager, KernelDebugMode } from '../debuggingTypes'; + +/** + * Implements the "restart" request. + */ +export class RestartController implements IDebuggingDelegate { + private debuggingManager: INotebookDebuggingManager; + + constructor( + private readonly mode: KernelDebugMode, + private readonly debugAdapter: IKernelDebugAdapter, + public readonly debugCell: NotebookCell, + private readonly serviceContainer: IServiceContainer + ) { + sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunByLine); + this.debuggingManager = this.serviceContainer.get(INotebookDebuggingManager); + } + + private trace(tag: string, msg: string) { + traceVerbose(`[Debug-Restart] ${tag}: ${msg}`); + } + + private error(tag: string, msg: string) { + traceError(`[Debug-Restart] ${tag}: ${msg}`); + } + + public async willSendRequest(request: DebugProtocol.Request): Promise { + if (request.command === 'restart') { + // We have to implement restart manually because the previous launch config includes the cell index, but the cell index may have changed. + this.trace('restart', 'Handling restart request'); + setTimeout(() => { + // The restart response has to be sent _before_ the debug session is disconnected - otherwise the pending restart request will be canceled, + // and considered to have failed. eg a call to executeCommand('workbench.action.debug.restart') would fail. + this.debugAdapter + .disconnect() + .then(() => { + this.trace('restart', 'doRestart'); + return this.debuggingManager.tryToStartDebugging(this.mode, this.debugCell); + }) + .catch((err) => { + this.error('restart', `Error restarting: ${err}`); + }); + }, 0); + return { + command: request.command, + request_seq: request.seq, + seq: request.seq, + success: true, + type: 'response' + }; + } + + return undefined; + } + + public async willSendResponse(response: DebugProtocol.Response): Promise { + if (response.command === 'initialize' && response.body) { + (response as DebugProtocol.InitializeResponse).body!.supportsRestartRequest = true; + } + } +} diff --git a/src/notebooks/debugger/runByLineController.ts b/src/notebooks/debugger/controllers/runByLineController.ts similarity index 84% rename from src/notebooks/debugger/runByLineController.ts rename to src/notebooks/debugger/controllers/runByLineController.ts index 20464c2d953..dbf585084a0 100644 --- a/src/notebooks/debugger/runByLineController.ts +++ b/src/notebooks/debugger/controllers/runByLineController.ts @@ -3,22 +3,22 @@ import { NotebookCell } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { IKernel } from '../../kernels/types'; -import { ICommandManager } from '../../platform/common/application/types'; -import { Commands } from '../../platform/common/constants'; -import { IConfigurationService } from '../../platform/common/types'; -import { parseForComments } from '../../platform/common/utils'; -import { noop } from '../../platform/common/utils/misc'; -import { traceInfoIfCI, traceVerbose } from '../../platform/logging'; -import * as path from '../../platform/vscode-path/path'; -import { sendTelemetryEvent } from '../../telemetry'; -import { DebuggingTelemetry } from './constants'; -import { isJustMyCodeNotification } from './debugCellControllers'; -import { IDebuggingDelegate, IKernelDebugAdapter, KernelDebugMode } from './debuggingTypes'; -import { cellDebugSetup } from './helper'; +import { IKernel } from '../../../kernels/types'; +import { ICommandManager } from '../../../platform/common/application/types'; +import { Commands } from '../../../platform/common/constants'; +import { IConfigurationService } from '../../../platform/common/types'; +import { parseForComments } from '../../../platform/common/utils'; +import { noop } from '../../../platform/common/utils/misc'; +import { traceInfoIfCI, traceVerbose } from '../../../platform/logging'; +import * as path from '../../../platform/vscode-path/path'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { DebuggingTelemetry } from '../constants'; +import { IDebuggingDelegate, IKernelDebugAdapter, KernelDebugMode } from '../debuggingTypes'; +import { cellDebugSetup } from '../helper'; +import { isJustMyCodeNotification } from './debugCellController'; /** - * Listens to event when doing run by line and controls the behavior of the debugger (like auto stepping on moving out of a cell) + * Implements the business logic of RBL (like auto stepping on moving out of a cell) */ export class RunByLineController implements IDebuggingDelegate { private lastPausedThreadId: number | undefined; @@ -74,11 +74,13 @@ export class RunByLineController implements IDebuggingDelegate { return false; } - public async willSendRequest(request: DebugProtocol.Request): Promise { + public async willSendRequest(request: DebugProtocol.Request): Promise { traceInfoIfCI(`willSendRequest: ${request.command}`); if (request.command === 'configurationDone') { await this.initializeExecute(); } + + return undefined; } private async handleStoppedEvent(threadId: number): Promise { @@ -150,7 +152,7 @@ export class RunByLineController implements IDebuggingDelegate { this.commandManager .executeCommand('notebook.cell.execute', { ranges: [{ start: this.debugCell.index, end: this.debugCell.index + 1 }], - document: this.debugCell.document.uri + document: this.debugCell.notebook.uri }) .then(noop, noop); } diff --git a/src/notebooks/debugger/debugger.ts b/src/notebooks/debugger/debugger.ts index 158905281e9..69e54c69014 100644 --- a/src/notebooks/debugger/debugger.ts +++ b/src/notebooks/debugger/debugger.ts @@ -2,44 +2,12 @@ // Licensed under the MIT License. 'use strict'; -import { debug, NotebookDocument, DebugSession, DebugSessionOptions, DebugConfiguration } from 'vscode'; -import { noop } from '../../platform/common/utils/misc'; +import { DebugConfiguration, DebugSession, NotebookDocument } from 'vscode'; -/** - * Wraps debug start in a promise - */ export class Debugger { - private resolveFunc?: (value: DebugSession) => void; - private rejectFunc?: (reason?: Error) => void; - - readonly session: Promise; - constructor( public readonly document: NotebookDocument, public readonly config: DebugConfiguration, - options?: DebugSessionOptions - ) { - this.session = new Promise((resolve, reject) => { - this.resolveFunc = resolve; - this.rejectFunc = reject; - - debug.startDebugging(undefined, config, options).then(undefined, reject); - }); - } - - resolve(session: DebugSession) { - if (this.resolveFunc) { - this.resolveFunc(session); - } - } - - reject(reason: Error) { - if (this.rejectFunc) { - this.rejectFunc(reason); - } - } - - async stop() { - void debug.stopDebugging(await this.session).then(noop, noop); - } + public readonly session: DebugSession + ) {} } diff --git a/src/notebooks/debugger/debuggerVariables.ts b/src/notebooks/debugger/debuggerVariables.ts index d76a64e6100..6d4ae9a98e8 100644 --- a/src/notebooks/debugger/debuggerVariables.ts +++ b/src/notebooks/debugger/debuggerVariables.ts @@ -8,19 +8,8 @@ import * as uriPath from '../../platform/vscode-path/resources'; import { DebugAdapterTracker, Disposable, Event, EventEmitter } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { Identifiers } from '../../platform/common/constants'; -import { IDebugService, IVSCodeNotebook } from '../../platform/common/application/types'; -import { traceError, traceVerbose } from '../../platform/logging'; -import { - IConfigurationService, - IDataFrameScriptGenerator, - IVariableScriptGenerator, - Resource -} from '../../platform/common/types'; -import { DebugLocationTracker } from './debugLocationTracker'; -import { sendTelemetryEvent, Telemetry } from '../../telemetry'; -import { IDebuggingManager, IJupyterDebugService, KernelDebugMode } from './debuggingTypes'; import { IKernel } from '../../kernels/types'; +import { convertDebugProtocolVariableToIJupyterVariable, DataViewableTypes } from '../../kernels/variables/helpers'; import { parseDataFrame } from '../../kernels/variables/pythonVariableRequester'; import { IConditionalJupyterVariables, @@ -28,8 +17,19 @@ import { IJupyterVariablesRequest, IJupyterVariablesResponse } from '../../kernels/variables/types'; -import { convertDebugProtocolVariableToIJupyterVariable, DataViewableTypes } from '../../kernels/variables/helpers'; +import { IDebugService, IVSCodeNotebook } from '../../platform/common/application/types'; +import { Identifiers } from '../../platform/common/constants'; +import { + IConfigurationService, + IDataFrameScriptGenerator, + IVariableScriptGenerator, + Resource +} from '../../platform/common/types'; import { noop } from '../../platform/common/utils/misc'; +import { traceError, traceVerbose } from '../../platform/logging'; +import { sendTelemetryEvent, Telemetry } from '../../telemetry'; +import { IJupyterDebugService, INotebookDebuggingManager, KernelDebugMode } from './debuggingTypes'; +import { DebugLocationTracker } from './debugLocationTracker'; const KnownExcludedVariables = new Set(['In', 'Out', 'exit', 'quit']); const MaximumRowChunkSizeForDebugger = 100; @@ -53,7 +53,7 @@ export class DebuggerVariables constructor( @inject(IJupyterDebugService) @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) private debugService: IDebugService, - @inject(IDebuggingManager) private readonly debuggingManager: IDebuggingManager, + @inject(INotebookDebuggingManager) private readonly debuggingManager: INotebookDebuggingManager, @inject(IConfigurationService) private configService: IConfigurationService, @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, @inject(IVariableScriptGenerator) private readonly varScriptGenerator: IVariableScriptGenerator, @@ -434,7 +434,7 @@ export class DebuggerVariables const threadId = stoppedMessage.body.threadId; if (doc) { - const session = await this.debuggingManager.getDebugSession(doc); + const session = this.debuggingManager.getDebugSession(doc); if (session) { // Call stack trace const stResponse: DebugProtocol.StackTraceResponse['body'] = await session.customRequest('stackTrace', { diff --git a/src/notebooks/debugger/debuggingManager.ts b/src/notebooks/debugger/debuggingManager.ts index 2b25cdeea7d..7c815537882 100644 --- a/src/notebooks/debugger/debuggingManager.ts +++ b/src/notebooks/debugger/debuggingManager.ts @@ -12,7 +12,6 @@ import { DebugSessionOptions, NotebookCell, NotebookDocument, - NotebookEditor, Uri } from 'vscode'; import { IKernelProvider } from '../../kernels/types'; @@ -23,26 +22,27 @@ import { IDebugService, IVSCodeNotebook } from '../../platform/common/application/types'; -import { Commands as DSCommands, EditorContexts } from '../../platform/common/constants'; +import { EditorContexts } from '../../platform/common/constants'; import { ContextKey } from '../../platform/common/contextKey'; import { IPlatformService } from '../../platform/common/platform/types'; import { IConfigurationService } from '../../platform/common/types'; import { DataScience } from '../../platform/common/utils/localize'; import { noop } from '../../platform/common/utils/misc'; import { IServiceContainer } from '../../platform/ioc/types'; -import { traceError, traceInfo, traceInfoIfCI } from '../../platform/logging'; +import { traceInfo } from '../../platform/logging'; import { ResourceSet } from '../../platform/vscode-path/map'; import * as path from '../../platform/vscode-path/path'; import { sendTelemetryEvent } from '../../telemetry'; import { IControllerLoader, IControllerSelection } from '../controllers/types'; import { DebuggingTelemetry, pythonKernelDebugAdapter } from './constants'; -import { DebugCellController } from './debugCellControllers'; +import { DebugCellController } from './controllers/debugCellController'; +import { RestartController } from './controllers/restartController'; +import { RunByLineController } from './controllers/runByLineController'; +import { Debugger } from './debugger'; import { DebuggingManagerBase } from './debuggingManagerBase'; -import { IDebuggingManager, IKernelDebugAdapterConfig, KernelDebugMode } from './debuggingTypes'; +import { INotebookDebugConfig, INotebookDebuggingManager, KernelDebugMode } from './debuggingTypes'; import { assertIsDebugConfig, IpykernelCheckResult } from './helper'; import { KernelDebugAdapter } from './kernelDebugAdapter'; -import { KernelDebugAdapterBase } from './kernelDebugAdapterBase'; -import { RunByLineController } from './runByLineController'; /** * The DebuggingManager maintains the mapping between notebook documents and debug sessions. @@ -50,7 +50,7 @@ import { RunByLineController } from './runByLineController'; @injectable() export class DebuggingManager extends DebuggingManagerBase - implements IExtensionSingleActivationService, IDebuggingManager + implements IExtensionSingleActivationService, INotebookDebuggingManager { private runByLineCells: ContextKey; private runByLineDocuments: ContextKey; @@ -89,76 +89,6 @@ export class DebuggingManager // factory for kernel debug adapters debug.registerDebugAdapterDescriptorFactory(pythonKernelDebugAdapter, { createDebugAdapterDescriptor: async (session) => this.createDebugAdapterDescriptor(session) - }), - this.commandManager.registerCommand(DSCommands.DebugNotebook, async () => { - const editor = this.vscNotebook.activeNotebookEditor; - await this.tryToStartDebugging(KernelDebugMode.Everything, editor); - }), - - this.commandManager.registerCommand(DSCommands.RunByLine, async (cell: NotebookCell | undefined) => { - sendTelemetryEvent(DebuggingTelemetry.clickedRunByLine); - const editor = this.vscNotebook.activeNotebookEditor; - if (!cell) { - const range = editor?.selections[0]; - if (range) { - cell = editor?.notebook.cellAt(range.start); - } - } - - if (!cell) { - return; - } - - await this.tryToStartDebugging(KernelDebugMode.RunByLine, editor, cell); - }), - - this.commandManager.registerCommand(DSCommands.RunByLineNext, (cell: NotebookCell | undefined) => { - if (!cell) { - const editor = this.vscNotebook.activeNotebookEditor; - const range = editor?.selections[0]; - if (range) { - cell = editor?.notebook.cellAt(range.start); - } - } - - if (!cell) { - return; - } - - const controller = this.notebookToRunByLineController.get(cell.notebook); - if (controller && controller.debugCell.document.uri.toString() === cell.document.uri.toString()) { - controller.continue(); - } - }), - - this.commandManager.registerCommand(DSCommands.RunByLineStop, () => { - const editor = this.vscNotebook.activeNotebookEditor; - if (editor) { - const controller = this.notebookToRunByLineController.get(editor.notebook); - if (controller) { - sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { - reason: 'withKeybinding' - }); - controller.stop(); - } - } - }), - - this.commandManager.registerCommand(DSCommands.RunAndDebugCell, async (cell: NotebookCell | undefined) => { - sendTelemetryEvent(DebuggingTelemetry.clickedRunAndDebugCell); - const editor = this.vscNotebook.activeNotebookEditor; - if (!cell) { - const range = editor?.selections[0]; - if (range) { - cell = editor?.notebook.cellAt(range.start); - } - } - - if (!cell) { - return; - } - - await this.tryToStartDebugging(KernelDebugMode.Cell, editor, cell); }) ); } @@ -194,89 +124,46 @@ export class DebuggingManager this.debugDocuments.set(Array.from(debugDocumentUris.values())).ignoreErrors(); } - private async tryToStartDebugging(mode: KernelDebugMode, editor?: NotebookEditor, cell?: NotebookCell) { - traceInfoIfCI(`Starting debugging with mode ${mode}`); + public async tryToStartDebugging(mode: KernelDebugMode, cell: NotebookCell) { + traceInfo(`Starting debugging with mode ${mode}`); - if (!editor) { - this.appShell.showErrorMessage(DataScience.noNotebookToDebug()).then(noop, noop); - return; - } - - if (this.notebookInProgress.has(editor.notebook)) { - traceInfo(`Cannot start debugging. Already debugging this notebook`); - return; + const ipykernelResult = await this.checkIpykernelAndPrompt(cell); + if (ipykernelResult === IpykernelCheckResult.Ok) { + if (mode === KernelDebugMode.RunByLine || mode === KernelDebugMode.Cell) { + await this.startDebuggingCell(mode, cell!); + } } + } - if (this.isDebugging(editor.notebook)) { - traceInfo(`Cannot start debugging. Already debugging this notebook document.`); - return; + public runByLineNext(cell: NotebookCell) { + const controller = this.notebookToRunByLineController.get(cell.notebook); + if (controller && controller.debugCell.document.uri.toString() === cell.document.uri.toString()) { + controller.continue(); } + } - const checkIpykernelAndStart = async (allowSelectKernel = true): Promise => { - const ipykernelResult = await this.checkForIpykernel6(editor.notebook); - switch (ipykernelResult) { - case IpykernelCheckResult.NotInstalled: - // User would have been notified about this, nothing more to do. - return; - case IpykernelCheckResult.Outdated: - case IpykernelCheckResult.Unknown: { - this.promptInstallIpykernel6().then(noop, noop); - return; - } - case IpykernelCheckResult.Ok: { - switch (mode) { - case KernelDebugMode.Everything: { - await this.startDebugging(editor.notebook); - return; - } - case KernelDebugMode.Cell: - if (cell) { - await this.startDebuggingCell(editor.notebook, KernelDebugMode.Cell, cell); - } - return; - case KernelDebugMode.RunByLine: - if (cell) { - await this.startDebuggingCell(editor.notebook, KernelDebugMode.RunByLine, cell); - } - return; - default: - return; - } - } - case IpykernelCheckResult.ControllerNotSelected: { - if (allowSelectKernel) { - await this.commandManager.executeCommand('notebook.selectKernel', { notebookEditor: editor }); - await checkIpykernelAndStart(false); - } - } - } - }; - - try { - this.notebookInProgress.add(editor.notebook); - this.updateDebugContextKey(); - await checkIpykernelAndStart(); - } catch (e) { - traceInfo(`Error starting debugging: ${e}`); - } finally { - this.notebookInProgress.delete(editor.notebook); - this.updateDebugContextKey(); + public runByLineStop(cell: NotebookCell) { + const controller = this.notebookToRunByLineController.get(cell.notebook); + if (controller) { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { + reason: 'withKeybinding' + }); + controller.stop(); } } - private async startDebuggingCell( - doc: NotebookDocument, - mode: KernelDebugMode.Cell | KernelDebugMode.RunByLine, - cell: NotebookCell - ) { + + private async startDebuggingCell(mode: KernelDebugMode.Cell | KernelDebugMode.RunByLine, cell: NotebookCell) { + const doc = cell.notebook; const settings = this.configurationService.getSettings(doc.uri); - const config: IKernelDebugAdapterConfig = { + const config: INotebookDebugConfig = { type: pythonKernelDebugAdapter, name: path.basename(doc.uri.toString()), request: 'attach', justMyCode: settings.debugJustMyCode, // add a property to the config to know if the session is runByLine __mode: mode, - __cellIndex: cell.index + __cellIndex: cell.index, + __notebookUri: doc.uri.toString() }; const opts: DebugSessionOptions | undefined = mode === KernelDebugMode.RunByLine @@ -287,81 +174,90 @@ export class DebuggingManager suppressSaveBeforeStart: true } : { suppressSaveBeforeStart: true }; - return this.startDebuggingConfig(doc, config, opts); - } - - private async startDebugging(doc: NotebookDocument) { - const config: IKernelDebugAdapterConfig = { - type: pythonKernelDebugAdapter, - name: path.basename(doc.uri.toString()), - request: 'attach', - internalConsoleOptions: 'neverOpen', - justMyCode: false, - __mode: KernelDebugMode.Everything - }; - return this.startDebuggingConfig(doc, config); - } - - protected override trackDebugAdapter(notebook: NotebookDocument, adapter: KernelDebugAdapterBase): void { - super.trackDebugAdapter(notebook, adapter); - this.updateDebugContextKey(); + return this.startDebuggingConfig(config, opts); } - protected override async createDebugAdapterDescriptor( - session: DebugSession - ): Promise { + protected async createDebugAdapterDescriptor(session: DebugSession): Promise { const config = session.configuration; assertIsDebugConfig(config); - const activeDoc = config.__interactiveWindowNotebookUri - ? this.vscNotebook.notebookDocuments.find( - (doc) => doc.uri.toString() === config.__interactiveWindowNotebookUri - ) - : this.vscNotebook.activeNotebookEditor?.notebook; - if (activeDoc) { - // TODO we apparently always have a kernel here, clean up typings - const kernel = await this.ensureKernelIsRunning(activeDoc); - const debug = this.getDebuggerByUri(activeDoc); - if (debug) { - if (kernel?.session) { - const adapter = new KernelDebugAdapter( - session, - debug.document, - kernel.session, - kernel, - this.platform, - this.debugService - ); + const notebookUri = config.__notebookUri; + const notebook = this.vscNotebook.notebookDocuments.find((doc) => doc.uri.toString() === notebookUri); + + if (!notebook) { + traceInfo(`Cannot start debugging. Notebook ${notebookUri} not found.`); + return; + } + + if (this.notebookInProgress.has(notebook)) { + traceInfo(`Cannot start debugging. Already debugging this notebook`); + return; + } - if (config.__mode === KernelDebugMode.RunByLine && typeof config.__cellIndex === 'number') { - const cell = activeDoc.cellAt(config.__cellIndex); - const controller = new RunByLineController( - adapter, - cell, - this.commandManager, - kernel!, - this.configurationService - ); - adapter.setDebuggingDelegate(controller); - this.notebookToRunByLineController.set(debug.document, controller); - this.updateRunByLineContextKeys(); - } else if (config.__mode === KernelDebugMode.Cell && typeof config.__cellIndex === 'number') { - const cell = activeDoc.cellAt(config.__cellIndex); - const controller = new DebugCellController(adapter, cell, kernel!, this.commandManager); - adapter.setDebuggingDelegate(controller); - } + if (this.isDebugging(notebook)) { + traceInfo(`Cannot start debugging. Already debugging this notebook document.`); + return; + } - this.trackDebugAdapter(debug.document, adapter); + this.notebookToDebugger.set(notebook, new Debugger(notebook, config, session)); + try { + this.notebookInProgress.add(notebook); + this.updateDebugContextKey(); + return await this.doCreateDebugAdapterDescriptor(config, session, notebook); + } finally { + this.notebookInProgress.delete(notebook); + this.updateDebugContextKey(); + } + } - // Wait till we're attached before resolving the session - debug.resolve(session); - return new DebugAdapterInlineImplementation(adapter); - } else { - this.appShell.showInformationMessage(DataScience.kernelWasNotStarted()).then(noop, noop); - } + private async doCreateDebugAdapterDescriptor( + config: INotebookDebugConfig, + session: DebugSession, + notebook: NotebookDocument + ): Promise { + const kernel = await this.ensureKernelIsRunning(notebook); + if (kernel?.session) { + const adapter = new KernelDebugAdapter( + session, + notebook, + kernel.session, + kernel, + this.platform, + this.debugService + ); + + if (config.__mode === KernelDebugMode.RunByLine && typeof config.__cellIndex === 'number') { + const cell = notebook.cellAt(config.__cellIndex); + const rblController = new RunByLineController( + adapter, + cell, + this.commandManager, + kernel!, + this.configurationService + ); + adapter.setDebuggingDelegates([ + rblController, + new RestartController(KernelDebugMode.RunByLine, adapter, cell, this.serviceContainer) + ]); + this.notebookToRunByLineController.set(notebook, rblController); + this.updateRunByLineContextKeys(); + } else if (config.__mode === KernelDebugMode.Cell && typeof config.__cellIndex === 'number') { + const cell = notebook.cellAt(config.__cellIndex); + const controller = new DebugCellController(adapter, cell, kernel!, this.commandManager); + adapter.setDebuggingDelegates([ + controller, + new RestartController(KernelDebugMode.Cell, adapter, cell, this.serviceContainer) + ]); } + + this.trackDebugAdapter(notebook, adapter); + this.updateDebugContextKey(); + + return new DebugAdapterInlineImplementation(adapter); + } else { + this.appShell.showInformationMessage(DataScience.kernelWasNotStarted()).then(noop, noop); } - traceError('Debug sessions should start only from the cell toolbar command'); + return; } } diff --git a/src/notebooks/debugger/debuggingManagerBase.ts b/src/notebooks/debugger/debuggingManagerBase.ts index 8dac8b6708e..ac655851f37 100644 --- a/src/notebooks/debugger/debuggingManagerBase.ts +++ b/src/notebooks/debugger/debuggingManagerBase.ts @@ -5,28 +5,29 @@ import { debug, - NotebookDocument, - workspace, DebugSession, DebugSessionOptions, - DebugAdapterDescriptor, Event, EventEmitter, - NotebookCell + NotebookCell, + NotebookDocument, + NotebookEditor, + workspace, + window } from 'vscode'; import { IKernel, IKernelProvider } from '../../kernels/types'; -import { IDisposable } from '../../platform/common/types'; import { IApplicationShell, ICommandManager, IVSCodeNotebook } from '../../platform/common/application/types'; -import { DebuggingTelemetry } from './constants'; -import { sendTelemetryEvent } from '../../telemetry'; -import { traceError, traceInfoIfCI } from '../../platform/logging'; +import { IDisposable } from '../../platform/common/types'; import { DataScience } from '../../platform/common/utils/localize'; -import { IKernelDebugAdapterConfig } from './debuggingTypes'; -import { Debugger } from './debugger'; -import { KernelDebugAdapterBase } from './kernelDebugAdapterBase'; -import { IpykernelCheckResult, isUsingIpykernel6OrLater } from './helper'; import { noop } from '../../platform/common/utils/misc'; +import { traceError, traceInfo, traceInfoIfCI } from '../../platform/logging'; +import { sendTelemetryEvent } from '../../telemetry'; import { IControllerLoader, IControllerSelection } from '../controllers/types'; +import { DebuggingTelemetry } from './constants'; +import { Debugger } from './debugger'; +import { IDebuggingManager, INotebookDebugConfig, KernelDebugMode } from './debuggingTypes'; +import { IpykernelCheckResult, isUsingIpykernel6OrLater } from './helper'; +import { KernelDebugAdapterBase } from './kernelDebugAdapterBase'; import { KernelConnector } from '../controllers/kernelConnector'; import { IServiceContainer } from '../../platform/ioc/types'; import { DisplayOptions } from '../../kernels/displayOptions'; @@ -34,8 +35,8 @@ import { DisplayOptions } from '../../kernels/displayOptions'; /** * The DebuggingManager maintains the mapping between notebook documents and debug sessions. */ -export abstract class DebuggingManagerBase implements IDisposable { - private notebookToDebugger = new Map(); +export abstract class DebuggingManagerBase implements IDisposable, IDebuggingManager { + protected notebookToDebugger = new Map(); protected notebookToDebugAdapter = new Map(); protected notebookInProgress = new Set(); protected readonly disposables: IDisposable[] = []; @@ -60,12 +61,15 @@ export abstract class DebuggingManagerBase implements IDisposable { workspace.onDidCloseNotebookDocument(async (document) => { const dbg = this.notebookToDebugger.get(document); if (dbg) { - await dbg.stop(); + await debug.stopDebugging(dbg.session); this.onDidStopDebugging(document); } }) ); } + + abstract getDebugMode(notebook: NotebookDocument): KernelDebugMode | undefined; + public getDebugCell(notebook: NotebookDocument): NotebookCell | undefined { return this.notebookToDebugAdapter.get(notebook)?.debugCell; } @@ -82,12 +86,13 @@ export abstract class DebuggingManagerBase implements IDisposable { return this.notebookToDebugger.has(notebook); } - public getDebugSession(notebook: NotebookDocument): Promise | undefined { + public getDebugSession(notebook: NotebookDocument): DebugSession | undefined { const dbg = this.notebookToDebugger.get(notebook); if (dbg) { return dbg.session; } } + public getDebugAdapter(notebook: NotebookDocument): KernelDebugAdapterBase | undefined { return this.notebookToDebugAdapter.get(notebook); } @@ -96,26 +101,14 @@ export abstract class DebuggingManagerBase implements IDisposable { // } - protected async startDebuggingConfig( - doc: NotebookDocument, - config: IKernelDebugAdapterConfig, - options?: DebugSessionOptions - ) { + protected async startDebuggingConfig(config: INotebookDebugConfig, options?: DebugSessionOptions) { traceInfoIfCI(`Attempting to start debugging with config ${JSON.stringify(config)}`); - let dbg = this.notebookToDebugger.get(doc); - if (!dbg) { - dbg = new Debugger(doc, config, options); - this.notebookToDebugger.set(doc, dbg); - - try { - const session = await dbg.session; - traceInfoIfCI(`Debugger session is ready. Should be debugging ${session.id} now`); - } catch (err) { - traceError(`Can't start debugging (${err})`); - this.appShell.showErrorMessage(DataScience.cantStartDebugging()).then(noop, noop); - } - } else { - traceInfoIfCI(`Not starting debugging because already debugging in this notebook`); + + try { + await debug.startDebugging(undefined, config, options); + } catch (err) { + traceError(`Can't start debugging (${err})`); + this.appShell.showErrorMessage(DataScience.cantStartDebugging()).then(noop, noop); } } @@ -123,11 +116,12 @@ export abstract class DebuggingManagerBase implements IDisposable { this.notebookToDebugAdapter.set(notebook, adapter); this.disposables.push(adapter.onDidEndSession(this.endSession.bind(this))); } + protected async endSession(session: DebugSession) { - traceInfoIfCI(`Ending debug session ${session.id}`); + traceInfo(`Ending debug session ${session.id}`); this._doneDebugging.fire(); for (const [doc, dbg] of this.notebookToDebugger.entries()) { - if (dbg && session.id === (await dbg.session).id) { + if (dbg && session.id === dbg.session.id) { this.notebookToDebugger.delete(doc); this.notebookToDebugAdapter.delete(doc); this.onDidStopDebugging(doc); @@ -136,8 +130,6 @@ export abstract class DebuggingManagerBase implements IDisposable { } } - protected abstract createDebugAdapterDescriptor(session: DebugSession): Promise; - protected getDebuggerByUri(document: NotebookDocument): Debugger | undefined { for (const [doc, dbg] of this.notebookToDebugger.entries()) { if (document === doc) { @@ -167,7 +159,43 @@ export abstract class DebuggingManagerBase implements IDisposable { return kernel; } - protected async checkForIpykernel6(doc: NotebookDocument): Promise { + private findEditorForCell(cell: NotebookCell): NotebookEditor | undefined { + const notebookUri = cell.notebook.uri.toString(); + return window.visibleNotebookEditors.find((e) => e.notebook.uri.toString() === notebookUri); + } + + protected async checkIpykernelAndPrompt( + cell: NotebookCell, + allowSelectKernel: boolean = true + ): Promise { + const editor = this.findEditorForCell(cell); + if (!editor) { + this.appShell.showErrorMessage(DataScience.noNotebookToDebug()).then(noop, noop); + return IpykernelCheckResult.Unknown; + } + + const ipykernelResult = await this.checkForIpykernel6(editor.notebook); + switch (ipykernelResult) { + case IpykernelCheckResult.NotInstalled: + // User would have been notified about this, nothing more to do. + break; + case IpykernelCheckResult.Outdated: + case IpykernelCheckResult.Unknown: { + this.promptInstallIpykernel6().then(noop, noop); + break; + } + case IpykernelCheckResult.ControllerNotSelected: { + if (allowSelectKernel) { + await this.commandManager.executeCommand('notebook.selectKernel', { notebookEditor: editor }); + return await this.checkIpykernelAndPrompt(cell, false); + } + } + } + + return ipykernelResult; + } + + private async checkForIpykernel6(doc: NotebookDocument): Promise { try { let kernel = this.kernelProvider.get(doc); if (!kernel) { @@ -193,7 +221,7 @@ export abstract class DebuggingManagerBase implements IDisposable { return IpykernelCheckResult.Unknown; } - protected async promptInstallIpykernel6() { + private async promptInstallIpykernel6() { const response = await this.appShell.showInformationMessage( DataScience.needIpykernel6(), { modal: true }, diff --git a/src/notebooks/debugger/debuggingTypes.ts b/src/notebooks/debugger/debuggingTypes.ts index 7d8831ccc1e..e218a59b2b6 100644 --- a/src/notebooks/debugger/debuggingTypes.ts +++ b/src/notebooks/debugger/debuggingTypes.ts @@ -68,19 +68,25 @@ export interface IKernelDebugAdapter extends DebugAdapter { disconnect(): Promise; onDidEndSession: Event; dumpAllCells(): Promise; - getConfiguration(): IKernelDebugAdapterConfig; + getConfiguration(): IBaseNotebookDebugConfig; } -export const IDebuggingManager = Symbol('IDebuggingManager'); export interface IDebuggingManager { readonly onDoneDebugging: Event; isDebugging(notebook: NotebookDocument): boolean; getDebugMode(notebook: NotebookDocument): KernelDebugMode | undefined; - getDebugSession(notebook: NotebookDocument): Promise | undefined; + getDebugSession(notebook: NotebookDocument): DebugSession | undefined; getDebugCell(notebook: NotebookDocument): NotebookCell | undefined; getDebugAdapter(notebook: NotebookDocument): IKernelDebugAdapter | undefined; } +export const INotebookDebuggingManager = Symbol('INotebookDebuggingManager'); +export interface INotebookDebuggingManager extends IDebuggingManager { + tryToStartDebugging(mode: KernelDebugMode, cell: NotebookCell): Promise; + runByLineNext(cell: NotebookCell): void; + runByLineStop(cell: NotebookCell): void; +} + export interface IDebuggingDelegate { /** * Called for every event sent from the debug adapter to the client. Returns true to signal that sending the message is vetoed. @@ -88,9 +94,14 @@ export interface IDebuggingDelegate { willSendEvent?(msg: DebugProtocol.Event): Promise; /** - * Called for every request sent from the client to the debug adapter. + * Called for every request sent from the client to the debug adapter. Returns true to signal that the request was handled by the delegate. + */ + willSendRequest?(request: DebugProtocol.Request): undefined | Promise; + + /** + * Called for every response returned from the debug adapter to the client. */ - willSendRequest?(request: DebugProtocol.Request): Promise; + willSendResponse?(request: DebugProtocol.Response): Promise; } export interface IDumpCellResponse { @@ -115,14 +126,29 @@ export interface IDebugInfoResponseBreakpoint { export enum KernelDebugMode { RunByLine, Cell, - Everything, InteractiveWindow } -export interface IKernelDebugAdapterConfig extends DebugConfiguration { +interface IBaseNotebookDebugConfig extends DebugConfiguration { __mode: KernelDebugMode; - __cellIndex?: number; - __interactiveWindowNotebookUri?: string; + __notebookUri: string; +} + +export type INotebookDebugConfig = IRunByLineDebugConfig | ICellDebugConfig | IInteractiveWindowDebugConfig; + +export interface IRunByLineDebugConfig extends IBaseNotebookDebugConfig { + __mode: KernelDebugMode.RunByLine; + __cellIndex: number; +} + +export interface ICellDebugConfig extends IBaseNotebookDebugConfig { + __mode: KernelDebugMode.Cell; + __cellIndex: number; +} + +export interface IInteractiveWindowDebugConfig extends IBaseNotebookDebugConfig { + __mode: KernelDebugMode.InteractiveWindow; + __cellIndex: number; } export interface IDebugLocation { diff --git a/src/notebooks/debugger/helper.ts b/src/notebooks/debugger/helper.ts index a5bcb68edd2..50dece16444 100644 --- a/src/notebooks/debugger/helper.ts +++ b/src/notebooks/debugger/helper.ts @@ -1,10 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { IDebugEventMsg } from '@jupyterlab/services/lib/kernel/messages'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { IKernelDebugAdapter, IKernelDebugAdapterConfig, KernelDebugMode } from './debuggingTypes'; import { IKernel } from '../../kernels/types'; -import { IDebugEventMsg } from '@jupyterlab/services/lib/kernel/messages'; +import { + IInteractiveWindowDebugConfig, + IKernelDebugAdapter, + INotebookDebugConfig, + KernelDebugMode +} from './debuggingTypes'; export enum IpykernelCheckResult { Unknown, @@ -29,9 +34,10 @@ builtins.print(ipykernel.__version__)`; return IpykernelCheckResult.Unknown; } -export function assertIsDebugConfig(thing: unknown): asserts thing is IKernelDebugAdapterConfig { - const config = thing as IKernelDebugAdapterConfig; +export function assertIsDebugConfig(thing: unknown): asserts thing is INotebookDebugConfig { + const config = thing as INotebookDebugConfig; if ( + typeof config.__notebookUri === 'undefined' || typeof config.__mode === 'undefined' || ((config.__mode === KernelDebugMode.Cell || config.__mode === KernelDebugMode.InteractiveWindow || @@ -42,6 +48,13 @@ export function assertIsDebugConfig(thing: unknown): asserts thing is IKernelDeb } } +export function assertIsInteractiveWindowDebugConfig(thing: unknown): asserts thing is IInteractiveWindowDebugConfig { + assertIsDebugConfig(thing); + if (thing.__mode !== KernelDebugMode.InteractiveWindow) { + throw new Error('Invalid launch configuration'); + } +} + export function getMessageSourceAndHookIt( msg: DebugProtocol.ProtocolMessage, sourceHook: ( diff --git a/src/notebooks/debugger/kernelDebugAdapterBase.ts b/src/notebooks/debugger/kernelDebugAdapterBase.ts index 5d2dba511a9..237ebd110f9 100644 --- a/src/notebooks/debugger/kernelDebugAdapterBase.ts +++ b/src/notebooks/debugger/kernelDebugAdapterBase.ts @@ -36,7 +36,7 @@ import { IDebuggingDelegate, IDebugInfoResponse, IKernelDebugAdapter, - IKernelDebugAdapterConfig, + INotebookDebugConfig, KernelDebugMode } from './debuggingTypes'; import { @@ -60,9 +60,9 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb protected readonly fileToCell = new Map(); private readonly sendMessage = new EventEmitter(); private readonly endSession = new EventEmitter(); - private readonly configuration: IKernelDebugAdapterConfig; + private readonly configuration: INotebookDebugConfig; protected readonly disposables: IDisposable[] = []; - private delegate: IDebuggingDelegate | undefined; + private delegates: IDebuggingDelegate[] | undefined; onDidSendMessage: Event = this.sendMessage.event; onDidEndSession: Event = this.endSession.event; public readonly debugCell: NotebookCell | undefined; @@ -148,8 +148,8 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb ); } - public setDebuggingDelegate(delegate: IDebuggingDelegate) { - this.delegate = delegate; + public setDebuggingDelegates(delegates: IDebuggingDelegate[]) { + this.delegates = delegates; } private trace(tag: string, msg: string) { @@ -160,11 +160,19 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb traceInfoIfCI(`Debug IO Pub message: ${JSON.stringify(msg)}`); if (isDebugEventMsg(msg)) { this.trace('event', JSON.stringify(msg)); - if (!this.delegate?.willSendEvent || !(await this.delegate.willSendEvent(msg.content))) { - this.sendMessage.fire(msg.content); + for (const d of this.delegates ?? []) { + if (await d?.willSendEvent?.(msg.content)) { + return; + } } + + this.sendMessage.fire(msg.content); } } + + /** + * Handle a message from the client to the debug adapter. + */ async handleMessage(message: DebugProtocol.ProtocolMessage) { try { traceInfoIfCI(`KernelDebugAdapter::handleMessage ${JSON.stringify(message, undefined, ' ')}`); @@ -190,7 +198,14 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb } if (message.type === 'request') { - await this.delegate?.willSendRequest?.(message as DebugProtocol.Request); + for (const d of this.delegates ?? []) { + const response = await d?.willSendRequest?.(message as DebugProtocol.Request); + if (response) { + this.trace('response', JSON.stringify(response)); + this.sendMessage.fire(response); + return; + } + } } return this.sendRequestToJupyterSession(message); @@ -199,7 +214,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb } } - public getConfiguration(): IKernelDebugAdapterConfig { + public getConfiguration(): INotebookDebugConfig { return this.configuration; } @@ -212,7 +227,12 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb this.disconnected = true; if (this.debugService.activeDebugSession === this.session) { try { - await this.session.customRequest('disconnect', { restart: false }); + await Promise.all([ + this.deleteDumpedFiles().catch((ex) => + traceWarning('Error deleting temporary debug files.', ex) + ), + this.session.customRequest('disconnect', { restart: false }) + ]); } catch (e) { traceError(`Failed to disconnect debug session`, e); } @@ -223,7 +243,6 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb } dispose() { - this.deleteDumpedFiles().catch((ex) => traceWarning('Error deleting temporary debug files.', ex)); this.disposables.forEach((d) => d.dispose()); } @@ -300,10 +319,14 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb true ); - control.onReply = (msg) => { - const message = msg.content as DebugProtocol.ProtocolMessage; + control.onReply = async (msg) => { + const message = msg.content as DebugProtocol.Response; getMessageSourceAndHookIt(message, this.translateDebuggerFileToRealFile.bind(this)); + for (const d of this.delegates ?? []) { + await d?.willSendResponse?.(message); + } + this.trace('response', JSON.stringify(message)); this.sendMessage.fire(message); }; @@ -343,7 +366,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb ): void; protected abstract getDumpFilesForDeletion(): string[]; - private async deleteDumpedFiles() { + private async deleteDumpedFiles(): Promise { const fileValues = this.getDumpFilesForDeletion(); // Need to have our Jupyter Session and some dumpCell files to delete if (this.jupyterSession && fileValues.length) { @@ -365,7 +388,7 @@ for file in _VSCODE_fileList: pass del _VSCODE_fileList`; - return executeSilently(this.jupyterSession, deleteFilesCode, { + await executeSilently(this.jupyterSession, deleteFilesCode, { traceErrors: true, traceErrorsMessage: 'Error deleting temporary debugging files' }); diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index 160c1352144..1b90d401d36 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -3,36 +3,36 @@ 'use strict'; +import { ITracebackFormatter } from '../kernels/types'; +import { IJupyterVariables } from '../kernels/variables/types'; import { IExtensionSingleActivationService, IExtensionSyncActivationService } from '../platform/activation/types'; +import { Identifiers } from '../platform/common/constants'; +import { IConfigurationService, IDataScienceCommandListener } from '../platform/common/types'; import { IServiceManager } from '../platform/ioc/types'; +import { InstallPythonControllerCommands } from './controllers/commands/installPythonControllerCommands'; +import { ServerConnectionControllerCommands } from './controllers/commands/serverConnectionControllerCommands'; import { KernelFilterService } from './controllers/kernelFilter/kernelFilterService'; import { KernelFilterUI } from './controllers/kernelFilter/kernelFilterUI'; import { LiveKernelSwitcher } from './controllers/liveKernelSwitcher'; -import { RemoteSwitcher } from './controllers/remoteSwitcher'; -import { NotebookCommandListener } from './notebookCommandListener'; -import { NotebookEditorProvider } from './notebookEditorProvider'; -import { ErrorRendererCommunicationHandler } from './outputs/errorRendererComms'; -import { INotebookCompletionProvider, INotebookEditorProvider } from './types'; -import { IConfigurationService, IDataScienceCommandListener } from '../platform/common/types'; +import { NotebookIPyWidgetCoordinator } from './controllers/notebookIPyWidgetCoordinator'; +import { RemoteKernelConnectionHandler } from './controllers/remoteKernelConnectionHandler'; import { RemoteKernelControllerWatcher } from './controllers/remoteKernelControllerWatcher'; -import { ITracebackFormatter } from '../kernels/types'; -import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; +import { RemoteSwitcher } from './controllers/remoteSwitcher'; +import { registerTypes as registerControllerTypes } from './controllers/serviceRegistry.node'; +import { CommandRegistry } from './debugger/commandRegistry'; +import { DebuggerVariableRegistration } from './debugger/debuggerVariableRegistration.node'; +import { DebuggerVariables } from './debugger/debuggerVariables'; +import { DebuggingManager } from './debugger/debuggingManager'; import { IDebuggingManager, IDebugLocationTracker, IDebugLocationTrackerFactory, - IJupyterDebugService + IJupyterDebugService, + INotebookDebuggingManager } from './debugger/debuggingTypes'; -import { Identifiers } from '../platform/common/constants'; +import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory'; import { JupyterDebugService } from './debugger/jupyterDebugService.node'; -import { NotebookIPyWidgetCoordinator } from './controllers/notebookIPyWidgetCoordinator'; -import { RemoteKernelConnectionHandler } from './controllers/remoteKernelConnectionHandler'; -import { JupyterServerSelectorCommand } from './serverSelectorCommand'; -import { InterpreterPackageTracker } from './telemetry/interpreterPackageTracker'; -import { InstallPythonControllerCommands } from './controllers/commands/installPythonControllerCommands'; -import { NotebookCellLanguageService } from './languages/cellLanguageService'; -import { EmptyNotebookCellLanguageService } from './languages/emptyNotebookCellLanguageService'; -import { DebuggingManager } from './debugger/debuggingManager'; +import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; import { ExportBase } from './export/exportBase.node'; import { ExportDialog } from './export/exportDialog'; import { ExportFileOpener } from './export/exportFileOpener'; @@ -41,17 +41,19 @@ import { ExportToHTML } from './export/exportToHTML'; import { ExportToPDF } from './export/exportToPDF'; import { ExportToPython } from './export/exportToPython'; import { ExportToPythonPlain } from './export/exportToPythonPlain'; +import { ExportUtilBase } from './export/exportUtil'; import { ExportUtil } from './export/exportUtil.node'; import { FileConverter } from './export/fileConverter.node'; -import { IFileConverter, INbConvertExport, ExportFormat, IExport, IExportDialog, IExportBase } from './export/types'; -import { ExportUtilBase } from './export/exportUtil'; -import { registerTypes as registerControllerTypes } from './controllers/serviceRegistry.node'; -import { ServerConnectionControllerCommands } from './controllers/commands/serverConnectionControllerCommands'; -import { DebuggerVariableRegistration } from './debugger/debuggerVariableRegistration.node'; -import { IJupyterVariables } from '../kernels/variables/types'; -import { DebuggerVariables } from './debugger/debuggerVariables'; -import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; -import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory'; +import { ExportFormat, IExport, IExportBase, IExportDialog, IFileConverter, INbConvertExport } from './export/types'; +import { NotebookCellLanguageService } from './languages/cellLanguageService'; +import { EmptyNotebookCellLanguageService } from './languages/emptyNotebookCellLanguageService'; +import { NotebookCommandListener } from './notebookCommandListener'; +import { NotebookEditorProvider } from './notebookEditorProvider'; +import { ErrorRendererCommunicationHandler } from './outputs/errorRendererComms'; +import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; +import { JupyterServerSelectorCommand } from './serverSelectorCommand'; +import { InterpreterPackageTracker } from './telemetry/interpreterPackageTracker'; +import { INotebookCompletionProvider, INotebookEditorProvider } from './types'; import { PickDocumentKernelSourceCommand } from './controllers/commands/pickDocumentKernelSourceCommand'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { @@ -115,7 +117,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea ); // Debugging - serviceManager.addSingleton(IDebuggingManager, DebuggingManager, undefined, [ + serviceManager.addSingleton(INotebookDebuggingManager, DebuggingManager, undefined, [ IExtensionSingleActivationService ]); serviceManager.addSingleton( @@ -135,6 +137,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory, undefined, [ IDebugLocationTrackerFactory ]); + serviceManager.addSingleton(IExtensionSingleActivationService, CommandRegistry); // File export/import serviceManager.addSingleton(IFileConverter, FileConverter); diff --git a/src/notebooks/serviceRegistry.web.ts b/src/notebooks/serviceRegistry.web.ts index 0cc5f93cf67..6d124366eab 100644 --- a/src/notebooks/serviceRegistry.web.ts +++ b/src/notebooks/serviceRegistry.web.ts @@ -3,50 +3,52 @@ 'use strict'; +import { ITracebackFormatter } from '../kernels/types'; +import { IJupyterVariables } from '../kernels/variables/types'; import { IExtensionSingleActivationService, IExtensionSyncActivationService } from '../platform/activation/types'; +import { Identifiers } from '../platform/common/constants'; +import { IConfigurationService, IDataScienceCommandListener } from '../platform/common/types'; import { IServiceManager } from '../platform/ioc/types'; +import { ServerConnectionControllerCommands } from './controllers/commands/serverConnectionControllerCommands'; import { KernelFilterService } from './controllers/kernelFilter/kernelFilterService'; import { KernelFilterUI } from './controllers/kernelFilter/kernelFilterUI'; import { LiveKernelSwitcher } from './controllers/liveKernelSwitcher'; -import { RemoteSwitcher } from './controllers/remoteSwitcher'; -import { INotebookEditorProvider } from './types'; -import { NotebookEditorProvider } from './notebookEditorProvider'; -import { RemoteKernelControllerWatcher } from './controllers/remoteKernelControllerWatcher'; -import { ITracebackFormatter } from '../kernels/types'; -import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; import { NotebookIPyWidgetCoordinator } from './controllers/notebookIPyWidgetCoordinator'; import { RemoteKernelConnectionHandler } from './controllers/remoteKernelConnectionHandler'; -import { JupyterServerSelectorCommand } from './serverSelectorCommand'; -import { IConfigurationService, IDataScienceCommandListener } from '../platform/common/types'; -import { NotebookCommandListener } from './notebookCommandListener'; -import { InterpreterPackageTracker } from './telemetry/interpreterPackageTracker'; -import { NotebookCellLanguageService } from './languages/cellLanguageService'; -import { EmptyNotebookCellLanguageService } from './languages/emptyNotebookCellLanguageService'; +import { RemoteKernelControllerWatcher } from './controllers/remoteKernelControllerWatcher'; +import { RemoteSwitcher } from './controllers/remoteSwitcher'; +import { registerTypes as registerControllerTypes } from './controllers/serviceRegistry.web'; +import { CommandRegistry } from './debugger/commandRegistry'; +import { DebuggerVariables } from './debugger/debuggerVariables'; +import { DebuggingManager } from './debugger/debuggingManager'; import { IDebuggingManager, IDebugLocationTracker, IDebugLocationTrackerFactory, - IJupyterDebugService + IJupyterDebugService, + INotebookDebuggingManager } from './debugger/debuggingTypes'; -import { DebuggingManager } from './debugger/debuggingManager'; -import { ErrorRendererCommunicationHandler } from './outputs/errorRendererComms'; +import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory'; +import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; +import { ExportBase } from './export/exportBase.web'; import { ExportDialog } from './export/exportDialog'; -import { ExportFormat, IExport, IExportBase, IExportDialog, IFileConverter, INbConvertExport } from './export/types'; -import { FileConverter } from './export/fileConverter'; import { ExportFileOpener } from './export/exportFileOpener'; -import { ExportToPythonPlain } from './export/exportToPythonPlain'; -import { ExportBase } from './export/exportBase.web'; -import { ExportUtilBase } from './export/exportUtil'; import { ExportToHTML } from './export/exportToHTML'; import { ExportToPDF } from './export/exportToPDF'; import { ExportToPython } from './export/exportToPython'; -import { registerTypes as registerControllerTypes } from './controllers/serviceRegistry.web'; -import { ServerConnectionControllerCommands } from './controllers/commands/serverConnectionControllerCommands'; -import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; -import { Identifiers } from '../platform/common/constants'; -import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory'; -import { IJupyterVariables } from '../kernels/variables/types'; -import { DebuggerVariables } from './debugger/debuggerVariables'; +import { ExportToPythonPlain } from './export/exportToPythonPlain'; +import { ExportUtilBase } from './export/exportUtil'; +import { FileConverter } from './export/fileConverter'; +import { ExportFormat, IExport, IExportBase, IExportDialog, IFileConverter, INbConvertExport } from './export/types'; +import { NotebookCellLanguageService } from './languages/cellLanguageService'; +import { EmptyNotebookCellLanguageService } from './languages/emptyNotebookCellLanguageService'; +import { NotebookCommandListener } from './notebookCommandListener'; +import { NotebookEditorProvider } from './notebookEditorProvider'; +import { ErrorRendererCommunicationHandler } from './outputs/errorRendererComms'; +import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; +import { JupyterServerSelectorCommand } from './serverSelectorCommand'; +import { InterpreterPackageTracker } from './telemetry/interpreterPackageTracker'; +import { INotebookEditorProvider } from './types'; import { PickDocumentKernelSourceCommand } from './controllers/commands/pickDocumentKernelSourceCommand'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { @@ -90,7 +92,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea EmptyNotebookCellLanguageService ); - serviceManager.addSingleton(IDebuggingManager, DebuggingManager, undefined, [ + serviceManager.addSingleton(INotebookDebuggingManager, DebuggingManager, undefined, [ IExtensionSingleActivationService ]); serviceManager.addSingleton( @@ -106,6 +108,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea DebuggerVariables, Identifiers.DEBUGGER_VARIABLES ); + serviceManager.addSingleton(IExtensionSingleActivationService, CommandRegistry); serviceManager.addSingleton( IExtensionSyncActivationService, diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index d0e208de2ae..4c1341d4fff 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -276,7 +276,6 @@ export namespace Commands { export const InteractiveCopyCell = 'jupyter.interactive.copyCell'; export const InteractiveExportAsNotebook = 'jupyter.interactive.exportasnotebook'; export const InteractiveExportAs = 'jupyter.interactive.exportas'; - export const DebugNotebook = 'jupyter.debugNotebook'; export const RunByLine = 'jupyter.runByLine'; export const RunAndDebugCell = 'jupyter.runAndDebugCell'; export const RunByLineNext = 'jupyter.runByLineNext'; diff --git a/src/test/datascience/debugger.vscode.test.ts b/src/test/datascience/debugger.vscode.test.ts index da087c503ab..0d838938fc4 100644 --- a/src/test/datascience/debugger.vscode.test.ts +++ b/src/test/datascience/debugger.vscode.test.ts @@ -2,37 +2,37 @@ // Licensed under the MIT License. 'use strict'; -import * as sinon from 'sinon'; +import { assert } from 'chai'; import * as fs from 'fs'; import * as os from 'os'; -import * as path from '../../platform/vscode-path/path'; -import { noop, sleep } from '../core'; +import * as sinon from 'sinon'; +import { debug } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IDebuggingManager, INotebookDebuggingManager } from '../../notebooks/debugger/debuggingTypes'; import { ICommandManager, IVSCodeNotebook } from '../../platform/common/application/types'; +import { Commands } from '../../platform/common/constants'; import { IDisposable } from '../../platform/common/types'; +import { traceInfo } from '../../platform/logging'; +import * as path from '../../platform/vscode-path/path'; +import { IVariableViewProvider } from '../../webviews/extension-side/variablesView/types'; import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../common.node'; +import { noop, sleep } from '../core'; import { initialize, IS_REMOTE_NATIVE_TEST } from '../initialize.node'; import { closeNotebooks, closeNotebooksAndCleanUpAfterTests, createEmptyPythonNotebook, + defaultNotebookTestTimeout, + getCellOutputs, + getDebugSessionAndAdapter, insertCodeCell, prewarmNotebooks, - getCellOutputs, - defaultNotebookTestTimeout, - waitForStoppedEvent, runCell, - getDebugSessionAndAdapter + waitForStoppedEvent } from './notebook/helper.node'; -import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; -import { traceInfo } from '../../platform/logging'; -import { assert } from 'chai'; -import { debug } from 'vscode'; import { ITestWebviewHost } from './testInterfaces'; -import { DebugProtocol } from 'vscode-debugprotocol'; import { waitForVariablesToMatch } from './variableView/variableViewHelpers'; -import { Commands } from '../../platform/common/constants'; -import { IVariableViewProvider } from '../../webviews/extension-side/variablesView/types'; -import { IDebuggingManager } from '../../notebooks/debugger/debuggingTypes'; +import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; suite('VSCode Notebook - Run By Line', function () { let api: IExtensionTestApi; @@ -58,7 +58,7 @@ suite('VSCode Notebook - Run By Line', function () { const coreVariableViewProvider = api.serviceContainer.get(IVariableViewProvider); // eslint-disable-next-line @typescript-eslint/no-explicit-any variableViewProvider = coreVariableViewProvider as any as ITestVariableViewProvider; // Cast to expose the test interfaces - debuggingManager = api.serviceContainer.get(IDebuggingManager); + debuggingManager = api.serviceContainer.get(INotebookDebuggingManager); vscodeNotebook = api.serviceContainer.get(IVSCodeNotebook); traceInfo(`Start Test Suite (completed)`); }); @@ -107,7 +107,7 @@ suite('VSCode Notebook - Run By Line', function () { await waitForStoppedEvent(debugAdapter!); // Go head and run to the end now - await commandManager.executeCommand(Commands.RunByLineStop); + await commandManager.executeCommand(Commands.RunByLineStop, cell); // Wait until we have finished and have output await waitForCondition( @@ -222,6 +222,28 @@ suite('VSCode Notebook - Run By Line', function () { assert.equal(stack2.stackFrames[0].line, 4, 'Stopped at the wrong line'); }); + test('Restart while debugging', async function () { + const cell = await insertCodeCell('def foo():\n print(1)\n\nfoo()', { index: 0 }); + const doc = vscodeNotebook.activeNotebookEditor?.notebook!; + + await commandManager.executeCommand(Commands.RunByLine, cell); + const { debugAdapter, session } = await getDebugSessionAndAdapter(debuggingManager, doc); + await waitForStoppedEvent(debugAdapter!); // First line + await commandManager.executeCommand('workbench.action.debug.restart'); + const { debugAdapter: debugAdapter2, session: session2 } = await getDebugSessionAndAdapter( + debuggingManager, + doc, + session.id + ); + const stoppedEvent = await waitForStoppedEvent(debugAdapter2!); // First line + const stack: DebugProtocol.StackTraceResponse['body'] = await session2!.customRequest('stackTrace', { + threadId: stoppedEvent.body.threadId + }); + assert.isTrue(stack.stackFrames.length > 0, 'has frames'); + assert.equal(stack.stackFrames[0].source?.path, cell.document.uri.toString(), 'Stopped at the wrong path'); + assert.equal(stack.stackFrames[0].line, 1, 'Stopped at the wrong line'); + }); + test.skip('Does not stop in other cell', async function () { // https://github.com/microsoft/vscode-jupyter/issues/8757 const cell0 = await insertCodeCell('def foo():\n print(1)'); @@ -284,7 +306,7 @@ suite('VSCode Notebook - Run By Line', function () { `Print during time loop is not working. Outputs: ${getCellOutputs(cell)}}`, 1000 ); - await commandManager.executeCommand(Commands.RunByLineStop); + await commandManager.executeCommand(Commands.RunByLineStop, cell); await waitForCondition( async () => !debug.activeDebugSession, defaultNotebookTestTimeout, diff --git a/src/test/datascience/notebook/helper.ts b/src/test/datascience/notebook/helper.ts index a362ee98b5b..9b33666edc7 100644 --- a/src/test/datascience/notebook/helper.ts +++ b/src/test/datascience/notebook/helper.ts @@ -1445,7 +1445,8 @@ export async function waitForDebugEvent( return asPromise( debugAdapter.onDidSendMessage, (message) => (message as DebugProtocol.Event).event === eventType, - timeout + timeout, + `waitForDebugEvent: ${eventType}` ) as Promise; } @@ -1456,14 +1457,17 @@ export async function waitForStoppedEvent(debugAdapter: IKernelDebugAdapter): Pr export async function getDebugSessionAndAdapter( debuggingManager: IDebuggingManager, - doc: NotebookDocument + doc: NotebookDocument, + prevSessionId?: string ): Promise<{ session: DebugSession; debugAdapter: IKernelDebugAdapter }> { await waitForCondition( - async () => !!debuggingManager.getDebugSession(doc), + async () => + !!debuggingManager.getDebugSession(doc) && + (!prevSessionId || prevSessionId !== debuggingManager.getDebugSession(doc)?.id), defaultNotebookTestTimeout, 'DebugSession should start' ); - const session = await debuggingManager.getDebugSession(doc)!; + const session = debuggingManager.getDebugSession(doc)!; const debugAdapter = debuggingManager.getDebugAdapter(doc)!; assert.isOk(debugAdapter, 'DebugAdapter not started');