Skip to content

Commit

Permalink
Support debugging IW using Jupyter protocol (#10105)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored May 24, 2022
1 parent 760d61e commit 191122c
Show file tree
Hide file tree
Showing 25 changed files with 1,181 additions and 580 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -2069,6 +2073,7 @@
},
"enabledApiProposals": [
"notebookControllerKind",
"notebookWorkspaceEdit",
"notebookDebugOptions",
"notebookDeprecated",
"notebookEditor",
Expand Down
53 changes: 53 additions & 0 deletions src/interactive-window/debugger/jupyter/debugCellControllers.ts
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 src/interactive-window/debugger/jupyter/debuggingManager.ts
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 src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts
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);
}
}
}
Loading

0 comments on commit 191122c

Please sign in to comment.