UNPKG

@jupyterlab/debugger

Version:
863 lines 30.4 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { nullTranslator } from '@jupyterlab/translation'; import { Signal } from '@lumino/signaling'; import { Debugger } from './debugger'; /** * A concrete implementation of the IDebugger interface. */ export class DebuggerService { /** * Instantiate a new DebuggerService. * * @param options The instantiation options for a DebuggerService. */ constructor(options) { var _a, _b; this._eventMessage = new Signal(this); this._isDisposed = false; this._sessionChanged = new Signal(this); this._pauseOnExceptionChanged = new Signal(this); this._config = options.config; // Avoids setting session with invalid client // session should be set only when a notebook or // a console get the focus. // TODO: also checks that the notebook or console // runs a kernel with debugging ability this._session = null; this._specsManager = (_a = options.specsManager) !== null && _a !== void 0 ? _a : null; this._model = new Debugger.Model(); this._debuggerSources = (_b = options.debuggerSources) !== null && _b !== void 0 ? _b : null; this._trans = (options.translator || nullTranslator).load('jupyterlab'); } /** * Signal emitted for debug event messages. */ get eventMessage() { return this._eventMessage; } /** * Get debugger config. */ get config() { return this._config; } /** * Whether the debug service is disposed. */ get isDisposed() { return this._isDisposed; } /** * Whether the current debugger is started. */ get isStarted() { var _a, _b; return (_b = (_a = this._session) === null || _a === void 0 ? void 0 : _a.isStarted) !== null && _b !== void 0 ? _b : false; } /** * A signal emitted when the pause on exception filter changes. */ get pauseOnExceptionChanged() { return this._pauseOnExceptionChanged; } /** * Returns the debugger service's model. */ get model() { return this._model; } /** * Returns the current debug session. */ get session() { return this._session; } /** * Sets the current debug session to the given parameter. * * @param session - the new debugger session. */ set session(session) { var _a; if (this._session === session) { return; } if (this._session) { this._session.dispose(); } this._session = session; (_a = this._session) === null || _a === void 0 ? void 0 : _a.eventMessage.connect((_, event) => { if (event.event === 'stopped') { this._model.stoppedThreads.add(event.body.threadId); void this._getAllFrames(); } else if (event.event === 'continued') { this._model.stoppedThreads.delete(event.body.threadId); this._clearModel(); this._clearSignals(); } this._eventMessage.emit(event); }); this._sessionChanged.emit(session); } /** * Signal emitted upon session changed. */ get sessionChanged() { return this._sessionChanged; } /** * Dispose the debug service. */ dispose() { if (this.isDisposed) { return; } this._isDisposed = true; Signal.clearData(this); } /** * Computes an id based on the given code. * * @param code The source code. */ getCodeId(code) { var _a, _b, _c, _d; try { return this._config.getCodeId(code, (_d = (_c = (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : ''); } catch (_e) { return ''; } } /** * Whether there exists a thread in stopped state. */ hasStoppedThreads() { var _a, _b; return (_b = ((_a = this._model) === null || _a === void 0 ? void 0 : _a.stoppedThreads.size) > 0) !== null && _b !== void 0 ? _b : false; } /** * Request whether debugging is available for the session connection. * * @param connection The session connection. */ async isAvailable(connection) { var _a, _b, _c, _d; if (!this._specsManager) { return true; } await this._specsManager.ready; const kernel = connection === null || connection === void 0 ? void 0 : connection.kernel; if (!kernel) { return false; } const name = kernel.name; if (!((_a = this._specsManager.specs) === null || _a === void 0 ? void 0 : _a.kernelspecs[name])) { return true; } return !!((_d = (_c = (_b = this._specsManager.specs.kernelspecs[name]) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c['debugger']) !== null && _d !== void 0 ? _d : false); } /** * Clear all the breakpoints for the current session. */ async clearBreakpoints() { var _a; if (((_a = this.session) === null || _a === void 0 ? void 0 : _a.isStarted) !== true) { return; } this._model.breakpoints.breakpoints.forEach((_, path, map) => { void this._setBreakpoints([], path); }); let bpMap = new Map(); this._model.breakpoints.restoreBreakpoints(bpMap); } /** * Continues the execution of the current thread. */ async continue() { try { if (!this.session) { throw new Error('No active debugger session'); } await this.session.sendRequest('continue', { threadId: this._currentThread() }); this._model.stoppedThreads.delete(this._currentThread()); this._clearModel(); this._clearSignals(); } catch (err) { console.error('Error:', err.message); } } /** * Retrieve the content of a source file. * * @param source The source object containing the path to the file. */ async getSource(source) { var _a, _b; if (!this.session) { throw new Error('No active debugger session'); } const reply = await this.session.sendRequest('source', { source, sourceReference: (_a = source.sourceReference) !== null && _a !== void 0 ? _a : 0 }); return { ...reply.body, path: (_b = source.path) !== null && _b !== void 0 ? _b : '' }; } /** * Evaluate an expression. * * @param expression The expression to evaluate as a string. */ async evaluate(expression) { var _a; if (!this.session) { throw new Error('No active debugger session'); } const frameId = (_a = this.model.callstack.frame) === null || _a === void 0 ? void 0 : _a.id; const reply = await this.session.sendRequest('evaluate', { context: 'repl', expression, frameId }); if (!reply.success) { return null; } // get the frames to retrieve the latest state of the variables this._clearModel(); await this._getAllFrames(); return reply.body; } /** * Makes the current thread run again for one step. */ async next() { try { if (!this.session) { throw new Error('No active debugger session'); } await this.session.sendRequest('next', { threadId: this._currentThread() }); } catch (err) { console.error('Error:', err.message); } } /** * Request rich representation of a variable. * * @param variableName The variable name to request * @param frameId The current frame id in which to request the variable * @returns The mime renderer data model */ async inspectRichVariable(variableName, frameId) { if (!this.session) { throw new Error('No active debugger session'); } const reply = await this.session.sendRequest('richInspectVariables', { variableName, frameId }); if (reply.success) { return reply.body; } else { throw new Error(reply.message); } } /** * Request variables for a given variable reference. * * @param variablesReference The variable reference to request. */ async inspectVariable(variablesReference) { if (!this.session) { throw new Error('No active debugger session'); } const reply = await this.session.sendRequest('variables', { variablesReference }); if (reply.success) { return reply.body.variables; } else { throw new Error(reply.message); } } /** * Request to set a variable in the global scope. * * @param name The name of the variable. */ async copyToGlobals(name) { if (!this.session) { throw new Error('No active debugger session'); } if (!this.model.supportCopyToGlobals) { throw new Error('The "copyToGlobals" request is not supported by the kernel'); } const frames = this.model.callstack.frames; this.session .sendRequest('copyToGlobals', { srcVariableName: name, dstVariableName: name, srcFrameId: frames[0].id }) .then(async () => { const scopes = await this._getScopes(frames[0]); const variables = await Promise.all(scopes.map(scope => this._getVariables(scope))); const variableScopes = this._convertScopes(scopes, variables); this._model.variables.scopes = variableScopes; }) .catch(reason => { console.error(reason); }); } /** * Requests all the defined variables and display them in the * table view. */ async displayDefinedVariables() { if (!this.session) { throw new Error('No active debugger session'); } const inspectReply = await this.session.sendRequest('inspectVariables', {}); const variables = inspectReply.body.variables; const variableScopes = [ { name: this._trans.__('Globals'), variables: variables } ]; this._model.variables.scopes = variableScopes; } async displayModules() { if (!this.session) { throw new Error('No active debugger session'); } const modules = await this.session.sendRequest('modules', {}); this._model.kernelSources.kernelSources = modules.body.modules.map(module => { return { name: module.name, path: module.path }; }); } /** * Restart the debugger. */ async restart() { const { breakpoints } = this._model.breakpoints; await this.stop(); await this.start(); await this._restoreBreakpoints(breakpoints); } /** * Restore the state of a debug session. * * @param autoStart - If true, starts the debugger if it has not been started. */ async restoreState(autoStart) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; if (!this.model || !this.session) { return; } const reply = await this.session.restoreState(); const { body } = reply; const breakpoints = this._mapBreakpoints(body.breakpoints); const stoppedThreads = new Set(body.stoppedThreads); this._model.hasRichVariableRendering = body.richRendering === true; this._model.supportCopyToGlobals = body.copyToGlobals === true; this._config.setHashParams({ kernel: (_d = (_c = (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '', method: body.hashMethod, seed: body.hashSeed }); this._config.setTmpFileParams({ kernel: (_h = (_g = (_f = (_e = this.session) === null || _e === void 0 ? void 0 : _e.connection) === null || _f === void 0 ? void 0 : _f.kernel) === null || _g === void 0 ? void 0 : _g.name) !== null && _h !== void 0 ? _h : '', prefix: body.tmpFilePrefix, suffix: body.tmpFileSuffix }); this._model.stoppedThreads = stoppedThreads; if (!this.isStarted && (autoStart || stoppedThreads.size !== 0)) { await this.start(); } if (this.isStarted || autoStart) { this._model.title = this.isStarted ? ((_k = (_j = this.session) === null || _j === void 0 ? void 0 : _j.connection) === null || _k === void 0 ? void 0 : _k.name) || '-' : '-'; } if (this._debuggerSources) { const filtered = this._filterBreakpoints(breakpoints); this._model.breakpoints.restoreBreakpoints(filtered); } else { this._model.breakpoints.restoreBreakpoints(breakpoints); } if (stoppedThreads.size !== 0) { await this._getAllFrames(); } else if (this.isStarted) { this._clearModel(); this._clearSignals(); } // Send the currentExceptionFilters to debugger. if (this.session.currentExceptionFilters) { await this.pauseOnExceptions(this.session.currentExceptionFilters); } } /** * Starts a debugger. * Precondition: !isStarted */ start() { if (!this.session) { throw new Error('No active debugger session'); } return this.session.start(); } /** * Makes the current thread pause if possible. */ async pause() { try { if (!this.session) { throw new Error('No active debugger session'); } await this.session.sendRequest('pause', { threadId: this._currentThread() }); } catch (err) { console.error('Error:', err.message); } } /** * Makes the current thread step in a function / method if possible. */ async stepIn() { try { if (!this.session) { throw new Error('No active debugger session'); } await this.session.sendRequest('stepIn', { threadId: this._currentThread() }); } catch (err) { console.error('Error:', err.message); } } /** * Makes the current thread step out a function / method if possible. */ async stepOut() { try { if (!this.session) { throw new Error('No active debugger session'); } await this.session.sendRequest('stepOut', { threadId: this._currentThread() }); } catch (err) { console.error('Error:', err.message); } } /** * Stops the debugger. * Precondition: isStarted */ async stop() { if (!this.session) { throw new Error('No active debugger session'); } await this.session.stop(); if (this._model) { this._model.clear(); } } /** * Update all breakpoints at once. * * @param code - The code in the cell where the breakpoints are set. * @param breakpoints - The list of breakpoints to set. * @param path - Optional path to the file where to set the breakpoints. */ async updateBreakpoints(code, breakpoints, path) { var _a; if (!((_a = this.session) === null || _a === void 0 ? void 0 : _a.isStarted)) { return; } if (!path) { path = (await this._dumpCell(code)).body.sourcePath; } const state = await this.session.restoreState(); const localBreakpoints = breakpoints .filter(({ line }) => typeof line === 'number') .map(({ line }) => ({ line: line })); const remoteBreakpoints = this._mapBreakpoints(state.body.breakpoints); // Set the local copy of breakpoints to reflect only editors that exist. if (this._debuggerSources) { const filtered = this._filterBreakpoints(remoteBreakpoints); this._model.breakpoints.restoreBreakpoints(filtered); } else { this._model.breakpoints.restoreBreakpoints(remoteBreakpoints); } // Removes duplicated breakpoints. It is better to do it here than // in the editor, because the kernel can change the line of a // breakpoint (when you attempt to set a breakpoint on an empty // line for instance). let addedLines = new Set(); // Set the kernel's breakpoints for this path. const reply = await this._setBreakpoints(localBreakpoints, path); const updatedBreakpoints = reply.body.breakpoints.filter((val, _, arr) => { const cond1 = arr.findIndex(el => el.line === val.line) > -1; const cond2 = !addedLines.has(val.line); addedLines.add(val.line); return cond1 && cond2; }); // Update the local model and finish kernel configuration. this._model.breakpoints.setBreakpoints(path, updatedBreakpoints); await this.session.sendRequest('configurationDone', {}); } /** * Determines if pausing on exceptions is supported by the kernel */ pauseOnExceptionsIsValid() { var _a, _b; if (this.isStarted) { if (((_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.exceptionBreakpointFilters) === null || _b === void 0 ? void 0 : _b.length) !== 0) { return true; } } return false; } /** * Add or remove a filter from the current used filters. * * @param exceptionFilter - The filter to add or remove from current filters. */ async pauseOnExceptionsFilter(exceptionFilter) { var _a; if (!((_a = this.session) === null || _a === void 0 ? void 0 : _a.isStarted)) { return; } let exceptionFilters = this.session.currentExceptionFilters; if (this.session.isPausingOnException(exceptionFilter)) { const index = exceptionFilters.indexOf(exceptionFilter); exceptionFilters.splice(index, 1); } else { exceptionFilters === null || exceptionFilters === void 0 ? void 0 : exceptionFilters.push(exceptionFilter); } await this.pauseOnExceptions(exceptionFilters); } /** * Enable or disable pausing on exceptions. * * @param exceptionFilters - The filters to use for the current debugging session. */ async pauseOnExceptions(exceptionFilters) { var _a, _b; if (!((_a = this.session) === null || _a === void 0 ? void 0 : _a.isStarted)) { return; } const exceptionBreakpointFilters = ((_b = this.session.exceptionBreakpointFilters) === null || _b === void 0 ? void 0 : _b.map(e => e.filter)) || []; let options = { filters: [] }; exceptionFilters.forEach(filter => { if (exceptionBreakpointFilters.includes(filter)) { options.filters.push(filter); } }); this.session.currentExceptionFilters = options.filters; await this.session.sendRequest('setExceptionBreakpoints', options); this._pauseOnExceptionChanged.emit(); } /** * Get the debugger state * * @returns Debugger state */ getDebuggerState() { var _a, _b, _c, _d, _e, _f, _g; const breakpoints = this._model.breakpoints.breakpoints; let cells = []; if (this._debuggerSources) { for (const id of breakpoints.keys()) { const editorList = this._debuggerSources.find({ focus: false, kernel: (_d = (_c = (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '', path: (_g = (_f = (_e = this._session) === null || _e === void 0 ? void 0 : _e.connection) === null || _f === void 0 ? void 0 : _f.path) !== null && _g !== void 0 ? _g : '', source: id }); const tmpCells = editorList.map(e => e.src.getSource()); cells = cells.concat(tmpCells); } } return { cells, breakpoints }; } /** * Restore the debugger state * * @param state Debugger state * @returns Whether the state has been restored successfully or not */ async restoreDebuggerState(state) { var _a, _b, _c, _d; await this.start(); for (const cell of state.cells) { await this._dumpCell(cell); } const breakpoints = new Map(); const kernel = (_d = (_c = (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : ''; const { prefix, suffix } = this._config.getTmpFileParams(kernel); for (const item of state.breakpoints) { const [id, list] = item; const unsuffixedId = id.substr(0, id.length - suffix.length); const codeHash = unsuffixedId.substr(unsuffixedId.lastIndexOf('/') + 1); const newId = prefix.concat(codeHash).concat(suffix); breakpoints.set(newId, list); } await this._restoreBreakpoints(breakpoints); const config = await this.session.sendRequest('configurationDone', {}); await this.restoreState(false); return config.success; } /** * Clear the current model. */ _clearModel() { this._model.callstack.frames = []; this._model.variables.scopes = []; } /** * Clear the signals set on the model. */ _clearSignals() { this._model.callstack.currentFrameChanged.disconnect(this._onCurrentFrameChanged, this); this._model.variables.variableExpanded.disconnect(this._onVariableExpanded, this); } /** * Map a list of scopes to a list of variables. * * @param scopes The list of scopes. * @param variables The list of variables. */ _convertScopes(scopes, variables) { if (!variables || !scopes) { return []; } return scopes.map((scope, i) => { return { name: scope.name, variables: variables[i].map(variable => { return { ...variable }; }) }; }); } /** * Get the current thread from the model. */ _currentThread() { // TODO: ask the model for the current thread ID return 1; } /** * Dump the content of a cell. * * @param code The source code to dump. */ async _dumpCell(code) { if (!this.session) { throw new Error('No active debugger session'); } return this.session.sendRequest('dumpCell', { code }); } /** * Filter breakpoints and only return those associated with a known editor. * * @param breakpoints - Map of breakpoints. * */ _filterBreakpoints(breakpoints) { if (!this._debuggerSources) { return breakpoints; } let bpMapForRestore = new Map(); for (const collection of breakpoints) { const [id, list] = collection; list.forEach(() => { var _a, _b, _c, _d, _e, _f, _g; this._debuggerSources.find({ focus: false, kernel: (_d = (_c = (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.kernel) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '', path: (_g = (_f = (_e = this._session) === null || _e === void 0 ? void 0 : _e.connection) === null || _f === void 0 ? void 0 : _f.path) !== null && _g !== void 0 ? _g : '', source: id }).forEach(() => { if (list.length > 0) { bpMapForRestore.set(id, list); } }); }); } return bpMapForRestore; } /** * Get all the frames from the kernel. */ async _getAllFrames() { this._model.callstack.currentFrameChanged.connect(this._onCurrentFrameChanged, this); this._model.variables.variableExpanded.connect(this._onVariableExpanded, this); const stackFrames = await this._getFrames(this._currentThread()); this._model.callstack.frames = stackFrames; } /** * Get all the frames for the given thread id. * * @param threadId The thread id. */ async _getFrames(threadId) { if (!this.session) { throw new Error('No active debugger session'); } const reply = await this.session.sendRequest('stackTrace', { threadId }); const stackFrames = reply.body.stackFrames; return stackFrames; } /** * Get all the scopes for the given frame. * * @param frame The frame. */ async _getScopes(frame) { if (!this.session) { throw new Error('No active debugger session'); } if (!frame) { return []; } const reply = await this.session.sendRequest('scopes', { frameId: frame.id }); return reply.body.scopes; } /** * Get the variables for a given scope. * * @param scope The scope to get variables for. */ async _getVariables(scope) { if (!this.session) { throw new Error('No active debugger session'); } if (!scope) { return []; } const reply = await this.session.sendRequest('variables', { variablesReference: scope.variablesReference }); return reply.body.variables; } /** * Process the list of breakpoints from the server and return as a map. * * @param breakpoints - The list of breakpoints from the kernel. * */ _mapBreakpoints(breakpoints) { if (!breakpoints.length) { return new Map(); } return breakpoints.reduce((map, val) => { const { breakpoints, source } = val; map.set(source, breakpoints.map(point => ({ ...point, source: { path: source }, verified: true }))); return map; }, new Map()); } /** * Handle a change of the current active frame. * * @param _ The callstack model * @param frame The frame. */ async _onCurrentFrameChanged(_, frame) { if (!frame) { return; } const scopes = await this._getScopes(frame); const variables = await Promise.all(scopes.map(scope => this._getVariables(scope))); const variableScopes = this._convertScopes(scopes, variables); this._model.variables.scopes = variableScopes; } /** * Handle a variable expanded event and request variables from the kernel. * * @param _ The variables model. * @param variable The expanded variable. */ async _onVariableExpanded(_, variable) { if (!this.session) { throw new Error('No active debugger session'); } const reply = await this.session.sendRequest('variables', { variablesReference: variable.variablesReference }); let newVariable = { ...variable, expanded: true }; reply.body.variables.forEach((variable) => { newVariable = { [variable.name]: variable, ...newVariable }; }); const newScopes = this._model.variables.scopes.map(scope => { const findIndex = scope.variables.findIndex(ele => ele.variablesReference === variable.variablesReference); scope.variables[findIndex] = newVariable; return { ...scope }; }); this._model.variables.scopes = [...newScopes]; return reply.body.variables; } /** * Set the breakpoints for a given file. * * @param breakpoints The list of breakpoints to set. * @param path The path to where to set the breakpoints. */ async _setBreakpoints(breakpoints, path) { if (!this.session) { throw new Error('No active debugger session'); } return await this.session.sendRequest('setBreakpoints', { breakpoints: breakpoints, source: { path }, sourceModified: false }); } /** * Re-send the breakpoints to the kernel and update the model. * * @param breakpoints The map of breakpoints to send */ async _restoreBreakpoints(breakpoints) { for (const [source, points] of breakpoints) { await this._setBreakpoints(points .filter(({ line }) => typeof line === 'number') .map(({ line }) => ({ line: line })), source); } this._model.breakpoints.restoreBreakpoints(breakpoints); } } //# sourceMappingURL=service.js.map