-
Notifications
You must be signed in to change notification settings - Fork 300
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support debugging IW using Jupyter protocol (#10105)
- Loading branch information
1 parent
760d61e
commit 191122c
Showing
25 changed files
with
1,181 additions
and
580 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
src/interactive-window/debugger/jupyter/debugCellControllers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void>(); | ||
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<boolean> { | ||
return false; | ||
} | ||
|
||
public async willSendRequest(request: DebugProtocol.Request): Promise<void> { | ||
const metadata = getInteractiveCellMetadata(this.debugCell); | ||
if (request.command === 'configurationDone' && metadata && metadata.generatedCode) { | ||
await cellDebugSetup(this.kernel, this.debugAdapter); | ||
|
||
const realPath = this.debugAdapter.getSourcePath(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 | ||
}, | ||
breakpoints: [initialBreakpoint], | ||
sourceModified: false | ||
}); | ||
} | ||
this._ready.resolve(); | ||
} | ||
} | ||
} |
184 changes: 184 additions & 0 deletions
184
src/interactive-window/debugger/jupyter/debuggingManager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> { | ||
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<void> => { | ||
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<DebugAdapterDescriptor | undefined> { | ||
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); | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// 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<KernelMessage.IDebugReplyMsg | undefined> { | ||
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); | ||
} | ||
}) | ||
); | ||
} | ||
// Dump content of given cell into a tmp file and return path to file. | ||
protected async dumpCell(index: number): Promise<void> { | ||
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.