From 71e064379158574cda176e6ac78247bf66de7aa5 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 23 May 2022 13:48:58 +1000 Subject: [PATCH 01/15] Move debugger files from platform folder --- src/kernels/serviceRegistry.node.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kernels/serviceRegistry.node.ts b/src/kernels/serviceRegistry.node.ts index 358377cbc74..6ebc6c3d3b9 100644 --- a/src/kernels/serviceRegistry.node.ts +++ b/src/kernels/serviceRegistry.node.ts @@ -51,6 +51,7 @@ import { registerTypes as registerJupyterTypes } from './jupyter/serviceRegistry import { KernelProvider } from './kernelProvider.node'; import { KernelFinder } from './kernelFinder.node'; import { ServerConnectionType } from './jupyter/launcher/serverConnectionType'; +import { MultiplexingDebugService } from './debugger/multiplexingDebugService.node'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton( From 423cab909c66522da9d9350182e2166e64606f02 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 08:09:30 +1000 Subject: [PATCH 02/15] Oops --- src/kernels/serviceRegistry.node.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kernels/serviceRegistry.node.ts b/src/kernels/serviceRegistry.node.ts index 6ebc6c3d3b9..358377cbc74 100644 --- a/src/kernels/serviceRegistry.node.ts +++ b/src/kernels/serviceRegistry.node.ts @@ -51,7 +51,6 @@ import { registerTypes as registerJupyterTypes } from './jupyter/serviceRegistry import { KernelProvider } from './kernelProvider.node'; import { KernelFinder } from './kernelFinder.node'; import { ServerConnectionType } from './jupyter/launcher/serverConnectionType'; -import { MultiplexingDebugService } from './debugger/multiplexingDebugService.node'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton( From b3dda618478f014b552b48cd34b37c01f37a07c1 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 11:11:30 +1000 Subject: [PATCH 03/15] Support debugging of IW using Jupyter protocol --- package.json | 4 + .../debugger/jupyter/debugCellControllers.ts | 53 +++ .../debugger/jupyter/debuggingManager.ts | 184 +++++++++ .../debugger/jupyter/kernelDebugAdapter.ts | 103 +++++ .../editor-integration/codeGenerator.ts | 72 +++- .../editor-integration/codelensprovider.ts | 13 +- .../editor-integration/types.ts | 7 +- src/interactive-window/interactiveWindow.ts | 7 +- .../interactiveWindowProvider.ts | 4 +- src/kernels/debugger/constants.ts | 2 + ...node.ts => debugLocationTrackerFactory.ts} | 35 +- src/kernels/debugger/debuggingManagerBase.ts | 208 ++++++++++ .../debugger/kernelDebugAdapterBase.ts | 378 ++++++++++++++++++ src/kernels/debugger/types.ts | 23 +- src/kernels/kernel.base.ts | 7 +- .../debuggerVariableRegistration.node.ts | 3 +- src/notebooks/debugger/debuggingManager.ts | 218 ++-------- src/notebooks/debugger/helper.ts | 66 ++- src/notebooks/debugger/kernelDebugAdapter.ts | 359 +---------------- src/notebooks/execution/cellExecution.ts | 4 +- src/platform/serviceRegistry.node.ts | 20 +- src/platform/serviceRegistry.web.ts | 4 + src/telemetry/index.ts | 1 + 23 files changed, 1179 insertions(+), 596 deletions(-) create mode 100644 src/interactive-window/debugger/jupyter/debugCellControllers.ts create mode 100644 src/interactive-window/debugger/jupyter/debuggingManager.ts create mode 100644 src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts rename src/kernels/debugger/{debugLocationTrackerFactory.node.ts => debugLocationTrackerFactory.ts} (52%) create mode 100644 src/kernels/debugger/debuggingManagerBase.ts create mode 100644 src/kernels/debugger/kernelDebugAdapterBase.ts diff --git a/package.json b/package.json index 7ca04352317..30d6017a62e 100644 --- a/package.json +++ b/package.json @@ -2061,6 +2061,10 @@ { "type": "Python Kernel Debug Adapter", "label": "Python Kernel Debug Adapter" + }, + { + "type": "Python Interactive Window Debug Adapter", + "label": "Python Interactive Window" } ], "markdown.previewStyles": [ diff --git a/src/interactive-window/debugger/jupyter/debugCellControllers.ts b/src/interactive-window/debugger/jupyter/debugCellControllers.ts new file mode 100644 index 00000000000..5bc378571e8 --- /dev/null +++ b/src/interactive-window/debugger/jupyter/debugCellControllers.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from '../../../platform/vscode-path/path'; +import { DebugProtocolMessage, NotebookCell, Uri } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IDebuggingDelegate, IKernelDebugAdapter } from '../../../kernels/debugger/types'; +import { DebuggingTelemetry } from '../../../kernels/debugger/constants'; +import { IKernel } from '../../../kernels/types'; +import { cellDebugSetup } from '../../../notebooks/debugger/helper'; +import { createDeferred } from '../../../platform/common/utils/async'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { getInteractiveCellMetadata } from '../../helpers'; + +export class DebugCellController implements IDebuggingDelegate { + private readonly _ready = createDeferred(); + public readonly ready = this._ready.promise; + constructor( + private readonly debugAdapter: IKernelDebugAdapter, + public readonly debugCell: NotebookCell, + private readonly kernel: IKernel + ) { + sendTelemetryEvent(DebuggingTelemetry.successfullyStartedRunAndDebugCell); + } + + public async willSendEvent(_msg: DebugProtocolMessage): Promise { + return false; + } + + public async willSendRequest(request: DebugProtocol.Request): Promise { + const metadata = getInteractiveCellMetadata(this.debugCell); + if (request.command === 'configurationDone' && metadata && metadata.generatedCode) { + await cellDebugSetup(this.kernel, this.debugAdapter); + + const realPath = this.debugAdapter.getSourceMap(metadata.interactive.uristring); + if (realPath) { + const initialBreakpoint: DebugProtocol.SourceBreakpoint = { + line: metadata.generatedCode.firstExecutableLineIndex - metadata.interactive.line + }; + const uri = Uri.parse(metadata.interactive.uristring); + await this.debugAdapter.setBreakpoints({ + source: { + name: path.basename(uri.path), + path: realPath.path + }, + breakpoints: [initialBreakpoint], + sourceModified: false + }); + } + this._ready.resolve(); + } + } +} diff --git a/src/interactive-window/debugger/jupyter/debuggingManager.ts b/src/interactive-window/debugger/jupyter/debuggingManager.ts new file mode 100644 index 00000000000..1763b6c02b2 --- /dev/null +++ b/src/interactive-window/debugger/jupyter/debuggingManager.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { + NotebookDocument, + DebugAdapterInlineImplementation, + DebugSession, + NotebookCell, + DebugSessionOptions, + DebugAdapterDescriptor, + NotebookEditor, + debug +} from 'vscode'; +import { pythonIWKernelDebugAdapter } from '../../../kernels/debugger/constants'; +import { + IDebuggingManager, + IInteractiveWindowDebuggingManager, + KernelDebugMode, + IKernelDebugAdapterConfig, + IDebugLocationTrackerFactory +} from '../../../kernels/debugger/types'; +import { IKernelProvider } from '../../../kernels/types'; +import { IpykernelCheckResult, assertIsDebugConfig } from '../../../notebooks/debugger/helper'; +import { KernelDebugAdapter } from './kernelDebugAdapter'; +import { INotebookControllerManager } from '../../../notebooks/types'; +import { IExtensionSingleActivationService } from '../../../platform/activation/types'; +import { ICommandManager, IApplicationShell, IVSCodeNotebook } from '../../../platform/common/application/types'; +import { IPlatformService } from '../../../platform/common/platform/types'; +import { DataScience } from '../../../platform/common/utils/localize'; +import { traceInfoIfCI, traceInfo, traceError } from '../../../platform/logging'; +import * as path from '../../../platform/vscode-path/path'; +import { DebugCellController } from './debugCellControllers'; +import { DebuggingManagerBase } from '../../../kernels/debugger/debuggingManagerBase'; +import { IConfigurationService } from '../../../platform/common/types'; + +/** + * The DebuggingManager maintains the mapping between notebook documents and debug sessions. + */ +@injectable() +export class InteractiveWindowDebuggingManager + extends DebuggingManagerBase + implements IExtensionSingleActivationService, IDebuggingManager, IInteractiveWindowDebuggingManager +{ + public constructor( + @inject(IKernelProvider) kernelProvider: IKernelProvider, + @inject(INotebookControllerManager) notebookControllerManager: INotebookControllerManager, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IVSCodeNotebook) vscNotebook: IVSCodeNotebook, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IDebugLocationTrackerFactory) + private readonly debugLocationTrackerFactory: IDebugLocationTrackerFactory, + @inject(IConfigurationService) private readonly configService: IConfigurationService + ) { + super(kernelProvider, notebookControllerManager, commandManager, appShell, vscNotebook); + } + + public override async activate(): Promise { + await super.activate(); + // factory for kernel debug adapters + this.disposables.push( + debug.registerDebugAdapterDescriptorFactory(pythonIWKernelDebugAdapter, { + createDebugAdapterDescriptor: async (session) => this.createDebugAdapterDescriptor(session) + }) + ); + } + 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.document); + switch (ipykernelResult) { + case IpykernelCheckResult.NotInstalled: + // User would have been notified about this, nothing more to do. + return; + case IpykernelCheckResult.Outdated: + case IpykernelCheckResult.Unknown: { + void this.promptInstallIpykernel6(); + 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); + } + } + + private async startDebuggingCell(doc: NotebookDocument, cell: NotebookCell) { + const settings = this.configService.getSettings(doc.uri); + const config: IKernelDebugAdapterConfig = { + type: pythonIWKernelDebugAdapter, + name: path.basename(doc.uri.toString()), + request: 'attach', + justMyCode: settings.debugJustMyCode, + __interactiveWindowNotebookUri: 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); + } + + 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. + 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) { + return; + } + if (!kernel?.session) { + void this.appShell.showInformationMessage(DataScience.kernelWasNotStarted()); + return; + } + const adapter = new KernelDebugAdapter( + session, + debug.document, + kernel.session, + kernel, + this.platform, + this.debugLocationTrackerFactory + ); + + this.notebookToDebugAdapter.set(debug.document, adapter); + 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 controller = new DebugCellController(adapter, cell, kernel!); + adapter.setDebuggingDelegate(controller); + controller.ready + .then(() => debug.resolve(session)) + .catch((ex) => console.error('Failed waiting for controller to be ready', ex)); + + this.trackDebugAdapter(activeDoc, adapter); + return new DebugAdapterInlineImplementation(adapter); + } +} diff --git a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts new file mode 100644 index 00000000000..8fa0ebab6b3 --- /dev/null +++ b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { KernelMessage } from '@jupyterlab/services'; +import * as path from '../../../platform/vscode-path/path'; +import { DebugAdapterTracker, DebugSession, NotebookCellKind, NotebookDocument, Uri } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IJupyterSession, IKernel } from '../../../kernels/types'; +import { IPlatformService } from '../../../platform/common/platform/types'; +import { IDumpCellResponse, IDebugLocationTrackerFactory } from '../../../kernels/debugger/types'; +import { traceError, traceInfoIfCI } from '../../../platform/logging'; +import { getInteractiveCellMetadata } from '../../../interactive-window/helpers'; +import { KernelDebugAdapterBase } from '../../../kernels/debugger/kernelDebugAdapterBase'; + +export class KernelDebugAdapter extends KernelDebugAdapterBase { + private readonly debugLocationTracker?: DebugAdapterTracker; + constructor( + session: DebugSession, + notebookDocument: NotebookDocument, + jupyterSession: IJupyterSession, + kernel: IKernel | undefined, + platformService: IPlatformService, + debugLocationTrackerFactory?: IDebugLocationTrackerFactory + ) { + super(session, notebookDocument, jupyterSession, kernel, platformService); + if (debugLocationTrackerFactory) { + this.debugLocationTracker = debugLocationTrackerFactory.createDebugAdapterTracker( + session + ) as DebugAdapterTracker; + if (this.debugLocationTracker.onWillStartSession) { + this.debugLocationTracker.onWillStartSession(); + } + this.onDidSendMessage( + (msg) => { + if (this.debugLocationTracker?.onDidSendMessage) { + this.debugLocationTracker.onDidSendMessage(msg); + } + }, + this, + this.disposables + ); + this.onDidEndSession( + () => { + if (this.debugLocationTracker?.onWillStopSession) { + this.debugLocationTracker.onWillStopSession(); + } + }, + this, + this.disposables + ); + } + } + + override handleMessage(message: DebugProtocol.ProtocolMessage): Promise { + traceInfoIfCI(`KernelDebugAdapter::handleMessage ${JSON.stringify(message, undefined, ' ')}`); + if (message.type === 'request' && this.debugLocationTracker?.onWillReceiveMessage) { + this.debugLocationTracker.onWillReceiveMessage(message); + } + if (message.type === 'response' && this.debugLocationTracker?.onDidSendMessage) { + this.debugLocationTracker.onDidSendMessage(message); + } + return super.handleMessage(message); + } + + public override async dumpAllCells() { + await Promise.all( + this.notebookDocument.getCells().map(async (cell) => { + if (cell.kind === NotebookCellKind.Code) { + await this.dumpCell(cell.index); + } + }) + ); + } + public override getSourceMap(filePath: string) { + return this.cellToFile.get(filePath); + } + // Dump content of given cell into a tmp file and return path to file. + protected async dumpCell(index: number): Promise { + const cell = this.notebookDocument.cellAt(index); + const metadata = getInteractiveCellMetadata(cell); + if (!metadata) { + throw new Error('Not an interactive window cell'); + } + try { + const response = await this.session.customRequest('dumpCell', { + code: (metadata.generatedCode?.code || cell.document.getText()).replace(/\r\n/g, '\n') + }); + const norm = path.normalize((response as IDumpCellResponse).sourcePath); + this.fileToCell.set(norm, { + uri: Uri.parse(metadata.interactive.uristring), + lineOffset: metadata.interactive.line + 1 // Add an extra 1 for the cell marker. + }); + this.cellToFile.set(metadata.interactive.uristring, { + path: norm, + lineOffset: metadata.interactive.line + 1 // Add an extra 1 for the cell marker. + }); + } catch (err) { + traceError(`Failed to dump cell for ${cell.index} with code ${metadata.interactive.originalSource}`, err); + } + } +} diff --git a/src/interactive-window/editor-integration/codeGenerator.ts b/src/interactive-window/editor-integration/codeGenerator.ts index 0bb06c67141..aa5c9524c7e 100644 --- a/src/interactive-window/editor-integration/codeGenerator.ts +++ b/src/interactive-window/editor-integration/codeGenerator.ts @@ -12,13 +12,14 @@ import { Uri } from 'vscode'; -import { splitMultilineString } from '../../webviews/webview-side/common'; +import { parseForComments, splitMultilineString } from '../../webviews/webview-side/common'; import { IDocumentManager } from '../../platform/common/application/types'; import { traceInfo } from '../../platform/logging'; import { IConfigurationService, IDisposableRegistry } from '../../platform/common/types'; import { uncommentMagicCommands } from './cellFactory'; import { CellMatcher } from './cellMatcher'; import { IGeneratedCode, IInteractiveWindowCodeGenerator, IGeneratedCodeStore, InteractiveCellMetadata } from './types'; +import { noop } from '../../platform/common/utils/misc'; // This class provides generated code for debugging jupyter cells. Call getGeneratedCode just before starting debugging to compute all of the // generated codes for cells & update the source maps in the python debugger. @@ -28,8 +29,8 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { private disposed?: boolean; private disposables: Disposable[] = []; constructor( - private documentManager: IDocumentManager, - private configService: IConfigurationService, + private readonly documentManager: IDocumentManager, + private readonly configService: IConfigurationService, private readonly storage: IGeneratedCodeStore, private readonly notebook: NotebookDocument, disposables: IDisposableRegistry @@ -53,13 +54,17 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { this.executionCount = 0; } - public generateCode(metadata: Pick, debug: boolean) { + public generateCode( + metadata: Pick, + debug: boolean, + usingJupyterDebugProtocol?: boolean + ) { // Don't log empty cells const executableLines = this.extractExecutableLines(metadata.interactive.originalSource); if (executableLines.length > 0 && executableLines.find((s) => s.trim().length > 0)) { // When the user adds new code, we know the execution count is increasing this.executionCount += 1; - return this.generateHash(metadata, this.executionCount, debug); + return this.generateCodeImpl(metadata, this.executionCount, debug, usingJupyterDebugProtocol); } } @@ -79,10 +84,11 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { return lines; } - private generateHash( + private generateCodeImpl( metadata: Pick, expectedCount: number, - debug: boolean + debug: boolean, + usingJupyterDebugProtocol?: boolean ) { // Find the text document that matches. We need more information than // the add code gives us @@ -92,7 +98,7 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { return; } // Compute the code that will really be sent to jupyter - const { stripped, trueStartLine } = this.extractStrippedLines(metadata); + const { stripped, trueStartLine, firstExecutableLineIndex } = this.extractStrippedLines(metadata); const line = doc.lineAt(trueStartLine); const endLine = doc.lineAt(Math.min(trueStartLine + stripped.length - 1, doc.lineCount - 1)); @@ -114,14 +120,15 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { debug, stripped, trueStartLine, - firstNonBlankLineIndex + firstNonBlankLineIndex, + usingJupyterDebugProtocol ); const hashedCode = stripped.join(''); const realCode = doc.getText(new Range(new Position(cellLine, 0), endLine.rangeIncludingLineBreak.end)); - const hashValue = hashjs.sha1().update(hashedCode).digest('hex').substr(0, 12); + const hashValue = hashjs.sha1().update(hashedCode).digest('hex').substring(0, 12); const runtimeFile = this.getRuntimeFile(hashValue, expectedCount); - + console.log(firstExecutableLineIndex); const hash: IGeneratedCode = { line: line ? line.lineNumber + 1 : 1, endLine: endLine ? endLine.lineNumber + 1 : 1, @@ -137,7 +144,8 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { runtimeLine, runtimeFile, id: metadata.id, - timestamp: Date.now() + timestamp: Date.now(), + firstExecutableLineIndex }; traceInfo(`Generated code for ${expectedCount} = ${runtimeFile} with ${stripped.length} lines`); @@ -164,6 +172,7 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { private extractStrippedLines(metadata: Pick): { stripped: string[]; trueStartLine: number; + firstExecutableLineIndex: number; } { const lines = splitMultilineString(metadata.interactive.originalSource); // Compute the code that will really be sent to jupyter @@ -210,8 +219,24 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { for (let i = 0; i < stripped.length; i++) { stripped[i] = stripped[i].replace(/\r\n/g, '\n'); } - - return { stripped, trueStartLine }; + // This will save the code lines of the cell in lineList (so ignore comments and emtpy lines) + // Its done to set the Run by Line breakpoint on the first code line + const textLines = metadata.interactive.originalSource.splitLines({ trim: false, removeEmptyEntries: false }); + const lineList: number[] = []; + parseForComments( + textLines, + () => noop(), + (s, i) => { + if (s.trim().length !== 0) { + lineList.push(i); + } + } + ); + lineList.sort(); + const firstExecutableLineIndex = lineList.length + ? metadata.interactive.line + lineList[0] + : metadata.interactive.line; + return { stripped, trueStartLine, firstExecutableLineIndex }; } private handleContentChange(docText: string, c: TextDocumentContentChangeEvent, generatedCodes: IGeneratedCode[]) { @@ -273,15 +298,22 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { debug: boolean, source: string[], trueStartLine: number, - firstNonBlankLineIndex: number + firstNonBlankLineIndex: number, + usingJupyterDebugProtocol?: boolean ): { runtimeLine: number; debuggerStartLine: number } { + const useNewDebugger = + usingJupyterDebugProtocol || this.configService.getSettings(undefined).useJupyterDebugger === true; if (debug && this.configService.getSettings(this.notebook.uri).stopOnFirstLineWhileDebugging) { - // Inject the breakpoint line - source.splice(0, 0, 'breakpoint()\n'); + if (useNewDebugger) { + return { runtimeLine: 1, debuggerStartLine: firstNonBlankLineIndex + 1 }; + } else { + // Inject the breakpoint line + source.splice(0, 0, 'breakpoint()\n'); - // Start on the second line - // Since a breakpoint was added map to the first line (even if blank) - return { runtimeLine: 2, debuggerStartLine: trueStartLine + 1 }; + // Start on the second line + // Since a breakpoint was added map to the first line (even if blank) + return { runtimeLine: 2, debuggerStartLine: trueStartLine + 1 }; + } } // No breakpoint necessary, start on the first line // Since no breakpoint was added map to the first non-blank line diff --git a/src/interactive-window/editor-integration/codelensprovider.ts b/src/interactive-window/editor-integration/codelensprovider.ts index 71a9beb0094..ddb42fd4bd2 100644 --- a/src/interactive-window/editor-integration/codelensprovider.ts +++ b/src/interactive-window/editor-integration/codelensprovider.ts @@ -134,7 +134,18 @@ export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider const debugLocation = this.debugLocationTracker.getLocation(this.debugService.activeDebugSession); // Debug locations only work on local paths, so check against fsPath here. - if (debugLocation && urlPath.isEqual(vscode.Uri.file(debugLocation.fileName), document.uri, true)) { + let uri: vscode.Uri | undefined; + try { + // When dealing with Jupyter debugger protocol, the paths are stringified Uris. + uri = debugLocation ? vscode.Uri.parse(debugLocation.fileName) : undefined; + } catch { + // + } + if ( + debugLocation && + (urlPath.isEqual(vscode.Uri.file(debugLocation.fileName), document.uri, true) || + (uri && urlPath.isEqual(uri, document.uri, true))) + ) { // We are in the given debug file, so only return the code lens that contains the given line const activeLenses = lenses.filter((lens) => { // -1 for difference between file system one based and debugger zero based diff --git a/src/interactive-window/editor-integration/types.ts b/src/interactive-window/editor-integration/types.ts index 0ab740facbe..93a10ecd937 100644 --- a/src/interactive-window/editor-integration/types.ts +++ b/src/interactive-window/editor-integration/types.ts @@ -73,6 +73,10 @@ export interface IGeneratedCode { realCode: string; trimmedRightCode: string; firstNonBlankLineIndex: number; // zero based. First non blank line of the real code. + /** + * First line (index) in the cell of the source file with executable code. + */ + firstExecutableLineIndex: number; } export interface IFileGeneratedCodes { @@ -108,7 +112,8 @@ export interface IInteractiveWindowCodeGenerator extends IDisposable { reset(): void; generateCode( metadata: Pick, - debug: boolean + debug: boolean, + usingJupyterDebugProtocol?: boolean ): IGeneratedCode | undefined; } diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index 33bfff22120..8a49b376eb5 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -72,6 +72,7 @@ import { IGeneratedCodeStorageFactory, InteractiveCellMetadata } from './editor-integration/types'; +import { IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; export class InteractiveWindow implements IInteractiveWindowLoadable { public get onDidChangeViewState(): Event { @@ -131,7 +132,8 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { public readonly inputUri: Uri, public readonly appShell: IApplicationShell, private readonly codeGeneratorFactory: ICodeGeneratorFactory, - private readonly storageFactory: IGeneratedCodeStorageFactory + private readonly storageFactory: IGeneratedCodeStorageFactory, + private readonly debuggingManager: IInteractiveWindowDebuggingManager ) { // Set our owner and first submitter if (this._owner) { @@ -576,6 +578,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { const kernel = await kernelPromise; if (isDebug && (settings.useJupyterDebugger || !isLocalConnection(kernel.kernelConnectionMetadata))) { // New ipykernel 7 debugger. + await this.debuggingManager.start(this.notebookEditor, cell); } else if (isDebug && isLocalConnection(kernel.kernelConnectionMetadata)) { // Old ipykernel 6 debugger. // If debugging attach to the kernel but don't enable tracing just yet @@ -754,7 +757,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { const id = uuid(); const generatedCode = this.codeGeneratorFactory .getOrCreate(this.notebookDocument) - .generateCode({ interactive, id }, isDebug); + .generateCode({ interactive, id }, isDebug, true); const metadata: InteractiveCellMetadata = { interactiveWindowCellMarker, diff --git a/src/interactive-window/interactiveWindowProvider.ts b/src/interactive-window/interactiveWindowProvider.ts index 1411fe589aa..e054c1b61aa 100644 --- a/src/interactive-window/interactiveWindowProvider.ts +++ b/src/interactive-window/interactiveWindowProvider.ts @@ -47,6 +47,7 @@ import { IExportDialog } from '../platform/export/types'; import { IVSCodeNotebookController } from '../notebooks/controllers/types'; import { InteractiveWindowView } from '../notebooks/constants'; import { ICodeGeneratorFactory, IGeneratedCodeStorageFactory } from './editor-integration/types'; +import { IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; // Export for testing export const AskedForPerFileSettingKey = 'ds_asked_per_file_interactive'; @@ -172,7 +173,8 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA inputUri, this.appShell, this.serviceContainer.get(ICodeGeneratorFactory), - this.serviceContainer.get(IGeneratedCodeStorageFactory) + this.serviceContainer.get(IGeneratedCodeStorageFactory), + this.serviceContainer.get(IInteractiveWindowDebuggingManager) ); this._windows.push(result); diff --git a/src/kernels/debugger/constants.ts b/src/kernels/debugger/constants.ts index 8ccd00bc922..21d92579f4e 100644 --- a/src/kernels/debugger/constants.ts +++ b/src/kernels/debugger/constants.ts @@ -4,6 +4,7 @@ 'use strict'; export const pythonKernelDebugAdapter = 'Python Kernel Debug Adapter'; +export const pythonIWKernelDebugAdapter = 'Python Interactive Window Debug Adapter'; export enum DebuggingTelemetry { clickedOnSetup = 'DATASCIENCE.DEBUGGING.CLICKED_ON_SETUP', @@ -11,6 +12,7 @@ export enum DebuggingTelemetry { ipykernel6Status = 'DATASCIENCE.DEBUGGING.IPYKERNEL6_STATUS', clickedRunByLine = 'DATASCIENCE.DEBUGGING.CLICKED_RUNBYLINE', successfullyStartedRunByLine = 'DATASCIENCE.DEBUGGING.SUCCESSFULLY_STARTED_RUNBYLINE', + successfullyStartedIWJupyterDebugger = 'DATASCIENCE.DEBUGGING.SUCCESSFULLY_STARTED_IW_JUPYTER', clickedRunAndDebugCell = 'DATASCIENCE.DEBUGGING.CLICKED_RUN_AND_DEBUG_CELL', successfullyStartedRunAndDebugCell = 'DATASCIENCE.DEBUGGING.SUCCESSFULLY_STARTED_RUN_AND_DEBUG_CELL', endedSession = 'DATASCIENCE.DEBUGGING.ENDED_SESSION' diff --git a/src/kernels/debugger/debugLocationTrackerFactory.node.ts b/src/kernels/debugger/debugLocationTrackerFactory.ts similarity index 52% rename from src/kernels/debugger/debugLocationTrackerFactory.node.ts rename to src/kernels/debugger/debugLocationTrackerFactory.ts index 9915516cff7..2bb31cbaef1 100644 --- a/src/kernels/debugger/debugLocationTrackerFactory.node.ts +++ b/src/kernels/debugger/debugLocationTrackerFactory.ts @@ -2,38 +2,33 @@ // Licensed under the MIT License. 'use strict'; import { inject, injectable } from 'inversify'; -import { - DebugAdapterTracker, - DebugAdapterTrackerFactory, - DebugSession, - Event, - EventEmitter, - ProviderResult -} from 'vscode'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, Event, EventEmitter } from 'vscode'; import { IDebugService } from '../../platform/common/application/types'; import { IDisposableRegistry } from '../../platform/common/types'; import { DebugLocationTracker } from './debugLocationTracker'; -import { IDebugLocationTracker } from './types'; +import { IDebugLocationTracker, IDebugLocationTrackerFactory } from './types'; // Hook up our IDebugLocationTracker to python debugging sessions @injectable() -export class DebugLocationTrackerFactory implements IDebugLocationTracker, DebugAdapterTrackerFactory { - private activeTrackers: Map = new Map(); +export class DebugLocationTrackerFactory + implements IDebugLocationTracker, IDebugLocationTrackerFactory, DebugAdapterTrackerFactory +{ + private activeTrackers = new WeakMap(); private updatedEmitter: EventEmitter = new EventEmitter(); constructor( @inject(IDebugService) debugService: IDebugService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry ) { disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); } - public createDebugAdapterTracker(session: DebugSession): ProviderResult { + public createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker { const result = new DebugLocationTracker(session.id); - this.activeTrackers.set(session.id, result); - result.sessionEnded(this.onSessionEnd.bind(this)); - result.debugLocationUpdated(this.onLocationUpdated.bind(this)); + this.activeTrackers.set(session, result); + result.sessionEnded(() => this.activeTrackers.delete(session), this, this.disposableRegistry); + result.debugLocationUpdated(this.onLocationUpdated, this, this.disposableRegistry); this.onLocationUpdated(); return result; } @@ -43,18 +38,12 @@ export class DebugLocationTrackerFactory implements IDebugLocationTracker, Debug } public getLocation(session: DebugSession) { - const tracker = this.activeTrackers.get(session.id); + const tracker = this.activeTrackers.get(session); if (tracker) { return tracker.debugLocation; } } - private onSessionEnd(locationTracker: DebugLocationTracker) { - if (locationTracker.sessionId) { - this.activeTrackers.delete(locationTracker.sessionId); - } - } - private onLocationUpdated() { this.updatedEmitter.fire(); } diff --git a/src/kernels/debugger/debuggingManagerBase.ts b/src/kernels/debugger/debuggingManagerBase.ts new file mode 100644 index 00000000000..fab9509d23d --- /dev/null +++ b/src/kernels/debugger/debuggingManagerBase.ts @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { + debug, + NotebookDocument, + workspace, + DebugSession, + DebugSessionOptions, + DebugAdapterDescriptor, + Event, + EventEmitter, + NotebookCell +} from 'vscode'; +import { IKernel, IKernelProvider } from '../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 { DataScience } from '../../platform/common/utils/localize'; +import { IKernelDebugAdapterConfig } from './types'; +import { Debugger } from '../../notebooks/debugger/debugger'; +import { KernelDebugAdapterBase } from './kernelDebugAdapterBase'; +import { INotebookControllerManager } from '../../notebooks/types'; +import { IpykernelCheckResult, isUsingIpykernel6OrLater } from '../../notebooks/debugger/helper'; + +/** + * The DebuggingManager maintains the mapping between notebook documents and debug sessions. + */ +@injectable() +export abstract class DebuggingManagerBase implements IDisposable { + private notebookToDebugger = new Map(); + protected notebookToDebugAdapter = new Map(); + protected notebookInProgress = new Set(); + protected readonly disposables: IDisposable[] = []; + private _doneDebugging = new EventEmitter(); + + public constructor( + private kernelProvider: IKernelProvider, + private readonly notebookControllerManager: INotebookControllerManager, + protected readonly commandManager: ICommandManager, + protected readonly appShell: IApplicationShell, + protected readonly vscNotebook: IVSCodeNotebook + ) {} + + public async activate() { + this.disposables.push( + // track termination of debug sessions + debug.onDidTerminateDebugSession(this.endSession.bind(this)), + + // track closing of notebooks documents + workspace.onDidCloseNotebookDocument(async (document) => { + const dbg = this.notebookToDebugger.get(document); + if (dbg) { + await dbg.stop(); + this.onDidStopDebugging(document); + } + }) + ); + } + public getDebugCell(notebook: NotebookDocument): NotebookCell | undefined { + return this.notebookToDebugAdapter.get(notebook)?.debugCell; + } + + public get onDoneDebugging(): Event { + return this._doneDebugging.event; + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + public isDebugging(notebook: NotebookDocument): boolean { + return this.notebookToDebugger.has(notebook); + } + + public getDebugSession(notebook: NotebookDocument): Promise | undefined { + const dbg = this.notebookToDebugger.get(notebook); + if (dbg) { + return dbg.session; + } + } + public getDebugAdapter(notebook: NotebookDocument): KernelDebugAdapterBase | undefined { + return this.notebookToDebugAdapter.get(notebook); + } + + protected onDidStopDebugging(_notebook: NotebookDocument): void { + // + } + + protected async startDebuggingConfig( + doc: NotebookDocument, + config: IKernelDebugAdapterConfig, + 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})`); + void this.appShell.showErrorMessage(DataScience.cantStartDebugging()); + } + } else { + traceInfoIfCI(`Not starting debugging because already debugging in this notebook`); + } + } + + protected trackDebugAdapter(notebook: NotebookDocument, adapter: KernelDebugAdapterBase) { + 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}`); + this._doneDebugging.fire(); + for (const [doc, dbg] of this.notebookToDebugger.entries()) { + if (dbg && session.id === (await dbg.session).id) { + this.notebookToDebugger.delete(doc); + this.notebookToDebugAdapter.delete(doc); + this.onDidStopDebugging(doc); + break; + } + } + } + + protected abstract createDebugAdapterDescriptor(session: DebugSession): Promise; + + protected getDebuggerByUri(document: NotebookDocument): Debugger | undefined { + for (const [doc, dbg] of this.notebookToDebugger.entries()) { + if (document === doc) { + return dbg; + } + } + } + + protected async ensureKernelIsRunning(doc: NotebookDocument): Promise { + await this.notebookControllerManager.loadNotebookControllers(); + const controller = this.notebookControllerManager.getSelectedNotebookController(doc); + + let kernel = this.kernelProvider.get(doc.uri); + if (!kernel && controller) { + kernel = this.kernelProvider.getOrCreate(doc.uri, { + metadata: controller.connection, + controller: controller?.controller, + resourceUri: doc.uri, + creator: 'jupyterExtension' + }); + } + if (kernel && kernel.status === 'unknown') { + await kernel.start(); + } + + return kernel; + } + + protected async checkForIpykernel6(doc: NotebookDocument): Promise { + try { + let kernel = this.kernelProvider.get(doc.uri); + if (!kernel) { + const controller = this.notebookControllerManager.getSelectedNotebookController(doc); + if (!controller) { + return IpykernelCheckResult.ControllerNotSelected; + } + kernel = this.kernelProvider.getOrCreate(doc.uri, { + metadata: controller.connection, + controller: controller?.controller, + resourceUri: doc.uri, + creator: 'jupyterExtension' + }); + } + + const result = await isUsingIpykernel6OrLater(kernel); + sendTelemetryEvent(DebuggingTelemetry.ipykernel6Status, undefined, { + status: result === IpykernelCheckResult.Ok ? 'installed' : 'notInstalled' + }); + return result; + } catch (ex) { + traceError('Debugging: Could not check for ipykernel 6', ex); + } + return IpykernelCheckResult.Unknown; + } + + protected async promptInstallIpykernel6() { + const response = await this.appShell.showInformationMessage( + DataScience.needIpykernel6(), + { modal: true }, + DataScience.setup() + ); + + if (response === DataScience.setup()) { + sendTelemetryEvent(DebuggingTelemetry.clickedOnSetup); + this.appShell.openUrl( + 'https://github.com/microsoft/vscode-jupyter/wiki/Setting-Up-Run-by-Line-and-Debugging-for-Notebooks' + ); + } else { + sendTelemetryEvent(DebuggingTelemetry.closedModal); + } + } +} diff --git a/src/kernels/debugger/kernelDebugAdapterBase.ts b/src/kernels/debugger/kernelDebugAdapterBase.ts new file mode 100644 index 00000000000..e5b8c70e6cb --- /dev/null +++ b/src/kernels/debugger/kernelDebugAdapterBase.ts @@ -0,0 +1,378 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { KernelMessage } from '@jupyterlab/services'; +import * as path from '../../platform/vscode-path/path'; +import { + debug, + DebugAdapter, + DebugProtocolMessage, + DebugSession, + Event, + EventEmitter, + NotebookCell, + NotebookCellExecutionState, + NotebookCellExecutionStateChangeEvent, + NotebookDocument, + notebooks, + Uri, + workspace +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { executeSilently } from '../helpers'; +import { IJupyterSession, IKernel } from '../types'; +import { IPlatformService } from '../../platform/common/platform/types'; +import { DebuggingTelemetry } from './constants'; +import { + IKernelDebugAdapter, + IKernelDebugAdapterConfig, + IDebuggingDelegate, + KernelDebugMode, + IDebugInfoResponse +} from './types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { IDisposable } from '../../platform/common/types'; +import { traceError, traceInfo, traceInfoIfCI, traceVerbose } from '../../platform/logging'; +import { + assertIsDebugConfig, + isShortNamePath, + shortNameMatchesLongName, + getMessageSourceAndHookIt +} from '../../notebooks/debugger/helper'; + +/** + * For info on the custom requests implemented by jupyter see: + * https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request + * https://jupyter-client.readthedocs.io/en/stable/messaging.html#additions-to-the-dap + */ +export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDebugAdapter, IDisposable { + protected readonly fileToCell = new Map< + string, + { + uri: Uri; + lineOffset?: number; + } + >(); + protected readonly cellToFile = new Map< + string, + { + path: string; + lineOffset?: number; + } + >(); + private readonly sendMessage = new EventEmitter(); + private readonly endSession = new EventEmitter(); + private readonly configuration: IKernelDebugAdapterConfig; + protected readonly disposables: IDisposable[] = []; + private delegate: IDebuggingDelegate | undefined; + onDidSendMessage: Event = this.sendMessage.event; + onDidEndSession: Event = this.endSession.event; + public readonly debugCell: NotebookCell | undefined; + private disconnected: boolean = false; + private kernelEventHook = (_event: 'willRestart' | 'willInterrupt') => this.disconnect(); + constructor( + protected session: DebugSession, + protected notebookDocument: NotebookDocument, + private readonly jupyterSession: IJupyterSession, + private readonly kernel: IKernel | undefined, + private readonly platformService: IPlatformService + ) { + traceInfoIfCI(`Creating kernel debug adapter for debugging notebooks`); + const configuration = this.session.configuration; + assertIsDebugConfig(configuration); + this.configuration = configuration; + + if ( + configuration.__mode === KernelDebugMode.InteractiveWindow || + configuration.__mode === KernelDebugMode.Cell || + configuration.__mode === KernelDebugMode.RunByLine + ) { + this.debugCell = notebookDocument.cellAt(configuration.__cellIndex!); + } + + this.disposables.push( + this.jupyterSession.onIOPubMessage(async (msg: KernelMessage.IIOPubMessage) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const anyMsg = msg as any; + traceInfoIfCI(`Debug IO Pub message: ${JSON.stringify(msg)}`); + if (anyMsg.header.msg_type === 'debug_event') { + this.trace('event', JSON.stringify(msg)); + if (!(await this.delegate?.willSendEvent(anyMsg))) { + this.sendMessage.fire(msg.content); + } + } + }) + ); + + if (this.kernel) { + this.kernel.addEventHook(this.kernelEventHook); + this.disposables.push( + this.kernel.onDisposed(() => { + void debug.stopDebugging(this.session); + this.endSession.fire(this.session); + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'onKernelDisposed' }); + }) + ); + } + + this.disposables.push( + notebooks.onDidChangeNotebookCellExecutionState( + (cellStateChange: NotebookCellExecutionStateChangeEvent) => { + // If a cell has moved to idle, stop the debug session + if ( + this.configuration.__cellIndex === cellStateChange.cell.index && + cellStateChange.state === NotebookCellExecutionState.Idle && + !this.disconnected + ) { + sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'normally' }); + void this.disconnect(); + } + }, + this, + this.disposables + ) + ); + + this.disposables.push( + workspace.onDidChangeNotebookDocument( + (e) => { + e.contentChanges.forEach((change) => { + change.removedCells.forEach((cell: NotebookCell) => { + if (cell === this.debugCell) { + void this.disconnect(); + } + }); + }); + }, + this, + this.disposables + ) + ); + } + + public setDebuggingDelegate(delegate: IDebuggingDelegate) { + this.delegate = delegate; + } + + private trace(tag: string, msg: string) { + traceVerbose(`[Debug] ${tag}: ${msg}`); + } + + async handleMessage(message: DebugProtocol.ProtocolMessage) { + try { + traceInfoIfCI(`KernelDebugAdapter::handleMessage ${JSON.stringify(message, undefined, ' ')}`); + // intercept 'setBreakpoints' request + if (message.type === 'request' && (message as DebugProtocol.Request).command === 'setBreakpoints') { + const args = (message as DebugProtocol.Request).arguments; + if (args.source && args.source.path && args.source.path.indexOf('vscode-notebook-cell:') === 0) { + const cell = this.notebookDocument + .getCells() + .find((c) => c.document.uri.toString() === args.source.path); + if (cell) { + await this.dumpCell(cell.index); + } + } + } + + // after attaching, send a 'debugInfo' request + // reset breakpoints and continue stopped threads if there are any + // we do this in case the kernel is stopped when we attach + // This might happen if VS Code or the extension host crashes + if (message.type === 'request' && (message as DebugProtocol.Request).command === 'attach') { + await this.debugInfo(); + } + + if (message.type === 'request') { + await this.delegate?.willSendRequest(message as DebugProtocol.Request); + } + + return this.sendRequestToJupyterSession(message); + } catch (e) { + traceError(`KernelDebugAdapter::handleMessage failure: ${e}`); + } + } + + public getConfiguration(): IKernelDebugAdapterConfig { + return this.configuration; + } + + public stepIn(threadId: number): Thenable { + return this.session.customRequest('stepIn', { threadId }); + } + + public async disconnect() { + await this.session.customRequest('disconnect', { restart: false }); + this.endSession.fire(this.session); + this.disconnected = true; + this.kernel?.removeEventHook(this.kernelEventHook); + } + + dispose() { + // On dispose, delete our temp cell files + this.deleteDumpCells().catch(() => { + traceError('Error deleting temporary debug files.'); + }); + this.disposables.forEach((d) => d.dispose()); + } + + public stackTrace(args: DebugProtocol.StackTraceArguments): Thenable { + return this.session.customRequest('stackTrace', args); + } + + public setBreakpoints( + args: DebugProtocol.SetBreakpointsArguments + ): Thenable { + return this.session.customRequest('setBreakpoints', args); + } + + public abstract dumpAllCells(): Promise; + protected abstract dumpCell(index: number): Promise; + public getSourceMap(filePath: string) { + return this.cellToFile.get(filePath); + } + + private async debugInfo(): Promise { + const response = await this.session.customRequest('debugInfo'); + + // If there's stopped threads at this point, continue them all + (response as IDebugInfoResponse).stoppedThreads.forEach((thread: number) => { + this.jupyterSession.requestDebug({ + seq: 0, + type: 'request', + command: 'continue', + arguments: { + threadId: thread + } + }); + }); + } + + private lookupCellByLongName(sourcePath: string) { + if (!this.platformService.isWindows) { + return undefined; + } + + sourcePath = path.normalize(sourcePath); + for (let [file, cell] of this.fileToCell.entries()) { + if (isShortNamePath(file) && shortNameMatchesLongName(file, sourcePath)) { + return cell; + } + } + + return undefined; + } + + // Use our jupyter session to delete all the cells + private async deleteDumpCells() { + const fileValues = [...this.cellToFile.values()]; + // Need to have our Jupyter Session and some dumpCell files to delete + if (this.jupyterSession && fileValues.length) { + // Create our python string of file names + const fileListString = fileValues + .map((filePath) => { + return '"' + filePath.path + '"'; + }) + .join(','); + + // Insert into our delete snippet + const deleteFilesCode = `import os +_VSCODE_fileList = [${fileListString}] +for file in _VSCODE_fileList: + try: + os.remove(file) + except: + pass +del _VSCODE_fileList`; + + return executeSilently(this.jupyterSession, deleteFilesCode, { + traceErrors: true, + traceErrorsMessage: 'Error deleting temporary debugging files' + }); + } + } + + private async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { + if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') { + traceInfo(`Skipping sending message ${message.type} because session is disposed`); + return; + } + // map Source paths from VS Code to Ipykernel temp files + getMessageSourceAndHookIt(message, (source, lines?: { line?: number; endLine?: number; lines?: number[] }) => { + if (source && source.path) { + const mapping = this.cellToFile.get(source.path); + if (mapping) { + source.path = mapping.path; + if (typeof lines?.endLine === 'number') { + lines.endLine = lines.endLine - (mapping.lineOffset || 0); + } + if (typeof lines?.line === 'number') { + lines.line = lines.line - (mapping.lineOffset || 0); + } + if (lines?.lines && Array.isArray(lines?.lines)) { + lines.lines = lines?.lines.map((line) => line - (mapping.lineOffset || 0)); + } + } + } + }); + + this.trace('to kernel', JSON.stringify(message)); + if (message.type === 'request') { + const request = message as DebugProtocol.Request; + const control = this.jupyterSession.requestDebug( + { + seq: request.seq, + type: 'request', + command: request.command, + arguments: request.arguments + }, + true + ); + + control.onReply = (msg) => { + const message = msg.content as DebugProtocol.ProtocolMessage; + getMessageSourceAndHookIt( + message, + (source, lines?: { line?: number; endLine?: number; lines?: number[] }) => { + if (source && source.path) { + const mapping = this.fileToCell.get(source.path) ?? this.lookupCellByLongName(source.path); + if (mapping) { + source.name = path.basename(mapping.uri.path); + source.path = mapping.uri.toString(); + if (typeof lines?.endLine === 'number') { + lines.endLine = lines.endLine + (mapping.lineOffset || 0); + } + if (typeof lines?.line === 'number') { + lines.line = lines.line + (mapping.lineOffset || 0); + } + if (lines?.lines && Array.isArray(lines?.lines)) { + lines.lines = lines?.lines.map((line) => line + (mapping.lineOffset || 0)); + } + } + } + } + ); + + this.trace('response', JSON.stringify(message)); + this.sendMessage.fire(message); + }; + return control.done; + } else if (message.type === 'response') { + // responses of reverse requests + const response = message as DebugProtocol.Response; + const control = this.jupyterSession.requestDebug( + { + seq: response.seq, + type: 'request', + command: response.command + }, + true + ); + return control.done; + } else { + // cannot send via iopub, no way to handle events even if they existed + traceError(`Unknown message type to send ${message.type}`); + } + } +} diff --git a/src/kernels/debugger/types.ts b/src/kernels/debugger/types.ts index 5dd180a4efa..7ca5363750d 100644 --- a/src/kernels/debugger/types.ts +++ b/src/kernels/debugger/types.ts @@ -6,12 +6,14 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { IDebugService } from '../../platform/common/application/types'; import { DebugAdapter, + DebugAdapterTracker, DebugConfiguration, DebugProtocolMessage, DebugSession, Event, NotebookCell, - NotebookDocument + NotebookDocument, + NotebookEditor } from 'vscode'; export interface ISourceMapMapping { @@ -68,6 +70,12 @@ export interface IKernelDebugAdapter extends DebugAdapter { onDidEndSession: Event; dumpAllCells(): Promise; getConfiguration(): IKernelDebugAdapterConfig; + getSourceMap(filePath: string): + | { + path: string; + lineOffset?: number; + } + | undefined; } export const IDebuggingManager = Symbol('IDebuggingManager'); @@ -80,6 +88,11 @@ export interface IDebuggingManager { getDebugAdapter(notebook: NotebookDocument): IKernelDebugAdapter | undefined; } +export const IInteractiveWindowDebuggingManager = Symbol('IInteractiveWindowDebuggingManager'); +export interface IInteractiveWindowDebuggingManager extends IDebuggingManager { + start(editor: NotebookEditor, cell: NotebookCell): Promise; +} + 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. @@ -114,12 +127,14 @@ export interface IDebugInfoResponseBreakpoint { export enum KernelDebugMode { RunByLine, Cell, - Everything + Everything, + InteractiveWindow } export interface IKernelDebugAdapterConfig extends DebugConfiguration { __mode: KernelDebugMode; __cellIndex?: number; + __interactiveWindowNotebookUri?: string; } export interface IDebugLocation { @@ -127,6 +142,10 @@ export interface IDebugLocation { lineNumber: number; column: number; } +export const IDebugLocationTrackerFactory = Symbol('IDebugLocationTrackerFactory'); +export interface IDebugLocationTrackerFactory { + createDebugAdapterTracker(session: DebugSession): DebugAdapterTracker; +} export const IDebugLocationTracker = Symbol('IDebugLocationTracker'); export interface IDebugLocationTracker { diff --git a/src/kernels/kernel.base.ts b/src/kernels/kernel.base.ts index 2eb85382e15..2d264b93022 100644 --- a/src/kernels/kernel.base.ts +++ b/src/kernels/kernel.base.ts @@ -602,7 +602,12 @@ export abstract class BaseKernel implements IKernel { ]); // Have our debug cell script run first for safety - result.push(...debugCellScripts); + if ( + isLocalConnection(this.kernelConnectionMetadata) && + !this.configService.getSettings(undefined).useJupyterDebugger + ) { + result.push(...debugCellScripts); + } result.push(...changeDirScripts); // Set the ipynb file diff --git a/src/kernels/variables/debuggerVariableRegistration.node.ts b/src/kernels/variables/debuggerVariableRegistration.node.ts index aff1d698ece..cf44fb1efe5 100644 --- a/src/kernels/variables/debuggerVariableRegistration.node.ts +++ b/src/kernels/variables/debuggerVariableRegistration.node.ts @@ -7,7 +7,7 @@ import { IExtensionSingleActivationService } from '../../platform/activation/typ import { IDebugService } from '../../platform/common/application/types'; import { PYTHON_LANGUAGE } from '../../platform/common/constants'; import { IDisposableRegistry } from '../../platform/common/types'; -import { pythonKernelDebugAdapter } from '../debugger/constants'; +import { pythonIWKernelDebugAdapter, pythonKernelDebugAdapter } from '../debugger/constants'; import { Identifiers } from '../../webviews/webview-side/common/constants'; import { IJupyterDebugService } from '../debugger/types'; import { IJupyterVariables } from './types'; @@ -22,6 +22,7 @@ export class DebuggerVariableRegistration implements IExtensionSingleActivationS public activate(): Promise { this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(PYTHON_LANGUAGE, this)); this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(pythonKernelDebugAdapter, this)); + this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(pythonIWKernelDebugAdapter, this)); return Promise.resolve(); } diff --git a/src/notebooks/debugger/debuggingManager.ts b/src/notebooks/debugger/debuggingManager.ts index 9d3f040e655..db90ddbef24 100644 --- a/src/notebooks/debugger/debuggingManager.ts +++ b/src/notebooks/debugger/debuggingManager.ts @@ -5,25 +5,21 @@ import { inject, injectable } from 'inversify'; import { - debug, NotebookDocument, - workspace, DebugAdapterInlineImplementation, DebugSession, NotebookCell, DebugSessionOptions, DebugAdapterDescriptor, - Event, - EventEmitter, - NotebookEditor + NotebookEditor, + debug } from 'vscode'; import * as path from '../../platform/vscode-path/path'; -import { IKernel, IKernelProvider } from '../../kernels/types'; -import { IConfigurationService, IDisposable } from '../../platform/common/types'; +import { IKernelProvider } from '../../kernels/types'; +import { IConfigurationService } from '../../platform/common/types'; import { Commands as DSCommands, EditorContexts } from '../../webviews/webview-side/common/constants'; import { IExtensionSingleActivationService } from '../../platform/activation/types'; import { ContextKey } from '../../platform/common/contextKey'; -import { Debugger } from './debugger'; import { RunByLineController } from './runByLineController'; import { INotebookControllerManager } from '../types'; import { IApplicationShell, ICommandManager, IVSCodeNotebook } from '../../platform/common/application/types'; @@ -34,39 +30,39 @@ import { traceError, traceInfo, traceInfoIfCI } from '../../platform/logging'; import { DataScience } from '../../platform/common/utils/localize'; import { DebugCellController } from './debugCellControllers'; import { KernelDebugAdapter } from './kernelDebugAdapter'; -import { assertIsDebugConfig, IpykernelCheckResult, isUsingIpykernel6OrLater } from './helper'; +import { assertIsDebugConfig, IpykernelCheckResult } from './helper'; import { IDebuggingManager, IKernelDebugAdapterConfig, KernelDebugMode } from '../../kernels/debugger/types'; +import { DebuggingManagerBase } from '../../kernels/debugger/debuggingManagerBase'; /** * The DebuggingManager maintains the mapping between notebook documents and debug sessions. */ @injectable() -export class DebuggingManager implements IExtensionSingleActivationService, IDebuggingManager, IDisposable { +export class DebuggingManager + extends DebuggingManagerBase + implements IExtensionSingleActivationService, IDebuggingManager +{ private debuggingInProgress: ContextKey; private runByLineInProgress: ContextKey; - private notebookToDebugger = new Map(); - private notebookToDebugAdapter = new Map(); private notebookToRunByLineController = new Map(); - private notebookInProgress = new Set(); - private readonly disposables: IDisposable[] = []; - private _doneDebugging = new EventEmitter(); public constructor( - @inject(IKernelProvider) private kernelProvider: IKernelProvider, - @inject(INotebookControllerManager) private readonly notebookControllerManager: INotebookControllerManager, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, - @inject(IConfigurationService) private settings: IConfigurationService, - @inject(IPlatformService) private platform: IPlatformService + @inject(IKernelProvider) kernelProvider: IKernelProvider, + @inject(INotebookControllerManager) notebookControllerManager: INotebookControllerManager, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IVSCodeNotebook) vscNotebook: IVSCodeNotebook, + @inject(IConfigurationService) private readonly settings: IConfigurationService, + @inject(IPlatformService) private readonly platform: IPlatformService ) { - this.debuggingInProgress = new ContextKey(EditorContexts.DebuggingInProgress, this.commandManager); - this.runByLineInProgress = new ContextKey(EditorContexts.RunByLineInProgress, this.commandManager); + super(kernelProvider, notebookControllerManager, commandManager, appShell, vscNotebook); + this.debuggingInProgress = new ContextKey(EditorContexts.DebuggingInProgress, commandManager); + this.runByLineInProgress = new ContextKey(EditorContexts.RunByLineInProgress, commandManager); this.updateToolbar(false); this.updateCellToolbar(false); this.disposables.push( - this.vscNotebook.onDidChangeActiveNotebookEditor( + vscNotebook.onDidChangeActiveNotebookEditor( (e?: NotebookEditor) => { if (e) { this.updateCellToolbar(this.isDebugging(e.notebook)); @@ -79,26 +75,13 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb ); } - public async activate() { + public override async activate() { + await super.activate(); this.disposables.push( - // track termination of debug sessions - debug.onDidTerminateDebugSession(this.endSession.bind(this)), - - // track closing of notebooks documents - workspace.onDidCloseNotebookDocument(async (document) => { - const dbg = this.notebookToDebugger.get(document); - if (dbg) { - await dbg.stop(); - this.updateToolbar(false); - this.updateCellToolbar(false); - } - }), - // 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); @@ -176,37 +159,14 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb ); } - public get onDoneDebugging(): Event { - return this._doneDebugging.event; - } - - public dispose() { - this.disposables.forEach((d) => d.dispose()); - } - - public isDebugging(notebook: NotebookDocument): boolean { - return this.notebookToDebugger.has(notebook); - } - - public getDebugSession(notebook: NotebookDocument): Promise | undefined { - const dbg = this.notebookToDebugger.get(notebook); - if (dbg) { - return dbg.session; - } - } - public getDebugMode(notebook: NotebookDocument): KernelDebugMode | undefined { const controller = this.notebookToRunByLineController.get(notebook); return controller?.getMode(); } - public getDebugCell(notebook: NotebookDocument): NotebookCell | undefined { - const controller = this.notebookToRunByLineController.get(notebook); - return controller?.debugCell; - } - - public getDebugAdapter(notebook: NotebookDocument): KernelDebugAdapter | undefined { - return this.notebookToDebugAdapter.get(notebook); + protected override onDidStopDebugging(_notebook: NotebookDocument) { + this.updateToolbar(false); + this.updateCellToolbar(false); } private updateToolbar(debugging: boolean) { @@ -325,50 +285,17 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb return this.startDebuggingConfig(doc, config); } - private async startDebuggingConfig( - doc: NotebookDocument, - config: IKernelDebugAdapterConfig, - 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})`); - void this.appShell.showErrorMessage(DataScience.cantStartDebugging()); - } - } else { - traceInfoIfCI(`Not starting debugging because already debugging in this notebook`); - } - } - - private async endSession(session: DebugSession) { - traceInfoIfCI(`Ending debug session ${session.id}`); - this._doneDebugging.fire(); - for (const [doc, dbg] of this.notebookToDebugger.entries()) { - if (dbg && session.id === (await dbg.session).id) { - this.notebookToDebugger.delete(doc); - this.notebookToDebugAdapter.delete(doc); - this.updateToolbar(false); - this.updateCellToolbar(false); - break; - } - } - } - - private async createDebugAdapterDescriptor(session: DebugSession): Promise { + protected override async createDebugAdapterDescriptor( + session: DebugSession + ): Promise { const config = session.configuration; assertIsDebugConfig(config); - - if (this.vscNotebook.activeNotebookEditor) { - const activeDoc = this.vscNotebook.activeNotebookEditor.notebook; - + 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); @@ -400,8 +327,7 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb adapter.setDebuggingDelegate(controller); } - this.notebookToDebugAdapter.set(debug.document, adapter); - this.disposables.push(adapter.onDidEndSession(this.endSession.bind(this))); + this.trackDebugAdapter(debug.document, adapter); // Wait till we're attached before resolving the session debug.resolve(session); @@ -414,76 +340,4 @@ export class DebuggingManager implements IExtensionSingleActivationService, IDeb traceError('Debug sessions should start only from the cell toolbar command'); return; } - - private getDebuggerByUri(document: NotebookDocument): Debugger | undefined { - for (const [doc, dbg] of this.notebookToDebugger.entries()) { - if (document === doc) { - return dbg; - } - } - } - - private async ensureKernelIsRunning(doc: NotebookDocument): Promise { - await this.notebookControllerManager.loadNotebookControllers(); - const controller = this.notebookControllerManager.getSelectedNotebookController(doc); - - let kernel = this.kernelProvider.get(doc.uri); - if (!kernel && controller) { - kernel = this.kernelProvider.getOrCreate(doc.uri, { - metadata: controller.connection, - controller: controller?.controller, - resourceUri: doc.uri, - creator: 'jupyterExtension' - }); - } - if (kernel && kernel.status === 'unknown') { - await kernel.start(); - } - - return kernel; - } - - private async checkForIpykernel6(doc: NotebookDocument): Promise { - try { - let kernel = this.kernelProvider.get(doc.uri); - if (!kernel) { - const controller = this.notebookControllerManager.getSelectedNotebookController(doc); - if (!controller) { - return IpykernelCheckResult.ControllerNotSelected; - } - kernel = this.kernelProvider.getOrCreate(doc.uri, { - metadata: controller.connection, - controller: controller?.controller, - resourceUri: doc.uri, - creator: 'jupyterExtension' - }); - } - - const result = await isUsingIpykernel6OrLater(kernel); - sendTelemetryEvent(DebuggingTelemetry.ipykernel6Status, undefined, { - status: result === IpykernelCheckResult.Ok ? 'installed' : 'notInstalled' - }); - return result; - } catch (ex) { - traceError('Debugging: Could not check for ipykernel 6', ex); - } - return IpykernelCheckResult.Unknown; - } - - private async promptInstallIpykernel6() { - const response = await this.appShell.showInformationMessage( - DataScience.needIpykernel6(), - { modal: true }, - DataScience.setup() - ); - - if (response === DataScience.setup()) { - sendTelemetryEvent(DebuggingTelemetry.clickedOnSetup); - this.appShell.openUrl( - 'https://github.com/microsoft/vscode-jupyter/wiki/Setting-Up-Run-by-Line-and-Debugging-for-Notebooks' - ); - } else { - sendTelemetryEvent(DebuggingTelemetry.closedModal); - } - } } diff --git a/src/notebooks/debugger/helper.ts b/src/notebooks/debugger/helper.ts index 70c651e788b..53bac4097e0 100644 --- a/src/notebooks/debugger/helper.ts +++ b/src/notebooks/debugger/helper.ts @@ -30,7 +30,9 @@ export function assertIsDebugConfig(thing: unknown): asserts thing is IKernelDeb const config = thing as IKernelDebugAdapterConfig; if ( typeof config.__mode === 'undefined' || - ((config.__mode === KernelDebugMode.Cell || config.__mode === KernelDebugMode.RunByLine) && + ((config.__mode === KernelDebugMode.Cell || + config.__mode === KernelDebugMode.InteractiveWindow || + config.__mode === KernelDebugMode.RunByLine) && typeof config.__cellIndex === 'undefined') ) { throw new Error('Invalid launch configuration'); @@ -39,20 +41,32 @@ export function assertIsDebugConfig(thing: unknown): asserts thing is IKernelDeb export function getMessageSourceAndHookIt( msg: DebugProtocol.ProtocolMessage, - sourceHook: (source: DebugProtocol.Source | undefined) => void + sourceHook: ( + source: DebugProtocol.Source | undefined, + lines?: { line?: number; endLine?: number; lines?: number[] } + ) => void ): void { switch (msg.type) { case 'event': const event = msg as DebugProtocol.Event; switch (event.event) { case 'output': - sourceHook((event as DebugProtocol.OutputEvent).body.source); + sourceHook( + (event as DebugProtocol.OutputEvent).body.source, + (event as DebugProtocol.OutputEvent).body + ); break; case 'loadedSource': - sourceHook((event as DebugProtocol.LoadedSourceEvent).body.source); + sourceHook( + (event as DebugProtocol.LoadedSourceEvent).body.source, + (event as DebugProtocol.OutputEvent).body + ); break; case 'breakpoint': - sourceHook((event as DebugProtocol.BreakpointEvent).body.breakpoint.source); + sourceHook( + (event as DebugProtocol.BreakpointEvent).body.breakpoint.source, + (event as DebugProtocol.OutputEvent).body + ); break; default: break; @@ -62,16 +76,30 @@ export function getMessageSourceAndHookIt( const request = msg as DebugProtocol.Request; switch (request.command) { case 'setBreakpoints': + // Keep track of the original source to be passed for other hooks. + const originalSource = { ...(request.arguments as DebugProtocol.SetBreakpointsArguments).source }; sourceHook((request.arguments as DebugProtocol.SetBreakpointsArguments).source); + // We pass a copy of the original source, as only the original object as the unaltered source. + sourceHook({ ...originalSource }, request.arguments); + const breakpoints = (request.arguments as DebugProtocol.SetBreakpointsArguments).breakpoints; + if (breakpoints && Array.isArray(breakpoints)) { + breakpoints.forEach((bk) => { + // Pass the original source to the hook (without the translation). + sourceHook({ ...originalSource }, bk); + }); + } break; case 'breakpointLocations': - sourceHook((request.arguments as DebugProtocol.BreakpointLocationsArguments).source); + sourceHook( + (request.arguments as DebugProtocol.BreakpointLocationsArguments).source, + request.arguments + ); break; case 'source': sourceHook((request.arguments as DebugProtocol.SourceArguments).source); break; case 'gotoTargets': - sourceHook((request.arguments as DebugProtocol.GotoTargetsArguments).source); + sourceHook((request.arguments as DebugProtocol.GotoTargetsArguments).source, request.arguments); break; default: break; @@ -82,9 +110,9 @@ export function getMessageSourceAndHookIt( if (response.success && response.body) { switch (response.command) { case 'stackTrace': - (response as DebugProtocol.StackTraceResponse).body.stackFrames.forEach((frame) => - sourceHook(frame.source) - ); + (response as DebugProtocol.StackTraceResponse).body.stackFrames.forEach((frame) => { + sourceHook(frame.source, frame); + }); break; case 'loadedSources': (response as DebugProtocol.LoadedSourcesResponse).body.sources.forEach((source) => @@ -92,19 +120,19 @@ export function getMessageSourceAndHookIt( ); break; case 'scopes': - (response as DebugProtocol.ScopesResponse).body.scopes.forEach((scope) => - sourceHook(scope.source) - ); + (response as DebugProtocol.ScopesResponse).body.scopes.forEach((scope) => { + sourceHook(scope.source, scope); + }); break; case 'setFunctionBreakpoints': - (response as DebugProtocol.SetFunctionBreakpointsResponse).body.breakpoints.forEach((bp) => - sourceHook(bp.source) - ); + (response as DebugProtocol.SetFunctionBreakpointsResponse).body.breakpoints.forEach((bp) => { + sourceHook(bp.source, bp); + }); break; case 'setBreakpoints': - (response as DebugProtocol.SetBreakpointsResponse).body.breakpoints.forEach((bp) => - sourceHook(bp.source) - ); + (response as DebugProtocol.SetBreakpointsResponse).body.breakpoints.forEach((bp) => { + sourceHook(bp.source, bp); + }); break; default: break; diff --git a/src/notebooks/debugger/kernelDebugAdapter.ts b/src/notebooks/debugger/kernelDebugAdapter.ts index 416a19d60cd..e045ab52e5a 100644 --- a/src/notebooks/debugger/kernelDebugAdapter.ts +++ b/src/notebooks/debugger/kernelDebugAdapter.ts @@ -3,210 +3,14 @@ 'use strict'; -import { KernelMessage } from '@jupyterlab/services'; import * as path from '../../platform/vscode-path/path'; -import { - debug, - DebugAdapter, - DebugProtocolMessage, - DebugSession, - Event, - EventEmitter, - NotebookCell, - NotebookCellExecutionState, - NotebookCellExecutionStateChangeEvent, - NotebookCellKind, - NotebookDocument, - notebooks, - workspace -} from 'vscode'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { assertIsDebugConfig, getMessageSourceAndHookIt, isShortNamePath, shortNameMatchesLongName } from './helper'; -import { executeSilently } from '../../kernels/helpers'; -import { IJupyterSession, IKernel } from '../../kernels/types'; -import { IPlatformService } from '../../platform/common/platform/types'; -import { DebuggingTelemetry } from '../../kernels/debugger/constants'; -import { - IKernelDebugAdapter, - IKernelDebugAdapterConfig, - IDebuggingDelegate, - KernelDebugMode, - IDumpCellResponse, - IDebugInfoResponse -} from '../../kernels/debugger/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { IDisposable } from '../../platform/common/types'; -import { traceError, traceInfo, traceInfoIfCI, traceVerbose } from '../../platform/logging'; +import { NotebookCellKind } from 'vscode'; +import { IDumpCellResponse } from '../../kernels/debugger/types'; +import { traceError } from '../../platform/logging'; +import { KernelDebugAdapterBase } from '../../kernels/debugger/kernelDebugAdapterBase'; -// For info on the custom requests implemented by jupyter see: -// https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request -// https://jupyter-client.readthedocs.io/en/stable/messaging.html#additions-to-the-dap -export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, IDisposable { - private readonly fileToCell = new Map(); - private readonly cellToFile = new Map(); - private readonly sendMessage = new EventEmitter(); - private readonly endSession = new EventEmitter(); - private readonly configuration: IKernelDebugAdapterConfig; - private readonly disposables: IDisposable[] = []; - private delegate: IDebuggingDelegate | undefined; - onDidSendMessage: Event = this.sendMessage.event; - onDidEndSession: Event = this.endSession.event; - public readonly debugCell: NotebookCell | undefined; - private disconnected: boolean = false; - private kernelEventHook = (_event: 'willRestart' | 'willInterrupt') => this.disconnect(); - - constructor( - private session: DebugSession, - private notebookDocument: NotebookDocument, - private readonly jupyterSession: IJupyterSession, - private readonly kernel: IKernel | undefined, - private readonly platformService: IPlatformService - ) { - traceInfoIfCI(`Creating kernel debug adapter for debugging notebooks`); - const configuration = this.session.configuration; - assertIsDebugConfig(configuration); - this.configuration = configuration; - - if (configuration.__mode === KernelDebugMode.Cell || configuration.__mode === KernelDebugMode.RunByLine) { - this.debugCell = notebookDocument.cellAt(configuration.__cellIndex!); - } - - this.disposables.push( - this.jupyterSession.onIOPubMessage(async (msg: KernelMessage.IIOPubMessage) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const anyMsg = msg as any; - traceInfoIfCI(`Debug IO Pub message: ${JSON.stringify(msg)}`); - if (anyMsg.header.msg_type === 'debug_event') { - this.trace('event', JSON.stringify(msg)); - if (!(await this.delegate?.willSendEvent(anyMsg))) { - this.sendMessage.fire(msg.content); - } - } - }) - ); - - if (this.kernel) { - this.kernel.addEventHook(this.kernelEventHook); - this.disposables.push( - this.kernel.onDisposed(() => { - void debug.stopDebugging(this.session); - this.endSession.fire(this.session); - sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'onKernelDisposed' }); - }) - ); - } - - this.disposables.push( - notebooks.onDidChangeNotebookCellExecutionState( - (cellStateChange: NotebookCellExecutionStateChangeEvent) => { - // If a cell has moved to idle, stop the debug session - if ( - this.configuration.__cellIndex === cellStateChange.cell.index && - cellStateChange.state === NotebookCellExecutionState.Idle && - !this.disconnected - ) { - sendTelemetryEvent(DebuggingTelemetry.endedSession, undefined, { reason: 'normally' }); - void this.disconnect(); - } - }, - this, - this.disposables - ) - ); - - this.disposables.push( - workspace.onDidChangeNotebookDocument( - (e) => { - e.contentChanges.forEach((change) => { - change.removedCells.forEach((cell: NotebookCell) => { - if (cell === this.debugCell) { - void this.disconnect(); - } - }); - }); - }, - this, - this.disposables - ) - ); - } - - public setDebuggingDelegate(delegate: IDebuggingDelegate) { - this.delegate = delegate; - } - - private trace(tag: string, msg: string) { - traceVerbose(`[Debug] ${tag}: ${msg}`); - } - - async handleMessage(message: DebugProtocol.ProtocolMessage) { - try { - traceInfoIfCI(`KernelDebugAdapter::handleMessage ${JSON.stringify(message, undefined, ' ')}`); - // intercept 'setBreakpoints' request - if (message.type === 'request' && (message as DebugProtocol.Request).command === 'setBreakpoints') { - const args = (message as DebugProtocol.Request).arguments; - if (args.source && args.source.path && args.source.path.indexOf('vscode-notebook-cell:') === 0) { - const cell = this.notebookDocument - .getCells() - .find((c) => c.document.uri.toString() === args.source.path); - if (cell) { - await this.dumpCell(cell.index); - } - } - } - - // after attaching, send a 'debugInfo' request - // reset breakpoints and continue stopped threads if there are any - // we do this in case the kernel is stopped when we attach - // This might happen if VS Code or the extension host crashes - if (message.type === 'request' && (message as DebugProtocol.Request).command === 'attach') { - await this.debugInfo(); - } - - if (message.type === 'request') { - await this.delegate?.willSendRequest(message as DebugProtocol.Request); - } - - return this.sendRequestToJupyterSession(message); - } catch (e) { - traceError(`KernelDebugAdapter::handleMessage failure: ${e}`); - } - } - - public getConfiguration(): IKernelDebugAdapterConfig { - return this.configuration; - } - - public stepIn(threadId: number): Thenable { - return this.session.customRequest('stepIn', { threadId }); - } - - public async disconnect() { - await this.session.customRequest('disconnect', { restart: false }); - this.endSession.fire(this.session); - this.disconnected = true; - this.kernel?.removeEventHook(this.kernelEventHook); - } - - dispose() { - // On dispose, delete our temp cell files - this.deleteDumpCells().catch(() => { - traceError('Error deleting temporary debug files.'); - }); - this.disposables.forEach((d) => d.dispose()); - } - - public stackTrace(args: DebugProtocol.StackTraceArguments): Thenable { - return this.session.customRequest('stackTrace', args); - } - - public setBreakpoints( - args: DebugProtocol.SetBreakpointsArguments - ): Thenable { - return this.session.customRequest('setBreakpoints', args); - } - - public async dumpAllCells() { +export class KernelDebugAdapter extends KernelDebugAdapterBase { + public override async dumpAllCells() { await Promise.all( this.notebookDocument.getCells().map(async (cell) => { if (cell.kind === NotebookCellKind.Code) { @@ -215,146 +19,25 @@ export class KernelDebugAdapter implements DebugAdapter, IKernelDebugAdapter, ID }) ); } - + public override getSourceMap(filePath: string) { + return this.cellToFile.get(filePath); + } // Dump content of given cell into a tmp file and return path to file. - private async dumpCell(index: number): Promise { + protected override async dumpCell(index: number): Promise { const cell = this.notebookDocument.cellAt(index); - if (cell) { - try { - const response = await this.session.customRequest('dumpCell', { - code: cell.document.getText().replace(/\r\n/g, '\n') - }); - const norm = path.normalize((response as IDumpCellResponse).sourcePath); - this.fileToCell.set(norm, cell); - this.cellToFile.set(cell.document.uri.toString(), norm); - } catch (err) { - traceError(err); - } - } - } - - private async debugInfo(): Promise { - const response = await this.session.customRequest('debugInfo'); - - // If there's stopped threads at this point, continue them all - (response as IDebugInfoResponse).stoppedThreads.forEach((thread: number) => { - this.jupyterSession.requestDebug({ - seq: 0, - type: 'request', - command: 'continue', - arguments: { - threadId: thread - } + try { + const response = await this.session.customRequest('dumpCell', { + code: cell.document.getText().replace(/\r\n/g, '\n') }); - }); - } - - private lookupCellByLongName(sourcePath: string): NotebookCell | undefined { - if (!this.platformService.isWindows) { - return undefined; - } - - sourcePath = path.normalize(sourcePath); - for (let [file, cell] of this.fileToCell.entries()) { - if (isShortNamePath(file) && shortNameMatchesLongName(file, sourcePath)) { - return cell; - } - } - - return undefined; - } - - // Use our jupyter session to delete all the cells - private async deleteDumpCells() { - const fileValues = [...this.cellToFile.values()]; - // Need to have our Jupyter Session and some dumpCell files to delete - if (this.jupyterSession && fileValues.length) { - // Create our python string of file names - const fileListString = fileValues - .map((filePath) => { - return '"' + filePath + '"'; - }) - .join(','); - - // Insert into our delete snippet - const deleteFilesCode = `import os -_VSCODE_fileList = [${fileListString}] -for file in _VSCODE_fileList: - try: - os.remove(file) - except: - pass -del _VSCODE_fileList`; - - return executeSilently(this.jupyterSession, deleteFilesCode, { - traceErrors: true, - traceErrorsMessage: 'Error deleting temporary debugging files' + const norm = path.normalize((response as IDumpCellResponse).sourcePath); + this.fileToCell.set(norm, { + uri: cell.document.uri }); - } - } - - private async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { - if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') { - traceInfo(`Skipping sending message ${message.type} because session is disposed`); - return; - } - // map Source paths from VS Code to Ipykernel temp files - getMessageSourceAndHookIt(message, (source) => { - if (source && source.path) { - const path = this.cellToFile.get(source.path); - if (path) { - source.path = path; - } - } - }); - - this.trace('to kernel', JSON.stringify(message)); - if (message.type === 'request') { - const request = message as DebugProtocol.Request; - const control = this.jupyterSession.requestDebug( - { - seq: request.seq, - type: 'request', - command: request.command, - arguments: request.arguments - }, - true - ); - - control.onReply = (msg) => { - const message = msg.content as DebugProtocol.ProtocolMessage; - getMessageSourceAndHookIt(message, (source) => { - if (source && source.path) { - const cell = this.fileToCell.get(source.path) ?? this.lookupCellByLongName(source.path); - if (cell) { - source.name = path.basename(cell.document.uri.path); - if (cell.index >= 0) { - source.name += `, Cell ${cell.index + 1}`; - } - source.path = cell.document.uri.toString(); - } - } - }); - - this.trace('response', JSON.stringify(message)); - this.sendMessage.fire(message); - }; - return control.done; - } else if (message.type === 'response') { - // responses of reverse requests - const response = message as DebugProtocol.Response; - const control = this.jupyterSession.requestDebug( - { - seq: response.seq, - type: 'request', - command: response.command - }, - true - ); - return control.done; - } else { - // cannot send via iopub, no way to handle events even if they existed - traceError(`Unknown message type to send ${message.type}`); + this.cellToFile.set(cell.document.uri.toString(), { + path: norm + }); + } catch (err) { + traceError(err); } } } diff --git a/src/notebooks/execution/cellExecution.ts b/src/notebooks/execution/cellExecution.ts index d93e4e75d0b..7aeb7a4673f 100644 --- a/src/notebooks/execution/cellExecution.ts +++ b/src/notebooks/execution/cellExecution.ts @@ -481,7 +481,7 @@ export class CellExecution implements IDisposable { try { // Compute the hash for the cell we're about to execute if on the interactive window - const iwCellMetata = getInteractiveCellMetadata(this.cell); + const iwCellMetadata = getInteractiveCellMetadata(this.cell); // At this point we're about to ACTUALLY execute some code. Fire an event to indicate that this._preExecuteEmitter.fire(this.cell); @@ -490,7 +490,7 @@ export class CellExecution implements IDisposable { // https://jupyter-client.readthedocs.io/en/stable/api/client.html#jupyter_client.KernelClient.execute this.request = session.requestExecute( { - code: iwCellMetata?.generatedCode?.code || code, + code: iwCellMetadata?.generatedCode?.code || code, silent: false, stop_on_error: false, allow_stdin: true, diff --git a/src/platform/serviceRegistry.node.ts b/src/platform/serviceRegistry.node.ts index 6b969930cf4..088d81d847b 100644 --- a/src/platform/serviceRegistry.node.ts +++ b/src/platform/serviceRegistry.node.ts @@ -32,7 +32,7 @@ import { ExtensionRecommendationService } from './common/extensionRecommendation import { GlobalActivation } from './common/globalActivation'; import { PreReleaseChecker } from './common/prereleaseChecker.node'; import { IConfigurationService, IDataScienceCommandListener, IExtensionContext } from './common/types'; -import { DebugLocationTrackerFactory } from '../kernels/debugger/debugLocationTrackerFactory.node'; +import { DebugLocationTrackerFactory } from '../kernels/debugger/debugLocationTrackerFactory'; import { DataScienceErrorHandler } from './errors/errorHandler'; import { IDataScienceErrorHandler } from './errors/types'; import { ExportBase } from './export/exportBase.node'; @@ -63,7 +63,13 @@ import { ExtensionSideRenderer, IExtensionSideRenderer } from '../webviews/exten import { OutputCommandListener } from './logging/outputCommandListener'; import { ExportUtilBase } from './export/exportUtil'; import { DebuggingManager } from '../notebooks/debugger/debuggingManager'; -import { IDebuggingManager, IDebugLocationTracker } from '../kernels/debugger/types'; +import { + IDebuggingManager, + IDebugLocationTracker, + IDebugLocationTrackerFactory, + IInteractiveWindowDebuggingManager +} from '../kernels/debugger/types'; +import { InteractiveWindowDebuggingManager } from '../interactive-window/debugger/jupyter/debuggingManager'; export function registerTypes(context: IExtensionContext, serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton(FileSystem, FileSystem); @@ -94,7 +100,9 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IExtensionSingleActivationService, GlobalActivation); serviceManager.addSingleton(IDataScienceCommandListener, GitHubIssueCommandListener); serviceManager.addSingleton(IDataViewerFactory, DataViewerFactory); - serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory); + serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory, undefined, [ + IDebugLocationTrackerFactory + ]); serviceManager.addSingleton(IExtensionSingleActivationService, Activation); if (isDevMode) { serviceManager.addSingleton( @@ -131,6 +139,12 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IDebuggingManager, DebuggingManager, undefined, [ IExtensionSingleActivationService ]); + serviceManager.addSingleton( + IInteractiveWindowDebuggingManager, + InteractiveWindowDebuggingManager, + undefined, + [IExtensionSingleActivationService] + ); serviceManager.addSingleton( IExtensionSingleActivationService, PreReleaseChecker diff --git a/src/platform/serviceRegistry.web.ts b/src/platform/serviceRegistry.web.ts index b754ec27415..bb422d02db3 100644 --- a/src/platform/serviceRegistry.web.ts +++ b/src/platform/serviceRegistry.web.ts @@ -46,6 +46,7 @@ import { IDataViewerFactory } from '../webviews/extension-side/dataviewer/types' import { INotebookWatcher } from '../webviews/extension-side/variablesView/types'; import { DebuggingManager } from '../notebooks/debugger/debuggingManager'; import { IDebuggingManager } from '../kernels/debugger/types'; +import { InteractiveWindowDebuggingManager } from '../interactive-window/debugger/jupyter/debuggingManager'; export function registerTypes(context: IExtensionContext, serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton(IFileSystem, FileSystem); @@ -81,4 +82,7 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IDebuggingManager, DebuggingManager, undefined, [ IExtensionSingleActivationService ]); + serviceManager.addSingleton(IDebuggingManager, InteractiveWindowDebuggingManager, undefined, [ + IExtensionSingleActivationService + ]); } diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index efd3331e962..52a955043dd 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -1526,6 +1526,7 @@ export interface IEventNamePropertyMapping { }; [DebuggingTelemetry.clickedRunByLine]: never | undefined; [DebuggingTelemetry.successfullyStartedRunByLine]: never | undefined; + [DebuggingTelemetry.successfullyStartedIWJupyterDebugger]: never | undefined; [DebuggingTelemetry.clickedRunAndDebugCell]: never | undefined; [DebuggingTelemetry.successfullyStartedRunAndDebugCell]: never | undefined; [DebuggingTelemetry.endedSession]: { From 1df9d1041cfad4eaf066d60fe665b37c3ed57dec Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 11:27:52 +1000 Subject: [PATCH 04/15] Updates to metadata --- package.json | 3 +- src/interactive-window/interactiveWindow.ts | 39 +++++++++++++++------ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 30d6017a62e..0223cccb76d 100644 --- a/package.json +++ b/package.json @@ -683,7 +683,7 @@ }, { "command": "jupyter.notebookeditor.addcellbelow", - "title": "%jupyter.command.jupyter.notebookeditor.addcellbelow.title%", + "title": "%jupyter.command.jupyter.notebookeditor.Ķaddcellbelow.title%", "category": "Notebook", "enablement": "!jupyter.webExtension" }, @@ -2073,6 +2073,7 @@ }, "enabledApiProposals": [ "notebookControllerKind", + "notebookWorkspaceEdit", "notebookDebugOptions", "notebookDeprecated", "notebookEditor", diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index 8a49b376eb5..e9a1a2d2e69 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -18,7 +18,8 @@ import { NotebookEditor, Disposable, window, - NotebookController + NotebookController, + NotebookEdit } from 'vscode'; import { IPythonExtensionChecker } from '../platform/api/types'; import { @@ -521,7 +522,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { const promises = cells.map((c) => { // Add the cell first. We don't need to wait for this part as we want to add them // as quickly as possible - const notebookCellPromise = this.addNotebookCell(c, fileUri, line, isDebug); + const notebookCellPromise = this.addNotebookCell(c, fileUri, line); // Queue up execution const promise = this.createExecutionPromise(notebookCellPromise, isDebug); @@ -576,8 +577,9 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { let detachKernel = async () => noop(); try { const kernel = await kernelPromise; + await this.generateCodeAndAddMetadata(cell, isDebug, kernel); if (isDebug && (settings.useJupyterDebugger || !isLocalConnection(kernel.kernelConnectionMetadata))) { - // New ipykernel 7 debugger. + // New ipykernel 7 debugger using the Jupyter protocol. await this.debuggingManager.start(this.notebookEditor, cell); } else if (isDebug && isLocalConnection(kernel.kernelConnectionMetadata)) { // Old ipykernel 6 debugger. @@ -719,8 +721,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { private async addNotebookCell( code: string, file: Uri, - line: number, - isDebug: boolean + line: number ): Promise<{ cell: NotebookCell; wasScrolled: boolean }> { const notebookDocument = this.notebookEditor.notebook; @@ -754,15 +755,10 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { line: line, originalSource: code }; - const id = uuid(); - const generatedCode = this.codeGeneratorFactory - .getOrCreate(this.notebookDocument) - .generateCode({ interactive, id }, isDebug, true); const metadata: InteractiveCellMetadata = { interactiveWindowCellMarker, interactive, - generatedCode, id: uuid() }; notebookCellData.metadata = metadata; @@ -785,6 +781,29 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { return { cell, wasScrolled: shouldScroll }; } + private async generateCodeAndAddMetadata(cell: NotebookCell, isDebug: boolean, kernel: IKernel) { + const metadata = getInteractiveCellMetadata(cell); + if (!metadata) { + return; + } + const useJupyterDebugger = + !isLocalConnection(kernel.kernelConnectionMetadata) || + this.configuration.getSettings(undefined).useJupyterDebugger; + + const generatedCode = this.codeGeneratorFactory + .getOrCreate(this.notebookDocument) + .generateCode(metadata, isDebug, useJupyterDebugger); + + const newMetadata: typeof metadata = { + ...metadata, + generatedCode + }; + + const edit = new WorkspaceEdit(); + const cellEdit = NotebookEdit.updateCellMetadata(cell.index, newMetadata); + edit.set(cell.notebook.uri, [cellEdit]); + await workspace.applyEdit(edit); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-empty,@typescript-eslint/no-empty-function public async export() { From dd05b785d2e926964107c25c0f131ff593494c7b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 11:41:19 +1000 Subject: [PATCH 05/15] Fixes --- .../debugger/jupyter/debugCellControllers.ts | 4 ++-- .../debugger/jupyter/kernelDebugAdapter.ts | 3 --- src/kernels/debugger/kernelDebugAdapterBase.ts | 4 ++-- src/kernels/debugger/types.ts | 7 +------ src/notebooks/debugger/kernelDebugAdapter.ts | 3 --- 5 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/interactive-window/debugger/jupyter/debugCellControllers.ts b/src/interactive-window/debugger/jupyter/debugCellControllers.ts index 5bc378571e8..b4f3e4f97ea 100644 --- a/src/interactive-window/debugger/jupyter/debugCellControllers.ts +++ b/src/interactive-window/debugger/jupyter/debugCellControllers.ts @@ -32,7 +32,7 @@ export class DebugCellController implements IDebuggingDelegate { if (request.command === 'configurationDone' && metadata && metadata.generatedCode) { await cellDebugSetup(this.kernel, this.debugAdapter); - const realPath = this.debugAdapter.getSourceMap(metadata.interactive.uristring); + const realPath = this.debugAdapter.getSourcePath(metadata.interactive.uristring); if (realPath) { const initialBreakpoint: DebugProtocol.SourceBreakpoint = { line: metadata.generatedCode.firstExecutableLineIndex - metadata.interactive.line @@ -41,7 +41,7 @@ export class DebugCellController implements IDebuggingDelegate { await this.debugAdapter.setBreakpoints({ source: { name: path.basename(uri.path), - path: realPath.path + path: realPath }, breakpoints: [initialBreakpoint], sourceModified: false diff --git a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts index 8fa0ebab6b3..6d0a953cff6 100644 --- a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts +++ b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts @@ -73,9 +73,6 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { }) ); } - public override getSourceMap(filePath: string) { - return this.cellToFile.get(filePath); - } // Dump content of given cell into a tmp file and return path to file. protected async dumpCell(index: number): Promise { const cell = this.notebookDocument.cellAt(index); diff --git a/src/kernels/debugger/kernelDebugAdapterBase.ts b/src/kernels/debugger/kernelDebugAdapterBase.ts index e5b8c70e6cb..c403b1fdecb 100644 --- a/src/kernels/debugger/kernelDebugAdapterBase.ts +++ b/src/kernels/debugger/kernelDebugAdapterBase.ts @@ -229,8 +229,8 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb public abstract dumpAllCells(): Promise; protected abstract dumpCell(index: number): Promise; - public getSourceMap(filePath: string) { - return this.cellToFile.get(filePath); + public getSourcePath(filePath: string) { + return this.cellToFile.get(filePath)?.path; } private async debugInfo(): Promise { diff --git a/src/kernels/debugger/types.ts b/src/kernels/debugger/types.ts index 7ca5363750d..e04f2ad905e 100644 --- a/src/kernels/debugger/types.ts +++ b/src/kernels/debugger/types.ts @@ -70,12 +70,7 @@ export interface IKernelDebugAdapter extends DebugAdapter { onDidEndSession: Event; dumpAllCells(): Promise; getConfiguration(): IKernelDebugAdapterConfig; - getSourceMap(filePath: string): - | { - path: string; - lineOffset?: number; - } - | undefined; + getSourcePath(filePath: string): string | undefined; } export const IDebuggingManager = Symbol('IDebuggingManager'); diff --git a/src/notebooks/debugger/kernelDebugAdapter.ts b/src/notebooks/debugger/kernelDebugAdapter.ts index e045ab52e5a..184c05a584a 100644 --- a/src/notebooks/debugger/kernelDebugAdapter.ts +++ b/src/notebooks/debugger/kernelDebugAdapter.ts @@ -19,9 +19,6 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { }) ); } - public override getSourceMap(filePath: string) { - return this.cellToFile.get(filePath); - } // Dump content of given cell into a tmp file and return path to file. protected override async dumpCell(index: number): Promise { const cell = this.notebookDocument.cellAt(index); From 9f541e8b417644020bcf7b6036348bebb9a6ca4c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 12:25:13 +1000 Subject: [PATCH 06/15] Oops --- src/kernels/debugger/debuggingManagerBase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kernels/debugger/debuggingManagerBase.ts b/src/kernels/debugger/debuggingManagerBase.ts index fab9509d23d..4f926f13b2d 100644 --- a/src/kernels/debugger/debuggingManagerBase.ts +++ b/src/kernels/debugger/debuggingManagerBase.ts @@ -31,7 +31,6 @@ import { IpykernelCheckResult, isUsingIpykernel6OrLater } from '../../notebooks/ /** * The DebuggingManager maintains the mapping between notebook documents and debug sessions. */ -@injectable() export abstract class DebuggingManagerBase implements IDisposable { private notebookToDebugger = new Map(); protected notebookToDebugAdapter = new Map(); From 2d7f073ba022c53325f4b46129fb4fedd368c80b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 12:25:34 +1000 Subject: [PATCH 07/15] Misc --- src/kernels/debugger/debuggingManagerBase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/kernels/debugger/debuggingManagerBase.ts b/src/kernels/debugger/debuggingManagerBase.ts index 4f926f13b2d..6e8452d045d 100644 --- a/src/kernels/debugger/debuggingManagerBase.ts +++ b/src/kernels/debugger/debuggingManagerBase.ts @@ -3,7 +3,6 @@ 'use strict'; -import { injectable } from 'inversify'; import { debug, NotebookDocument, From 25edd118a1c44f08961db7f4f19a5bbfe518fbee Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 13:13:20 +1000 Subject: [PATCH 08/15] Oops --- src/platform/serviceRegistry.web.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/platform/serviceRegistry.web.ts b/src/platform/serviceRegistry.web.ts index bb422d02db3..3f87085f009 100644 --- a/src/platform/serviceRegistry.web.ts +++ b/src/platform/serviceRegistry.web.ts @@ -45,7 +45,7 @@ import { DataViewerFactory } from '../webviews/extension-side/dataviewer/dataVie import { IDataViewerFactory } from '../webviews/extension-side/dataviewer/types'; import { INotebookWatcher } from '../webviews/extension-side/variablesView/types'; import { DebuggingManager } from '../notebooks/debugger/debuggingManager'; -import { IDebuggingManager } from '../kernels/debugger/types'; +import { IDebuggingManager, IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; import { InteractiveWindowDebuggingManager } from '../interactive-window/debugger/jupyter/debuggingManager'; export function registerTypes(context: IExtensionContext, serviceManager: IServiceManager, isDevMode: boolean) { @@ -82,7 +82,10 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IDebuggingManager, DebuggingManager, undefined, [ IExtensionSingleActivationService ]); - serviceManager.addSingleton(IDebuggingManager, InteractiveWindowDebuggingManager, undefined, [ - IExtensionSingleActivationService - ]); + serviceManager.addSingleton( + IInteractiveWindowDebuggingManager, + InteractiveWindowDebuggingManager, + undefined, + [IExtensionSingleActivationService] + ); } From f45be77df0404f15c652e95ddf1f6646a682e949 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 13:29:29 +1000 Subject: [PATCH 09/15] Misc --- src/platform/serviceRegistry.web.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/platform/serviceRegistry.web.ts b/src/platform/serviceRegistry.web.ts index 3f87085f009..851baecae25 100644 --- a/src/platform/serviceRegistry.web.ts +++ b/src/platform/serviceRegistry.web.ts @@ -45,8 +45,9 @@ import { DataViewerFactory } from '../webviews/extension-side/dataviewer/dataVie import { IDataViewerFactory } from '../webviews/extension-side/dataviewer/types'; import { INotebookWatcher } from '../webviews/extension-side/variablesView/types'; import { DebuggingManager } from '../notebooks/debugger/debuggingManager'; -import { IDebuggingManager, IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; +import { IDebuggingManager, IDebugLocationTracker, IDebugLocationTrackerFactory, IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; import { InteractiveWindowDebuggingManager } from '../interactive-window/debugger/jupyter/debuggingManager'; +import { DebugLocationTrackerFactory } from '../kernels/debugger/debugLocationTrackerFactory'; export function registerTypes(context: IExtensionContext, serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton(IFileSystem, FileSystem); @@ -70,6 +71,9 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(INbConvertExport, ExportToPDF, ExportFormat.pdf); serviceManager.addSingleton(INbConvertExport, ExportToPython, ExportFormat.python); serviceManager.addSingleton(ExportUtilBase, ExportUtilBase); + serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory, undefined, [ + IDebugLocationTrackerFactory + ]); registerCommonTypes(serviceManager); registerApiTypes(serviceManager); From 392f8e7b9b73ba6591de5be15ed8735d47294572 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 14:38:06 +1000 Subject: [PATCH 10/15] Misc --- .../debugger/kernelDebugAdapterBase.ts | 36 +----------------- src/notebooks/debugger/kernelDebugAdapter.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/kernels/debugger/kernelDebugAdapterBase.ts b/src/kernels/debugger/kernelDebugAdapterBase.ts index c403b1fdecb..a5e8ba729fe 100644 --- a/src/kernels/debugger/kernelDebugAdapterBase.ts +++ b/src/kernels/debugger/kernelDebugAdapterBase.ts @@ -21,7 +21,6 @@ import { workspace } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; -import { executeSilently } from '../helpers'; import { IJupyterSession, IKernel } from '../types'; import { IPlatformService } from '../../platform/common/platform/types'; import { DebuggingTelemetry } from './constants'; @@ -75,7 +74,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb constructor( protected session: DebugSession, protected notebookDocument: NotebookDocument, - private readonly jupyterSession: IJupyterSession, + protected readonly jupyterSession: IJupyterSession, private readonly kernel: IKernel | undefined, private readonly platformService: IPlatformService ) { @@ -210,10 +209,6 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb } dispose() { - // On dispose, delete our temp cell files - this.deleteDumpCells().catch(() => { - traceError('Error deleting temporary debug files.'); - }); this.disposables.forEach((d) => d.dispose()); } @@ -264,35 +259,6 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb return undefined; } - // Use our jupyter session to delete all the cells - private async deleteDumpCells() { - const fileValues = [...this.cellToFile.values()]; - // Need to have our Jupyter Session and some dumpCell files to delete - if (this.jupyterSession && fileValues.length) { - // Create our python string of file names - const fileListString = fileValues - .map((filePath) => { - return '"' + filePath.path + '"'; - }) - .join(','); - - // Insert into our delete snippet - const deleteFilesCode = `import os -_VSCODE_fileList = [${fileListString}] -for file in _VSCODE_fileList: - try: - os.remove(file) - except: - pass -del _VSCODE_fileList`; - - return executeSilently(this.jupyterSession, deleteFilesCode, { - traceErrors: true, - traceErrorsMessage: 'Error deleting temporary debugging files' - }); - } - } - private async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) { if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') { traceInfo(`Skipping sending message ${message.type} because session is disposed`); diff --git a/src/notebooks/debugger/kernelDebugAdapter.ts b/src/notebooks/debugger/kernelDebugAdapter.ts index 184c05a584a..84862d063a4 100644 --- a/src/notebooks/debugger/kernelDebugAdapter.ts +++ b/src/notebooks/debugger/kernelDebugAdapter.ts @@ -8,6 +8,7 @@ import { NotebookCellKind } from 'vscode'; import { IDumpCellResponse } from '../../kernels/debugger/types'; import { traceError } from '../../platform/logging'; import { KernelDebugAdapterBase } from '../../kernels/debugger/kernelDebugAdapterBase'; +import { executeSilently } from '../../kernels/helpers'; export class KernelDebugAdapter extends KernelDebugAdapterBase { public override async dumpAllCells() { @@ -19,6 +20,14 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { }) ); } + public override dispose() { + super.dispose(); + // On dispose, delete our temp cell files + this.deleteDumpCells().catch(() => { + traceError('Error deleting temporary debug files.'); + }); + } + // Dump content of given cell into a tmp file and return path to file. protected override async dumpCell(index: number): Promise { const cell = this.notebookDocument.cellAt(index); @@ -37,4 +46,33 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase { traceError(err); } } + + // Use our jupyter session to delete all the cells + private async deleteDumpCells() { + const fileValues = [...this.cellToFile.values()]; + // Need to have our Jupyter Session and some dumpCell files to delete + if (this.jupyterSession && fileValues.length) { + // Create our python string of file names + const fileListString = fileValues + .map((filePath) => { + return '"' + filePath.path + '"'; + }) + .join(','); + + // Insert into our delete snippet + const deleteFilesCode = `import os +_VSCODE_fileList = [${fileListString}] +for file in _VSCODE_fileList: + try: + os.remove(file) + except: + pass +del _VSCODE_fileList`; + + return executeSilently(this.jupyterSession, deleteFilesCode, { + traceErrors: true, + traceErrorsMessage: 'Error deleting temporary debugging files' + }); + } + } } From 6e9e71a5fb25d34b3b713a76415b12dab2928b1a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 15:16:32 +1000 Subject: [PATCH 11/15] Misc --- src/notebooks/serviceRegistry.node.ts | 1 - src/notebooks/serviceRegistry.web.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index 8328fe98556..2c8c77d5db9 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -70,6 +70,5 @@ export function registerTypes(serviceManager: IServiceManager) { IExtensionSyncActivationService, RemoteKernelControllerWatcher ); - serviceManager.addSingleton(ITracebackFormatter, InteractiveWindowTracebackFormatter); serviceManager.addSingleton(ITracebackFormatter, NotebookTracebackFormatter); } diff --git a/src/notebooks/serviceRegistry.web.ts b/src/notebooks/serviceRegistry.web.ts index 30d40450ca4..7197a2c55d4 100644 --- a/src/notebooks/serviceRegistry.web.ts +++ b/src/notebooks/serviceRegistry.web.ts @@ -40,6 +40,5 @@ export function registerTypes(serviceManager: IServiceManager) { IExtensionSyncActivationService, RemoteKernelControllerWatcher ); - serviceManager.addSingleton(ITracebackFormatter, InteractiveWindowTracebackFormatter); serviceManager.addSingleton(ITracebackFormatter, NotebookTracebackFormatter); } From b7ccf85461c74dd0bc742d843c4988130e4b15c8 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 16:28:44 +1000 Subject: [PATCH 12/15] oops --- src/notebooks/serviceRegistry.node.ts | 1 - src/notebooks/serviceRegistry.web.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index 2c8c77d5db9..3f7e860d1b1 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -25,7 +25,6 @@ import { CondaControllerRefresher } from './controllers/condaControllerRefresher import { IntellisenseProvider } from '../intellisense/intellisenseProvider.node'; import { RemoteKernelControllerWatcher } from './controllers/remoteKernelControllerWatcher'; import { ITracebackFormatter } from '../kernels/types'; -import { InteractiveWindowTracebackFormatter } from '../interactive-window/outputs/tracebackFormatter'; import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; export function registerTypes(serviceManager: IServiceManager) { diff --git a/src/notebooks/serviceRegistry.web.ts b/src/notebooks/serviceRegistry.web.ts index 7197a2c55d4..69daa91713c 100644 --- a/src/notebooks/serviceRegistry.web.ts +++ b/src/notebooks/serviceRegistry.web.ts @@ -16,7 +16,6 @@ import { NotebookUsageTracker } from './notebookUsageTracker'; import { NotebookEditorProvider } from './notebookEditorProvider'; import { RemoteKernelControllerWatcher } from './controllers/remoteKernelControllerWatcher'; import { ITracebackFormatter } from '../kernels/types'; -import { InteractiveWindowTracebackFormatter } from '../interactive-window/outputs/tracebackFormatter'; import { NotebookTracebackFormatter } from './outputs/tracebackFormatter'; export function registerTypes(serviceManager: IServiceManager) { From 4777b647051cd7c97f19e74aa1722a5b6f94a955 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 19:34:10 +1000 Subject: [PATCH 13/15] MIsc --- src/platform/serviceRegistry.web.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/serviceRegistry.web.ts b/src/platform/serviceRegistry.web.ts index 851baecae25..ffd3ed0cf19 100644 --- a/src/platform/serviceRegistry.web.ts +++ b/src/platform/serviceRegistry.web.ts @@ -45,7 +45,12 @@ import { DataViewerFactory } from '../webviews/extension-side/dataviewer/dataVie import { IDataViewerFactory } from '../webviews/extension-side/dataviewer/types'; import { INotebookWatcher } from '../webviews/extension-side/variablesView/types'; import { DebuggingManager } from '../notebooks/debugger/debuggingManager'; -import { IDebuggingManager, IDebugLocationTracker, IDebugLocationTrackerFactory, IInteractiveWindowDebuggingManager } from '../kernels/debugger/types'; +import { + IDebuggingManager, + IDebugLocationTracker, + IDebugLocationTrackerFactory, + IInteractiveWindowDebuggingManager +} from '../kernels/debugger/types'; import { InteractiveWindowDebuggingManager } from '../interactive-window/debugger/jupyter/debuggingManager'; import { DebugLocationTrackerFactory } from '../kernels/debugger/debugLocationTrackerFactory'; From 81797d846e5ab990efa02fe4f51d93cb273cef8a Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 19:35:14 +1000 Subject: [PATCH 14/15] oops --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0223cccb76d..e34326b368f 100644 --- a/package.json +++ b/package.json @@ -683,7 +683,7 @@ }, { "command": "jupyter.notebookeditor.addcellbelow", - "title": "%jupyter.command.jupyter.notebookeditor.Ķaddcellbelow.title%", + "title": "%jupyter.command.jupyter.notebookeditor.addcellbelow.title%", "category": "Notebook", "enablement": "!jupyter.webExtension" }, From 6ccc99d633268666f7debda9fa213eb7ee3f2d47 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 24 May 2022 19:36:06 +1000 Subject: [PATCH 15/15] oops --- src/interactive-window/editor-integration/codeGenerator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/interactive-window/editor-integration/codeGenerator.ts b/src/interactive-window/editor-integration/codeGenerator.ts index aa5c9524c7e..9a11e750af2 100644 --- a/src/interactive-window/editor-integration/codeGenerator.ts +++ b/src/interactive-window/editor-integration/codeGenerator.ts @@ -128,7 +128,6 @@ export class CodeGenerator implements IInteractiveWindowCodeGenerator { const realCode = doc.getText(new Range(new Position(cellLine, 0), endLine.rangeIncludingLineBreak.end)); const hashValue = hashjs.sha1().update(hashedCode).digest('hex').substring(0, 12); const runtimeFile = this.getRuntimeFile(hashValue, expectedCount); - console.log(firstExecutableLineIndex); const hash: IGeneratedCode = { line: line ? line.lineNumber + 1 : 1, endLine: endLine ? endLine.lineNumber + 1 : 1,