Skip to content

Commit

Permalink
Fixes to IW debugging with breakpoints (#10263)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored Jun 3, 2022
1 parent fb90ab7 commit 6ec5855
Show file tree
Hide file tree
Showing 11 changed files with 1,369 additions and 1,233 deletions.
13 changes: 8 additions & 5 deletions src/interactive-window/debugger/jupyter/debugCellControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ export class DebugCellController implements IDebuggingDelegate {
public async willSendEvent(_msg: DebugProtocolMessage): Promise<boolean> {
return false;
}
private debugCellDumped?: Promise<void>;
public async willSendRequest(request: DebugProtocol.Request): Promise<void> {
const metadata = getInteractiveCellMetadata(this.debugCell);
if (request.command === 'setBreakpoints' && metadata && metadata.generatedCode && !this.cellDumpInvoked) {
this.cellDumpInvoked = true;
await cellDebugSetup(this.kernel, this.debugAdapter);
if (!this.debugCellDumped) {
this.debugCellDumped = cellDebugSetup(this.kernel, this.debugAdapter);
}
await this.debugCellDumped;
}
if (request.command === 'configurationDone' && metadata && metadata.generatedCode) {
if (!this.cellDumpInvoked) {
this.cellDumpInvoked = true;
await cellDebugSetup(this.kernel, this.debugAdapter);
if (!this.debugCellDumped) {
this.debugCellDumped = cellDebugSetup(this.kernel, this.debugAdapter);
}
await this.debugCellDumped;
this._ready.resolve();
}
}
Expand Down
151 changes: 139 additions & 12 deletions src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ 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 { traceError, traceInfo, traceInfoIfCI } from '../../../platform/logging';
import { getInteractiveCellMetadata } from '../../../interactive-window/helpers';
import { KernelDebugAdapterBase } from '../../../kernels/debugger/kernelDebugAdapterBase';
import { InteractiveCellMetadata } from '../../editor-integration/types';

export class KernelDebugAdapter extends KernelDebugAdapterBase {
private readonly debugLocationTracker?: DebugAdapterTracker;
private readonly cellToDebugFileSortedInReverseOrderByLineNumber: {
debugFilePath: string;
interactiveWindow: Uri;
lineOffset: number;
metadata: InteractiveCellMetadata;
}[] = [];

constructor(
session: DebugSession,
notebookDocument: NotebookDocument,
Expand Down Expand Up @@ -72,24 +80,143 @@ export class KernelDebugAdapter extends KernelDebugAdapterBase {
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 code = (metadata.generatedCode?.code || cell.document.getText()).replace(/\r\n/g, '\n');
const response = await this.session.customRequest('dumpCell', { code });

// We know jupyter will strip out leading white spaces, hence take that into account.
const lines = metadata.generatedCode!.realCode.splitLines({ trim: false, removeEmptyEntries: false });
const indexOfFirstNoneEmptyLine = lines.findIndex((line) => line.trim().length);
console.error(indexOfFirstNoneEmptyLine);
const norm = path.normalize((response as IDumpCellResponse).sourcePath);
this.fileToCell.set(norm, {
uri: Uri.parse(metadata.interactive.uristring),
lineOffset:
metadata.interactive.lineIndex +
(metadata.generatedCode?.lineOffsetRelativeToIndexOfFirstLineInCell || 0)
});
this.cellToFile.set(Uri.parse(metadata.interactive.uristring), {
path: norm,
this.fileToCell.set(norm, Uri.parse(metadata.interactive.uristring));

// If this cell doesn't have a cell marker, then
// Jupyter will strip out any leading whitespace.
// Take that into account.
let numberOfStrippedLines = 0;
if (metadata.generatedCode && !metadata.generatedCode.hasCellMarker) {
numberOfStrippedLines = metadata.generatedCode.firstNonBlankLineIndex;
}
this.cellToDebugFileSortedInReverseOrderByLineNumber.push({
debugFilePath: norm,
interactiveWindow: Uri.parse(metadata.interactive.uristring),
metadata,
lineOffset:
numberOfStrippedLines +
metadata.interactive.lineIndex +
(metadata.generatedCode?.lineOffsetRelativeToIndexOfFirstLineInCell || 0)
});
// Order cells in reverse order.
this.cellToDebugFileSortedInReverseOrderByLineNumber.sort(
(a, b) => b.metadata.interactive.lineIndex - a.metadata.interactive.lineIndex
);
} catch (err) {
traceError(`Failed to dump cell for ${cell.index} with code ${metadata.interactive.originalSource}`, err);
}
}
protected override translateDebuggerFileToRealFile(
source: DebugProtocol.Source | undefined,
lines?: { line?: number; endLine?: number; lines?: number[] }
) {
if (!source || !source.path || !lines || (typeof lines.line !== 'number' && !Array.isArray(lines.lines))) {
return;
}
// Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file.
const cell = this.cellToDebugFileSortedInReverseOrderByLineNumber.find(
(item) => item.debugFilePath === source.path
);
if (!cell) {
return;
}
source.name = path.basename(cell.interactiveWindow.path);
source.path = cell.interactiveWindow.toString();
if (typeof lines?.endLine === 'number') {
lines.endLine = lines.endLine + (cell.lineOffset || 0);
}
if (typeof lines?.line === 'number') {
lines.line = lines.line + (cell.lineOffset || 0);
}
if (lines?.lines && Array.isArray(lines?.lines)) {
lines.lines = lines?.lines.map((line) => line + (cell.lineOffset || 0));
}
}
protected override translateRealFileToDebuggerFile(
source: DebugProtocol.Source | undefined,
lines?: { line?: number; endLine?: number; lines?: number[] }
) {
if (!source || !source.path || !lines || (typeof lines.line !== 'number' && !Array.isArray(lines.lines))) {
return;
}
const startLine = lines.line || lines.lines![0];
// Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file.
const cell = this.cellToDebugFileSortedInReverseOrderByLineNumber.find(
(item) => startLine >= item.metadata.interactive.lineIndex + 1
);
if (!cell) {
return;
}
source.path = cell.debugFilePath;
if (typeof lines?.endLine === 'number') {
lines.endLine = lines.endLine - (cell.lineOffset || 0);
}
if (typeof lines?.line === 'number') {
lines.line = lines.line - (cell.lineOffset || 0);
}
if (lines?.lines && Array.isArray(lines?.lines)) {
lines.lines = lines?.lines.map((line) => line - (cell.lineOffset || 0));
}
}

protected override async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) {
if (this.jupyterSession.disposed || this.jupyterSession.status === 'dead') {
traceInfo(`Skipping sending message ${message.type} because session is disposed`);
return;
}

const request = message as unknown as DebugProtocol.SetBreakpointsRequest;
if (request.type === 'request' && request.command === 'setBreakpoints') {
const sortedLines = (request.arguments.lines || []).concat(
(request.arguments.breakpoints || []).map((bp) => bp.line)
);
const startLine = sortedLines.length ? sortedLines[0] : undefined;
// Find the cell that matches this line in the IW file by mapping the debugFilePath to the IW file.
const cell = startLine
? this.cellToDebugFileSortedInReverseOrderByLineNumber.find(
(item) => startLine >= item.metadata.interactive.lineIndex + 1
)
: undefined;
if (cell) {
const clonedRequest: typeof request = JSON.parse(JSON.stringify(request));
if (request.arguments.lines) {
request.arguments.lines = request.arguments.lines.filter(
(line) => line <= cell.metadata.generatedCode!.endLine
);
}
if (request.arguments.breakpoints) {
request.arguments.breakpoints = request.arguments.breakpoints.filter(
(bp) => bp.line <= cell.metadata.generatedCode!.endLine
);
}
if (sortedLines.filter((line) => line > cell.metadata.generatedCode!.endLine).length) {
// Find all the lines that don't belong to this cell & add breakpoints for those as well
// However do that separately as they belong to different files.
await this.setBreakpoints({
source: clonedRequest.arguments.source,
breakpoints: clonedRequest.arguments.breakpoints?.filter(
(bp) => bp.line > cell.metadata.generatedCode!.endLine
),
lines: clonedRequest.arguments.lines?.filter(
(line) => line > cell.metadata.generatedCode!.endLine
)
});
}
}
}

return super.sendRequestToJupyterSession(message);
}

protected getDumpFilesForDeletion() {
return this.cellToDebugFileSortedInReverseOrderByLineNumber.map((item) => item.debugFilePath);
}
}
108 changes: 53 additions & 55 deletions src/kernels/debugger/kernelDebugAdapterBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,23 @@ import {
IDebugInfoResponse
} from './types';
import { sendTelemetryEvent } from '../../telemetry';
import { traceError, traceInfo, traceInfoIfCI, traceVerbose } from '../../platform/logging';
import { traceError, traceInfo, traceInfoIfCI, traceVerbose, traceWarning } from '../../platform/logging';
import {
assertIsDebugConfig,
isShortNamePath,
shortNameMatchesLongName,
getMessageSourceAndHookIt
} from '../../notebooks/debugger/helper';
import { ResourceMap } from '../../platform/vscode-path/map';
import { IDisposable } from '../../platform/common/types';
import { executeSilently } from '../helpers';

/**
* 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 ResourceMap<{
path: string;
lineOffset?: number;
}>();
protected readonly fileToCell = new Map<string, Uri>();
private readonly sendMessage = new EventEmitter<DebugProtocolMessage>();
private readonly endSession = new EventEmitter<DebugSession>();
private readonly configuration: IKernelDebugAdapterConfig;
Expand Down Expand Up @@ -210,6 +200,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb
}

dispose() {
this.deleteDumpedFiles().catch((ex) => traceWarning('Error deleting temporary debug files.', ex));
this.disposables.forEach((d) => d.dispose());
}

Expand All @@ -233,9 +224,6 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb
);
}
protected abstract dumpCell(index: number): Promise<void>;
public getSourcePath(filePath: Uri) {
return this.cellToFile.get(filePath)?.path;
}

private async debugInfo(): Promise<void> {
const response = await this.session.customRequest('debugInfo');
Expand Down Expand Up @@ -268,29 +256,13 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb
return undefined;
}

private async sendRequestToJupyterSession(message: DebugProtocol.ProtocolMessage) {
protected 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(Uri.parse(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));
}
}
}
});
getMessageSourceAndHookIt(message, this.translateRealFileToDebuggerFile.bind(this));

this.trace('to kernel', JSON.stringify(message));
if (message.type === 'request') {
Expand All @@ -307,27 +279,7 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb

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));
}
}
}
}
);
getMessageSourceAndHookIt(message, this.translateDebuggerFileToRealFile.bind(this));

this.trace('response', JSON.stringify(message));
this.sendMessage.fire(message);
Expand All @@ -350,4 +302,50 @@ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDeb
traceError(`Unknown message type to send ${message.type}`);
}
}
protected translateDebuggerFileToRealFile(
source: DebugProtocol.Source | undefined,
_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.path);
source.path = mapping.toString();
}
}
}
protected abstract translateRealFileToDebuggerFile(
source: DebugProtocol.Source | undefined,
_lines?: { line?: number; endLine?: number; lines?: number[] }
): void;

protected abstract getDumpFilesForDeletion(): string[];
private async deleteDumpedFiles() {
const fileValues = this.getDumpFilesForDeletion();
// 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) => {
// escape Windows path separators again for python
return '"' + filePath.replace(/\\/g, '\\\\') + '"';
})
.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'
});
}
}
}
4 changes: 1 addition & 3 deletions src/kernels/debugger/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
Event,
NotebookCell,
NotebookDocument,
NotebookEditor,
Uri
NotebookEditor
} from 'vscode';
import { IFileGeneratedCodes } from '../../interactive-window/editor-integration/types';

Expand Down Expand Up @@ -72,7 +71,6 @@ export interface IKernelDebugAdapter extends DebugAdapter {
onDidEndSession: Event<DebugSession>;
dumpAllCells(): Promise<void>;
getConfiguration(): IKernelDebugAdapterConfig;
getSourcePath(filePath: Uri): string | undefined;
}

export const IDebuggingManager = Symbol('IDebuggingManager');
Expand Down
Loading

0 comments on commit 6ec5855

Please sign in to comment.