/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ /* global debuggee, quit, loadNative, readline, uneval */ // SPDX-License-Identifier: MPL-2.0 // SPDX-FileCopyrightText: 2011 Mozilla Foundation and contributors /* * This is a simple command-line debugger for GJS programs. It is based on * jorendb, which is a toy debugger for shell-js programs included in the * SpiderMonkey source. * * To run it: gjs -d path/to/file.js * Execution will stop at debugger statements, and you'll get a prompt before * the first frame is executed. */ const {print, logError} = loadNative('_print'); // Debugger state. var focusedFrame = null; var topFrame = null; var debuggeeValues = {}; var nextDebuggeeValueIndex = 1; var lastExc = null; var options = {pretty: true, colors: true, ignoreCaughtExceptions: true}; var breakpoints = [undefined]; // Breakpoint numbers start at 1 // Cleanup functions to run when we next re-enter the repl. var replCleanups = []; // Convert a debuggee value v to a string. function dvToString(v) { if (typeof v === 'undefined') return 'undefined'; // uneval(undefined) === '(void 0)', confusing if (v === null) return 'null'; // typeof null === 'object', so avoid that case return typeof v !== 'object' || v === null ? uneval(v) : `[object ${v.class}]`; } function debuggeeValueToString(dv, style = {pretty: options.pretty}) { // Special sentinel values returned by Debugger.Environment.getVariable() if (typeof dv === 'object' && dv !== null) { if (dv.missingArguments) return ['', undefined]; if (dv.optimizedOut) return ['', undefined]; if (dv.uninitialized) return ['', undefined]; if (!(dv instanceof Debugger.Object)) return ['', JSON.stringify(dv, null, 4)]; } const dvrepr = dvToString(dv); if (!style.pretty || dv === null || typeof dv !== 'object') return [dvrepr, undefined]; if (['TypeError', 'Error', 'GIRespositoryNamespace', 'GObject_Object'].includes(dv.class)) { const errval = debuggeeGlobalWrapper.executeInGlobalWithBindings( 'v.toString()', {v: dv}); return [dvrepr, errval['return']]; } if (style.brief) return [dvrepr, dvrepr]; const str = debuggeeGlobalWrapper.executeInGlobalWithBindings( 'imports._print.getPrettyPrintFunction(globalThis)(v)', {v: dv}); if ('throw' in str) { if (style.noerror) return [dvrepr, undefined]; const substyle = {...style, noerror: true}; return [dvrepr, debuggeeValueToString(str.throw, substyle)]; } return [dvrepr, str['return']]; } function showDebuggeeValue(dv, style = {pretty: options.pretty}) { const i = nextDebuggeeValueIndex++; debuggeeValues[`$${i}`] = dv; debuggeeValues['$$'] = dv; const [brief, full] = debuggeeValueToString(dv, style); print(`$${i} = ${brief}`); if (full !== undefined) print(full); } Object.defineProperty(Debugger.Frame.prototype, 'num', { configurable: true, enumerable: false, get() { let i = 0; let f; for (f = topFrame; f && f !== this; f = f.older) i++; return f === null ? undefined : i; }, }); Debugger.Frame.prototype.describeFrame = function () { if (this.type === 'call') { return `${this.callee.name || ''}(${ this.arguments.map(dvToString).join(', ')})`; } else if (this.type === 'global') { return 'toplevel'; } else { return `${this.type} code`; } }; Debugger.Frame.prototype.describePosition = function () { if (this.script) return this.script.describeOffset(this.offset); return null; }; Debugger.Frame.prototype.describeFull = function () { const fr = this.describeFrame(); const pos = this.describePosition(); if (pos) return `${fr} at ${pos}`; return fr; }; Object.defineProperty(Debugger.Frame.prototype, 'line', { configurable: true, enumerable: false, get() { if (this.script) return this.script.getOffsetLocation(this.offset).lineNumber; else return null; }, }); Debugger.Script.prototype.describeOffset = function describeOffset(offset) { const {lineNumber, columnNumber} = this.getOffsetLocation(offset); const url = this.url || ''; return `${url}:${lineNumber}:${columnNumber}`; }; function showFrame(f, n, option = {btCommand: false, fullOption: false}) { if (f === undefined || f === null) { f = focusedFrame; if (f === null) { print('No stack.'); return; } } if (n === undefined) { n = f.num; if (n === undefined) throw new Error('Internal error: frame not on stack'); } print(`#${n.toString().padEnd(4)} ${f.describeFull()}`); if (option.btCommand) { if (option.fullOption) { const variables = f.environment.names(); for (let i = 0; i < variables.length; i++) { if (variables.length === 0) print('No locals.'); const value = f.environment.getVariable(variables[i]); const [brief] = debuggeeValueToString(value, {brief: false, pretty: false}); print(`${variables[i]} = ${brief}`); } } } else { let lineNumber = f.line; print(` ${lineNumber}\t${f.script.source.text.split('\n')[lineNumber - 1]}`); } } function saveExcursion(fn) { const tf = topFrame, ff = focusedFrame; try { return fn(); } finally { topFrame = tf; focusedFrame = ff; } } // Accept debugger commands starting with '#' so that scripting the debugger // can be annotated function commentCommand(comment) { void comment; } // Evaluate an expression in the Debugger global - used for debugging the // debugger function evalCommand(expr) { eval(expr); } function quitCommand() { dbg.removeAllDebuggees(); quit(0); } quitCommand.summary = 'Quit the debugger'; quitCommand.helpText = `USAGE quit`; function backtraceCommand(option) { if (topFrame === null) print('No stack.'); if (option === '') { for (let i = 0, f = topFrame; f; i++, f = f.older) showFrame(f, i, {btCommand: true, fullOption: false}); } else if (option === 'full') { for (let i = 0, f = topFrame; f; i++, f = f.older) showFrame(f, i, {btCommand: true, fullOption: true}); } else { print('Invalid option'); } } backtraceCommand.summary = 'Print backtrace of all stack frames and details of all local variables if the full option is added'; backtraceCommand.helpText = `USAGE bt