Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support debugging IW using Jupyter protocol #10105

Merged
merged 15 commits into from
May 24, 2022
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved a lot of stuff into a base class

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved a lot of stuff into a base class

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() {
DonJayamanne marked this conversation as resolved.
Show resolved Hide resolved
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