UNPKG

dbgmits

Version:

Provides the ability to control GDB and LLDB programmatically via GDB/MI.

1,072 lines (1,071 loc) 88.1 kB
// Copyright (c) 2015 Vadim Macagon // MIT License, see LICENSE file for full terms. var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } __.prototype = b.prototype; d.prototype = new __(); }; /// <reference path="../typings/lib/tsd.d.ts" /> var child_process_1 = require('child_process'); var readline = require('readline'); var os = require('os'); var path = require('path'); var events = require('events'); var parser = require('./mi_output_parser'); var mi_output_1 = require('./mi_output'); var pty = require('pty.js'); var DebugCommand = (function () { /** * @param cmd MI command string (minus the token and dash prefix). * @param token Token that can be used to match up the command with a response. * @param done Callback to invoke once a response is received for the command. */ function DebugCommand(cmd, token, done) { this.token = token; this.text = cmd; this.done = done; } return DebugCommand; })(); /** * Used to indicate failure of a MI command sent to the debugger. */ var CommandFailedError = (function () { function CommandFailedError(message, command, code, token) { this.name = "CommandFailedError"; this.message = message; this.code = code; this.command = command; this.token = token; } return CommandFailedError; })(); exports.CommandFailedError = CommandFailedError; /** * Used to indicate the response to an MI command didn't match the expected format. */ var MalformedResponseError = (function () { /** * @param message The description of the error. * @param response The malformed response text (usually just the relevant part). * @param command The command text that was sent to the debugger (minus token and dash prefix). * @param token Token of the command (if the command had one). */ function MalformedResponseError(message, response, command, token) { this.message = message; this.response = response; this.command = command; this.token = token; this.name = "MalformedResponseError"; } return MalformedResponseError; })(); exports.MalformedResponseError = MalformedResponseError; (function (TargetStopReason) { /** A breakpoint was hit. */ TargetStopReason[TargetStopReason["BreakpointHit"] = 0] = "BreakpointHit"; /** A step instruction finished. */ TargetStopReason[TargetStopReason["EndSteppingRange"] = 1] = "EndSteppingRange"; /** A step-out instruction finished. */ TargetStopReason[TargetStopReason["FunctionFinished"] = 2] = "FunctionFinished"; /** The target finished executing and terminated normally. */ TargetStopReason[TargetStopReason["ExitedNormally"] = 3] = "ExitedNormally"; /** The target was signalled. */ TargetStopReason[TargetStopReason["SignalReceived"] = 4] = "SignalReceived"; /** The target encountered an exception (this is LLDB specific). */ TargetStopReason[TargetStopReason["ExceptionReceived"] = 5] = "ExceptionReceived"; /** Catch-all for any of the other numerous reasons. */ TargetStopReason[TargetStopReason["Unrecognized"] = 6] = "Unrecognized"; })(exports.TargetStopReason || (exports.TargetStopReason = {})); var TargetStopReason = exports.TargetStopReason; /** Indicates how much information should be retrieved when calling * [[DebugSession.getLocalVariables]]. */ (function (VariableDetailLevel) { /** Only variable names will be retrieved, not their types or values. */ VariableDetailLevel[VariableDetailLevel["None"] = 0] = "None"; /** Only variable names and values will be retrieved, not their types. */ VariableDetailLevel[VariableDetailLevel["All"] = 1] = "All"; /** * The name and type will be retrieved for all variables, however values will only be retrieved * for simple variable types (not arrays, structures or unions). */ VariableDetailLevel[VariableDetailLevel["Simple"] = 2] = "Simple"; })(exports.VariableDetailLevel || (exports.VariableDetailLevel = {})); var VariableDetailLevel = exports.VariableDetailLevel; /** Output format specifiers for watch values. */ (function (WatchFormatSpec) { WatchFormatSpec[WatchFormatSpec["Binary"] = 0] = "Binary"; WatchFormatSpec[WatchFormatSpec["Decimal"] = 1] = "Decimal"; WatchFormatSpec[WatchFormatSpec["Hexadecimal"] = 2] = "Hexadecimal"; WatchFormatSpec[WatchFormatSpec["Octal"] = 3] = "Octal"; /** * This specifier is used to indicate that one of the other ones should be automatically chosen * based on the expression type, for example `Decimal` for integers, `Hexadecimal` for pointers. */ WatchFormatSpec[WatchFormatSpec["Default"] = 4] = "Default"; })(exports.WatchFormatSpec || (exports.WatchFormatSpec = {})); var WatchFormatSpec = exports.WatchFormatSpec; /** A watch may have one or more of these attributes associated with it. */ (function (WatchAttribute) { /** Indicates the watch value can be modified. */ WatchAttribute[WatchAttribute["Editable"] = 0] = "Editable"; /** * Indicates the watch value can't be modified. This will be the case for any watch with * children (at least when implemented correctly by the debugger, *cough* not LLDB-MI *cough*). */ WatchAttribute[WatchAttribute["NonEditable"] = 1] = "NonEditable"; })(exports.WatchAttribute || (exports.WatchAttribute = {})); var WatchAttribute = exports.WatchAttribute; /** Output format specifiers for register values. */ (function (RegisterValueFormatSpec) { RegisterValueFormatSpec[RegisterValueFormatSpec["Binary"] = 0] = "Binary"; RegisterValueFormatSpec[RegisterValueFormatSpec["Decimal"] = 1] = "Decimal"; RegisterValueFormatSpec[RegisterValueFormatSpec["Hexadecimal"] = 2] = "Hexadecimal"; RegisterValueFormatSpec[RegisterValueFormatSpec["Octal"] = 3] = "Octal"; RegisterValueFormatSpec[RegisterValueFormatSpec["Raw"] = 4] = "Raw"; /** * This specifier is used to indicate that one of the other ones should be automatically chosen. */ RegisterValueFormatSpec[RegisterValueFormatSpec["Default"] = 5] = "Default"; })(exports.RegisterValueFormatSpec || (exports.RegisterValueFormatSpec = {})); var RegisterValueFormatSpec = exports.RegisterValueFormatSpec; /** * A debug session provides two-way communication with a debugger process via the GDB/LLDB * machine interface. * * Currently commands are queued and executed one at a time in the order they are issued, * a command will not be executed until all the previous commands have been acknowledged by the * debugger. * * Out of band notifications from the debugger are emitted via events, the names of these events * are provided by the EVENT_XXX static constants. */ var DebugSession = (function (_super) { __extends(DebugSession, _super); /** * In most cases [[startDebugSession]] should be used to construct new instances. * * @param inStream Debugger responses and notifications will be read from this stream. * @param outStream Debugger commands will be written to this stream. */ function DebugSession(inStream, outStream) { _super.call(this); this.outStream = outStream; this.lineReader = readline.createInterface({ input: inStream, output: null }); this.lineReader.on('line', this.parseDebbugerOutput.bind(this)); this.nextCmdId = 1; this.cmdQueue = []; this.cleanupWasCalled = false; } Object.defineProperty(DebugSession.prototype, "logger", { get: function () { return this._logger; }, set: function (logger) { this._logger = logger; }, enumerable: true, configurable: true }); /** * Ends the debugging session. * * @param notifyDebugger If **false** the session is cleaned up immediately without waiting for * the debugger to respond (useful in cases where the debugger terminates * unexpectedly). If **true** the debugger is asked to exit, and once the * request is acknowldeged the session is cleaned up. */ DebugSession.prototype.end = function (notifyDebugger) { var _this = this; if (notifyDebugger === void 0) { notifyDebugger = true; } return new Promise(function (resolve, reject) { var cleanup = function (err, data) { _this.cleanupWasCalled = true; _this.lineReader.close(); err ? reject(err) : resolve(); }; if (!_this.cleanupWasCalled) { notifyDebugger ? _this.enqueueCommand(new DebugCommand('gdb-exit', null, cleanup)) : cleanup(null, null); } ; }); }; /** * Returns `true` if [[EVENT_FUNCTION_FINISHED]] can be emitted during this debugging session. * * LLDB-MI currently doesn't emit [[EVENT_FUNCTION_FINISHED]] after stepping out of a function, * instead it emits [[EVENT_STEP_FINISHED]] just like it does for any other stepping operation. */ DebugSession.prototype.canEmitFunctionFinishedNotification = function () { return false; }; DebugSession.prototype.emitExecNotification = function (name, data) { switch (name) { case 'running': this.emit(DebugSession.EVENT_TARGET_RUNNING, data['thread-id']); break; case 'stopped': if (this.logger) { this.logger.debug(data); } var standardNotify = { reason: parseTargetStopReason(data.reason), threadId: parseInt(data['thread-id'], 10), stoppedThreads: parseStoppedThreadsList(data['stopped-threads']), processCore: data.core }; this.emit(DebugSession.EVENT_TARGET_STOPPED, standardNotify); // emit a more specialized event for notifications that contain additional info switch (standardNotify.reason) { case TargetStopReason.BreakpointHit: var breakpointNotify = { reason: standardNotify.reason, threadId: standardNotify.threadId, stoppedThreads: standardNotify.stoppedThreads, processorCore: standardNotify.processorCore, breakpointId: parseInt(data.bkptno, 10), frame: extractFrameInfo(data.frame) }; this.emit(DebugSession.EVENT_BREAKPOINT_HIT, breakpointNotify); break; case TargetStopReason.EndSteppingRange: var stepNotify = { reason: standardNotify.reason, threadId: standardNotify.threadId, stoppedThreads: standardNotify.stoppedThreads, processorCore: standardNotify.processorCore, frame: extractFrameInfo(data.frame) }; this.emit(DebugSession.EVENT_STEP_FINISHED, stepNotify); break; case TargetStopReason.FunctionFinished: var stepOutNotify = { reason: standardNotify.reason, threadId: standardNotify.threadId, stoppedThreads: standardNotify.stoppedThreads, processorCore: standardNotify.processorCore, frame: extractFrameInfo(data.frame), resultVar: data['gdb-result-var'], returnValue: data['return-value'] }; this.emit(DebugSession.EVENT_FUNCTION_FINISHED, stepOutNotify); break; case TargetStopReason.SignalReceived: var signalNotify = { reason: standardNotify.reason, threadId: standardNotify.threadId, stoppedThreads: standardNotify.stoppedThreads, processorCore: standardNotify.processorCore, signalCode: data.signal, signalName: data['signal-name'], signalMeaning: data['signal-meaning'] }; this.emit(DebugSession.EVENT_SIGNAL_RECEIVED, signalNotify); break; case TargetStopReason.ExceptionReceived: var exceptionNotify = { reason: standardNotify.reason, threadId: standardNotify.threadId, stoppedThreads: standardNotify.stoppedThreads, processorCore: standardNotify.processorCore, exception: data.exception }; this.emit(DebugSession.EVENT_EXCEPTION_RECEIVED, exceptionNotify); break; } break; default: // TODO: log and keep on going break; } }; DebugSession.prototype.emitAsyncNotification = function (name, data) { var shlibInfo; switch (name) { case 'thread-group-added': this.emit(DebugSession.EVENT_THREAD_GROUP_ADDED, data); break; case 'thread-group-removed': this.emit(DebugSession.EVENT_THREAD_GROUP_REMOVED, data); break; case 'thread-group-started': this.emit(DebugSession.EVENT_THREAD_GROUP_STARTED, data); break; case 'thread-group-exited': this.emit(DebugSession.EVENT_THREAD_GROUP_EXITED, { id: data.id, exitCode: data['exit-code'] }); break; case 'thread-created': this.emit(DebugSession.EVENT_THREAD_CREATED, { id: data.id, groupId: data['group-id'] }); break; case 'thread-exited': this.emit(DebugSession.EVENT_THREAD_EXITED, { id: data.id, groupId: data['group-id'] }); break; case 'thread-selected': this.emit(DebugSession.EVENT_THREAD_SELECTED, data); break; case 'library-loaded': this.emit(DebugSession.EVENT_LIB_LOADED, { id: data.id, targetName: data['target-name'], hostName: data['host-name'], threadGroup: data['thread-group'], symbolsPath: data['symbols-path'], loadAddress: data.loaded_addr }); break; case 'library-unloaded': this.emit(DebugSession.EVENT_LIB_UNLOADED, { id: data.id, targetName: data['target-name'], hostName: data['host-name'], threadGroup: data['thread-group'], symbolsPath: data['symbols-path'], loadAddress: data.loaded_addr }); break; default: // TODO: log and keep on going break; } ; }; /** * Parse a single line containing a response to a MI command or some sort of async notification. */ DebugSession.prototype.parseDebbugerOutput = function (line) { // '(gdb)' (or '(gdb) ' in some cases) is used to indicate the end of a set of output lines // from the debugger, but since we process each line individually as it comes in this // particular marker is of no use if (line.match(/^\(gdb\)\s*/) || (line === '')) { return; } var cmdQueuePopped = false; try { var result = parser.parse(line); } catch (err) { if (this.logger) { this.logger.error(err, 'Attempted to parse: ->' + line + '<-'); } throw err; } switch (result.recordType) { case mi_output_1.RecordType.Done: case mi_output_1.RecordType.Running: case mi_output_1.RecordType.Connected: case mi_output_1.RecordType.Exit: case mi_output_1.RecordType.Error: // this record is a response for the last command that was sent to the debugger, // which is the command at the front of the queue var cmd = this.cmdQueue.shift(); cmdQueuePopped = true; // todo: check that the token in the response matches the one sent with the command if (cmd.done) { if (result.recordType === mi_output_1.RecordType.Error) { cmd.done(new CommandFailedError(result.data.msg, cmd.text, result.data.code, cmd.token), null); } else { cmd.done(null, result.data); } } break; case mi_output_1.RecordType.AsyncExec: this.emitExecNotification(result.data[0], result.data[1]); break; case mi_output_1.RecordType.AsyncNotify: this.emitAsyncNotification(result.data[0], result.data[1]); break; case mi_output_1.RecordType.DebuggerConsoleOutput: this.emit(DebugSession.EVENT_DBG_CONSOLE_OUTPUT, result.data); break; case mi_output_1.RecordType.TargetOutput: this.emit(DebugSession.EVENT_TARGET_OUTPUT, result.data); break; case mi_output_1.RecordType.DebuggerLogOutput: this.emit(DebugSession.EVENT_DBG_LOG_OUTPUT, result.data); break; } // if a command was popped from the qeueu we can send through the next command if (cmdQueuePopped && (this.cmdQueue.length > 0)) { this.sendCommandToDebugger(this.cmdQueue[0]); } }; /** * Sends an MI command to the debugger process. */ DebugSession.prototype.sendCommandToDebugger = function (command) { var cmdStr; if (command.token) { cmdStr = command.token + "-" + command.text; } else { cmdStr = '-' + command.text; } if (this.logger) { this.logger.info(cmdStr); } this.outStream.write(cmdStr + '\n'); }; /** * Adds an MI command to the back of the command queue. * * If the command queue is empty when this method is called then the command is dispatched * immediately, otherwise it will be dispatched after all the previously queued commands are * processed. */ DebugSession.prototype.enqueueCommand = function (command) { this.cmdQueue.push(command); if (this.cmdQueue.length === 1) { this.sendCommandToDebugger(this.cmdQueue[0]); } }; /** * Sends an MI command to the debugger. * * @param command Full MI command string, excluding the optional token and dash prefix. * @param token Token to be prefixed to the command string (must consist only of digits). * @returns A promise that will be resolved when the command response is received. */ DebugSession.prototype.executeCommand = function (command, token) { var _this = this; return new Promise(function (resolve, reject) { _this.enqueueCommand(new DebugCommand(command, token, function (err, data) { err ? reject(err) : resolve(); })); }); }; /** * Sends an MI command to the debugger and returns the response. * * @param command Full MI command string, excluding the optional token and dash prefix. * @param token Token to be prefixed to the command string (must consist only of digits). * @param transformOutput This function will be invoked with the output of the MI Output parser * and should transform that output into an instance of type `T`. * @returns A promise that will be resolved when the command response is received. */ DebugSession.prototype.getCommandOutput = function (command, token, transformOutput) { var _this = this; return new Promise(function (resolve, reject) { _this.enqueueCommand(new DebugCommand(command, token, function (err, data) { if (err) { reject(err); } else { try { resolve(transformOutput ? transformOutput(data) : data); } catch (err) { reject(err); } } })); }); }; /** * Sets the executable file to be debugged, the symbol table will also be read from this file. * * This must be called prior to [[connectToRemoteTarget]] when setting up a remote debugging * session. * * @param file This would normally be a full path to the host's copy of the executable to be * debugged. */ DebugSession.prototype.setExecutableFile = function (file) { // NOTE: While the GDB/MI spec. contains multiple -file-XXX commands that allow the // executable and symbol files to be specified separately the LLDB MI driver // currently (30-Mar-2015) only supports this one command. return this.executeCommand("file-exec-and-symbols " + file); }; /** * Sets the terminal to be used by the next inferior that's launched. * * @param slaveName Name of the slave end of a pseudoterminal that should be associated with * the inferior, see `man pty` for an overview of pseudoterminals. */ DebugSession.prototype.setInferiorTerminal = function (slaveName) { return this.executeCommand('inferior-tty-set ' + slaveName); }; /** * Connects the debugger to a remote target. * * @param host * @param port */ DebugSession.prototype.connectToRemoteTarget = function (host, port) { return this.executeCommand("target-select remote " + host + ":" + port); }; // // Breakpoint Commands // /** * Adds a new breakpoint. * * @param location The location at which a breakpoint should be added, can be specified in the * following formats: * - function_name * - filename:line_number * - filename:function_name * - address * @param options.isTemp Set to **true** to create a temporary breakpoint which will be * automatically removed after being hit. * @param options.isHardware Set to **true** to create a hardware breakpoint * (presently not supported by LLDB MI). * @param options.isPending Set to **true** if the breakpoint should still be created even if * the location cannot be parsed (e.g. it refers to uknown files or * functions). * @param options.isDisabled Set to **true** to create a breakpoint that is initially disabled, * otherwise the breakpoint will be enabled by default. * @param options.isTracepoint Set to **true** to create a tracepoint * (presently not supported by LLDB MI). * @param options.condition The debugger will only stop the program execution when this * breakpoint is hit if the condition evaluates to **true**. * @param options.ignoreCount The number of times the breakpoint should be hit before it takes * effect, zero (the default) means the breakpoint will stop the * program every time it's hit. * @param options.threadId Restricts the new breakpoint to the given thread. */ DebugSession.prototype.addBreakpoint = function (location, options) { var cmd = 'break-insert'; if (options) { if (options.isTemp) { cmd = cmd + ' -t'; } if (options.isHardware) { cmd = cmd + ' -h'; } if (options.isPending) { cmd = cmd + ' -f'; } if (options.isDisabled) { cmd = cmd + ' -d'; } if (options.isTracepoint) { cmd = cmd + ' -a'; } if (options.condition) { cmd = cmd + ' -c ' + options.condition; } if (options.ignoreCount !== undefined) { cmd = cmd + ' -i ' + options.ignoreCount; } if (options.threadId !== undefined) { cmd = cmd + ' -p ' + options.threadId; } } return this.getCommandOutput(cmd + ' ' + location, null, function (output) { return extractBreakpointInfo(output); }); }; /** * Removes a breakpoint. */ DebugSession.prototype.removeBreakpoint = function (breakId) { return this.executeCommand('break-delete ' + breakId); }; /** * Removes multiple breakpoints. */ DebugSession.prototype.removeBreakpoints = function (breakIds) { // FIXME: LLDB MI driver only supports removing one breakpoint at a time, // so multiple breakpoints need to be removed one by one. return this.executeCommand('break-delete ' + breakIds.join(' ')); }; /** * Enables a breakpoint. */ DebugSession.prototype.enableBreakpoint = function (breakId) { return this.executeCommand('break-enable ' + breakId); }; /** * Disables a breakpoint. */ DebugSession.prototype.disableBreakpoint = function (breakId) { return this.executeCommand('break-disable ' + breakId); }; /** * Tells the debugger to ignore a breakpoint the next `ignoreCount` times it's hit. * * @param breakId Identifier of the breakpoint for which the ignore count should be set. * @param ignoreCount The number of times the breakpoint should be hit before it takes effect, * zero means the breakpoint will stop the program every time it's hit. */ DebugSession.prototype.ignoreBreakpoint = function (breakId, ignoreCount) { return this.getCommandOutput("break-after " + breakId + " " + ignoreCount, null, function (output) { return extractBreakpointInfo(output); }); }; /** * Sets the condition under which a breakpoint should take effect when hit. * * @param breakId Identifier of the breakpoint for which the condition should be set. * @param condition Expression to evaluate when the breakpoint is hit, if it evaluates to * **true** the breakpoint will stop the program, otherwise the breakpoint * will have no effect. */ DebugSession.prototype.setBreakpointCondition = function (breakId, condition) { return this.executeCommand("break-condition " + breakId + " " + condition); }; // // Program Execution Commands // /** * Sets the commandline arguments to be passed to the target process next time it is started * using [[startTarget]]. */ DebugSession.prototype.setTargetArguments = function (args) { return this.executeCommand('exec-arguments ' + args); }; /** * Executes an inferior from the beginning until it exits. * * Execution may stop before the inferior finishes running due to a number of reasons, * for example a breakpoint being hit. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadGroup *(GDB specific)* The identifier of the thread group to start, * if omitted the currently selected inferior will be started. * @param options.stopAtStart *(GDB specific)* If `true` then execution will stop at the start * of the main function. */ DebugSession.prototype.startInferior = function (options) { var fullCmd = 'exec-run'; if (options) { if (options.threadGroup) { fullCmd = fullCmd + ' --thread-group ' + options.threadGroup; } if (options.stopAtStart) { fullCmd = fullCmd + ' --start'; } } return this.executeCommand(fullCmd, null); }; /** * Executes all inferiors from the beginning until they exit. * * Execution may stop before an inferior finishes running due to a number of reasons, * for example a breakpoint being hit. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param stopAtStart *(GDB specific)* If `true` then execution will stop at the start * of the main function. */ DebugSession.prototype.startAllInferiors = function (stopAtStart) { var fullCmd = 'exec-run --all'; if (stopAtStart) { fullCmd = fullCmd + ' --start'; } return this.executeCommand(fullCmd, null); }; /** * Kills the currently selected inferior. */ DebugSession.prototype.abortInferior = function () { return this.executeCommand('exec-abort'); }; /** * Resumes execution of an inferior, execution may stop at any time due to a number of reasons, * for example a breakpoint being hit. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadGroup *(GDB specific)* Identifier of the thread group to resume, * if omitted the currently selected inferior is resumed. * @param options.reverse *(GDB specific)* If **true** the inferior is executed in reverse. */ DebugSession.prototype.resumeInferior = function (options) { var fullCmd = 'exec-continue'; if (options) { if (options.threadGroup) { fullCmd = fullCmd + ' --thread-group ' + options.threadGroup; } if (options.reverse) { fullCmd = fullCmd + ' --reverse'; } } return this.executeCommand(fullCmd, null); }; /** * Resumes execution of all inferiors. * * @param reverse *(GDB specific)* If `true` the inferiors are executed in reverse. */ DebugSession.prototype.resumeAllInferiors = function (reverse) { var fullCmd = 'exec-continue --all'; if (reverse) { fullCmd = fullCmd + ' --reverse'; } return this.executeCommand(fullCmd, null); }; /** * Interrupts execution of an inferior. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadGroup The identifier of the thread group to interrupt, if omitted the * currently selected inferior will be interrupted. */ DebugSession.prototype.interruptInferior = function (threadGroup) { var fullCmd = 'exec-interrupt'; if (threadGroup) { fullCmd = fullCmd + ' --thread-group ' + threadGroup; } return this.executeCommand(fullCmd, null); }; /** * Interrupts execution of all threads in all inferiors. * * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. */ DebugSession.prototype.interruptAllInferiors = function () { return this.executeCommand('exec-interrupt --all', null); }; /** * Resumes execution of the target until the beginning of the next source line is reached. * If a function is called while the target is running then execution stops on the first * source line of the called function. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadId Identifier of the thread to execute the command on. * @param options.reverse *(GDB specific)* If **true** the target is executed in reverse. */ DebugSession.prototype.stepIntoLine = function (options) { return this.executeCommand(appendExecCmdOptions('exec-step', options)); }; /** * Resumes execution of the target until the beginning of the next source line is reached. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadId Identifier of the thread to execute the command on. * @param options.reverse *(GDB specific)* If **true** the target is executed in reverse until * the beginning of the previous source line is reached. */ DebugSession.prototype.stepOverLine = function (options) { return this.executeCommand(appendExecCmdOptions('exec-next', options)); }; /** * Executes one instruction, if the instruction is a function call then execution stops at the * beginning of the function. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadId Identifier of the thread to execute the command on. * @param options.reverse *(GDB specific)* If **true** the target is executed in reverse until * the previous instruction is reached. */ DebugSession.prototype.stepIntoInstruction = function (options) { return this.executeCommand(appendExecCmdOptions('exec-step-instruction', options)); }; /** * Executes one instruction, if the instruction is a function call then execution continues * until the function returns. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadId Identifier of the thread to execute the command on. * @param options.reverse *(GDB specific)* If **true** the target is executed in reverse until * the previous instruction is reached. */ DebugSession.prototype.stepOverInstruction = function (options) { return this.executeCommand(appendExecCmdOptions('exec-next-instruction', options)); }; /** * Resumes execution of the target until the current function returns. * [[EVENT_TARGET_STOPPED]] will be emitted when execution stops. * * @param options.threadId Identifier of the thread to execute the command on. * @param options.reverse *(GDB specific)* If **true** the target is executed in reverse. */ DebugSession.prototype.stepOut = function (options) { return this.executeCommand(appendExecCmdOptions('exec-finish', options)); }; // // Stack Inspection Commands // /** * Retrieves information about a stack frame. * * @param options.threadId The thread for which the stack depth should be retrieved, * defaults to the currently selected thread if not specified. * @param options.frameLevel Stack index of the frame for which to retrieve locals, * zero for the innermost frame, one for the frame from which the call * to the innermost frame originated, etc. Defaults to the currently * selected frame if not specified. If a value is provided for this * option then `threadId` must be specified as well. */ DebugSession.prototype.getStackFrame = function (options) { var fullCmd = 'stack-info-frame'; if (options) { if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread ' + options.threadId; } if (options.frameLevel !== undefined) { fullCmd = fullCmd + ' --frame ' + options.frameLevel; } } return this.getCommandOutput(fullCmd, null, function (output) { return extractStackFrameInfo(output.frame); }); }; /** * Retrieves the current depth of the stack. * * @param options.threadId The thread for which the stack depth should be retrieved, * defaults to the currently selected thread if not specified. * @param options.maxDepth *(GDB specific)* If specified the returned stack depth will not exceed * this number. */ DebugSession.prototype.getStackDepth = function (options) { var fullCmd = 'stack-info-depth'; if (options) { if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread ' + options.threadId; } if (options.maxDepth !== undefined) { fullCmd = fullCmd + ' ' + options.maxDepth; } } return this.getCommandOutput(fullCmd, null, function (output) { return parseInt(output.depth); }); }; /** * Retrieves the frames currently on the stack. * * The `lowFrame` and `highFrame` options can be used to limit the number of frames retrieved, * if both are supplied only the frame with levels in that range (inclusive) are retrieved. * If either `lowFrame` or `highFrame` option is omitted (but not both) then only a single * frame corresponding to that level is retrieved. * * @param options.threadId The thread for which the stack frames should be retrieved, * defaults to the currently selected thread if not specified. * @param options.noFrameFilters *(GDB specific)* If `true` the Python frame filters will not be * executed. * @param options.lowFrame Must not be larger than the actual number of frames on the stack. * @param options.highFrame May be larger than the actual number of frames on the stack, in which * case only the existing frames will be retrieved. */ DebugSession.prototype.getStackFrames = function (options) { var fullCmd = 'stack-list-frames'; if (options) { if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread' + options.threadId; } if (options.noFrameFilters === true) { fullCmd = fullCmd + ' --no-frame-filters'; } if ((options.lowFrame !== undefined) && (options.highFrame !== undefined)) { fullCmd = fullCmd + (" " + options.lowFrame + " " + options.highFrame); } else if (options.lowFrame !== undefined) { fullCmd = fullCmd + (" " + options.lowFrame + " " + options.lowFrame); } else if (options.highFrame !== undefined) { fullCmd = fullCmd + (" " + options.highFrame + " " + options.highFrame); } } return this.getCommandOutput(fullCmd, null, function (output) { var data = output.stack.frame; if (Array.isArray(data)) { return data.map(function (frame) { return extractStackFrameInfo(frame); }); } else { return [extractStackFrameInfo(data)]; } }); }; /** * Retrieves a list of all the arguments for the specified frames. * * The `lowFrame` and `highFrame` options can be used to limit the frames for which arguments * are retrieved. If both are supplied only the frames with levels in that range (inclusive) are * taken into account, if both are omitted the arguments of all frames currently on the stack * will be retrieved. * * Note that while it's possible to specify a frame range of one frame in order to retrieve the * arguments of a single frame it's better to just use [[getStackFrameVariables]] instead. * * @param detail Specifies what information should be retrieved for each argument. * @param options.threadId The thread for which arguments should be retrieved, * defaults to the currently selected thread if not specified. * @param options.noFrameFilters *(GDB specific)* If `true` then Python frame filters will not be * executed. * @param options.skipUnavailable If `true` information about arguments that are not available * will not be retrieved. * @param options.lowFrame Must not be larger than the actual number of frames on the stack. * @param options.highFrame May be larger than the actual number of frames on the stack, in which * case only the existing frames will be retrieved. */ DebugSession.prototype.getStackFrameArgs = function (detail, options) { var fullCmd = 'stack-list-arguments'; if (options) { if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread ' + options.threadId; } if (options.noFrameFilters === true) { fullCmd = fullCmd + ' --no-frame-filters'; } if (options.skipUnavailable === true) { fullCmd = fullCmd + ' --skip-unavailable'; } } fullCmd = fullCmd + ' ' + detail; if (options) { if ((options.lowFrame !== undefined) && (options.highFrame !== undefined)) { fullCmd = fullCmd + (" " + options.lowFrame + " " + options.highFrame); } else if ((options.lowFrame !== undefined) && (options.highFrame === undefined)) { throw new Error("highFrame option must be provided to getStackFrameArgs() if lowFrame option is used."); } else if ((options.lowFrame === undefined) && (options.highFrame !== undefined)) { throw new Error("lowFrame option must be provided to getStackFrameArgs() if highFrame option is used."); } } return this.getCommandOutput(fullCmd, null, function (output) { var data = output['stack-args']; if (Array.isArray(data.frame)) { // data is in the form: { frame: [{ level: 0, args: [...] }, { level: 1, args: arg1 }, ...] return data.frame.map(function (frame) { return { level: parseInt(frame.level, 10), args: Array.isArray(frame.args) ? frame.args : [frame.args] }; }); } else { // data is in the form: { frame: { level: 0, args: [...] } return [{ level: parseInt(data.frame.level, 10), args: Array.isArray(data.frame.args) ? data.frame.args : [data.frame.args] }]; } }); }; /** * Retrieves a list of all arguments and local variables in the specified frame. * * @param detail Specifies what information to retrieve for each argument or local variable. * @param options.threadId The thread for which variables should be retrieved, * defaults to the currently selected thread if not specified. * @param options.frameLevel Stack index of the frame for which to retrieve locals, * zero for the innermost frame, one for the frame from which the call * to the innermost frame originated, etc. Defaults to the currently * selected frame if not specified. * @param options.noFrameFilters *(GDB specific)* If `true` then Python frame filters will not be * executed. * @param options.skipUnavailable If `true` information about variables that are not available * will not be retrieved. */ DebugSession.prototype.getStackFrameVariables = function (detail, options) { var fullCmd = 'stack-list-variables'; if (options) { if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread ' + options.threadId; } if (options.frameLevel !== undefined) { fullCmd = fullCmd + ' --frame ' + options.frameLevel; } if (options.noFrameFilters === true) { fullCmd = fullCmd + ' --no-frame-filters'; } if (options.skipUnavailable === true) { fullCmd = fullCmd + ' --skip-unavailable'; } } fullCmd = fullCmd + ' ' + detail; return this.getCommandOutput(fullCmd, null, function (output) { var args = []; var locals = []; output.variables.forEach(function (varInfo) { if (varInfo.arg === '1') { args.push({ name: varInfo.name, value: varInfo.value, type: varInfo.type }); } else { locals.push({ name: varInfo.name, value: varInfo.value, type: varInfo.type }); } }); return { args: args, locals: locals }; }); }; // // Watch Manipulation (aka Variable Objects) // /** * Creates a new watch to monitor the value of the given expression. * * @param expression Any expression valid in the current language set (so long as it doesn't * begin with a `*`), or one of the following: * - a memory cell address, e.g. `*0x0000000000400cd0` * - a CPU register name, e.g. `$sp` * @param options.id Unique identifier for the new watch, if omitted one is auto-generated. * Auto-generated identifiers begin with the letters `var` and are followed by * one or more digits, when providing your own identifiers it's best to use a * different naming scheme that doesn't clash with auto-generated identifiers. * @param options.threadId The thread within which the watch expression will be evaluated. * *Default*: the currently selected thread. * @param options.threadGroup * @param options.frameLevel The index of the stack frame within which the watch expression will * be evaluated initially, zero for the innermost stack frame. Note that * if `frameLevel` is specified then `threadId` must also be specified. * *Default*: the currently selected frame. * @param options.frameAddress *(GDB specific)* Address of the frame within which the expression * should be evaluated. * @param options.isFloating Set to `true` if the expression should be re-evaluated every time * within the current frame, i.e. it's not bound to a specific frame. * Set to `false` if the expression should be bound to the frame within * which the watch is created. * *Default*: `false`. */ DebugSession.prototype.addWatch = function (expression, options) { var fullCmd = 'var-create'; var id = '-'; // auto-generate id var addr = '*'; // use current frame if (options) { if (options.id) { id = options.id; } if (options.threadId !== undefined) { fullCmd = fullCmd + ' --thread ' + options.threadId; } if (options.threadGroup) { fullCmd = fullCmd + ' --thread-group ' + options.threadGroup; } if (options.frameLevel !== undefined) { fullCmd = fullCmd + ' --frame ' + options.frameLevel; } if (options.isFloating === true) { addr = '@'; } else if (options.frameAddress) { addr = options.frameAddress; } } fullCmd = fullCmd + (" " + id + " " + addr + " " + expression); return this.getCommandOutput(fullCmd, null, function (output) { return { id: output.name, childCount: parseInt(output.numchild), value: output.value, expressionType: output['type'], threadId: parseInt(output['thread-id']), hasMoreChildren: output.has_more !== '0', isDynamic: output.dynamic === '1', displayHint: output.displayhint }; }); }; /** * Destroys a previously created watch. * * @param id Identifier of the watch to destroy. */ DebugSession.prototype.removeWatch = function (id) { return this.executeCommand('var-delete ' + id); }; /** * Updates the state of an existing watch. * * @param id Identifier of the watch to update. */ DebugSession.prototype.updateWatch = function (id, detail) { var fullCmd = 'var-update'; if (detail !== undefined) { fullCmd = fullCmd + ' ' + detail; }