asmimproved-dbgmits
Version:
Provides the ability to control GDB and LLDB programmatically via GDB/MI.
1,300 lines (1,197 loc) • 60.4 kB
text/typescript
// Copyright (c) 2015 Vadim Macagon
// MIT License, see LICENSE file for full terms.
import * as readline from 'readline';
import * as events from 'events';
import * as stream from 'stream';
import * as parser from './mi_output_parser';
import { RecordType } from './mi_output';
import * as bunyan from 'bunyan';
import * as Events from './events';
import {
IBreakpointInfo, IBreakpointLocationInfo,
IStackFrameInfo, IStackFrameArgsInfo, IStackFrameVariablesInfo, IVariableInfo,
IWatchInfo, IWatchUpdateInfo, IWatchChildInfo, IMemoryBlock, IAsmInstruction, ISourceLineAsm,
IThreadFrameInfo, IThreadInfo, IMultiThreadInfo,
VariableDetailLevel, WatchFormatSpec, WatchAttribute, RegisterValueFormatSpec, ISourceAddress
} from './types';
import {
extractBreakpointInfo, extractStackFrameInfo, extractWatchChildren, extractAsmInstructions,
extractAsmBySourceLine, extractThreadInfo
} from './extractors';
import { CommandFailedError, MalformedResponseError } from './errors';
// aliases
type ReadLine = readline.ReadLine;
type ErrDataCallback = (err: Error, data: any) => void;
class DebugCommand {
/**
* Optional token that can be used to match up the command with a response,
* if provided it must only contain digits.
*/
token: string;
/** The MI command string that will be sent to debugger (minus the token and dash prefix). */
text: string;
/** Optional callback to invoke once a response is received for the command. */
done: ErrDataCallback;
/**
* @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.
*/
constructor(cmd: string, token?: string, done?: ErrDataCallback) {
this.token = token;
this.text = cmd;
this.done = done;
}
}
/**
* 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.
*/
export default class DebugSession extends events.EventEmitter {
// the stream to which debugger commands will be written
private outStream: stream.Writable;
// reads input from the debugger's stdout one line at a time
private lineReader: ReadLine;
// used to generate to auto-generate tokens for commands
// FIXME: this is currently unused since I need to decide if tokens should be auto-generated
// when the user doesn't supply them.
private nextCmdId: number;
// commands to be processed (one at a time)
private cmdQueue: DebugCommand[];
// used to to ensure session cleanup is only done once
private cleanupWasCalled: boolean;
private _logger: bunyan.Logger;
get logger(): bunyan.Logger {
return this._logger;
}
set logger(logger: bunyan.Logger) {
this._logger = logger;
}
/**
* 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.
*/
constructor(inStream: stream.Readable, outStream: stream.Writable) {
super();
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;
}
/**
* 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.
*/
end(notifyDebugger: boolean = true): Promise<void> {
return new Promise<void>((resolve, reject) => {
var cleanup = (err: Error, data: any) => {
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.
*/
canEmitFunctionFinishedNotification(): boolean {
return false;
}
private emitExecNotification(name: string, data: any) {
let events = Events.createEventsForExecNotification(name, data);
events.forEach((event: Events.IDebugSessionEvent) => {
this.emit(event.name, event.data);
});
}
private emitAsyncNotification(name: string, data: any) {
let event = Events.createEventForAsyncNotification(name, data);
if (event) {
this.emit(event.name, event.data);
} else {
if (this.logger) {
this.logger.warn({ name: name, data: data }, 'Unhandled notification.');
}
}
}
/**
* Parse a single line containing a response to a MI command or some sort of async notification.
*/
private parseDebbugerOutput(line: string): void {
// '(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: boolean = 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 RecordType.Done:
case RecordType.Running:
case RecordType.Connected:
case RecordType.Exit:
case 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 === 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 RecordType.AsyncExec:
this.emitExecNotification(result.data[0], result.data[1]);
break;
case RecordType.AsyncNotify:
this.emitAsyncNotification(result.data[0], result.data[1]);
break;
case RecordType.DebuggerConsoleOutput:
this.emit(Events.EVENT_DBG_CONSOLE_OUTPUT, result.data);
break;
case RecordType.TargetOutput:
this.emit(Events.EVENT_TARGET_OUTPUT, result.data);
break;
case RecordType.DebuggerLogOutput:
this.emit(Events.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.
*/
private sendCommandToDebugger(command: DebugCommand): void {
var cmdStr: string;
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.
*/
private enqueueCommand(command: DebugCommand): void {
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.
*/
private executeCommand(command: string, token?: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.enqueueCommand(
new DebugCommand(command, token, (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.
*/
private getCommandOutput<T>(command: string, token?: string, transformOutput?: (data: any) => T)
: Promise<T> {
return new Promise<T>((resolve, reject) => {
this.enqueueCommand(
new DebugCommand(command, token, (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.
*/
setExecutableFile(file: string): Promise<void> {
// 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.
*/
setInferiorTerminal(slaveName: string): Promise<void> {
return this.executeCommand('inferior-tty-set ' + slaveName);
}
/**
* Connects the debugger to a remote target.
*
* @param host
* @param port
*/
connectToRemoteTarget(host: string, port: number): Promise<void> {
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.
*/
addBreakpoint(
location: string,
options?: {
isTemp?: boolean;
isHardware?: boolean;
isPending?: boolean;
isDisabled?: boolean;
isTracepoint?: boolean;
condition?: string;
ignoreCount?: number;
threadId?: number;
}
): Promise<IBreakpointInfo> {
var cmd: string = '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<IBreakpointInfo>(cmd + ' ' + location, null, extractBreakpointInfo);
}
/**
* Removes a breakpoint.
*/
removeBreakpoint(breakId: number): Promise<void> {
return this.executeCommand('break-delete ' + breakId);
}
/**
* Removes multiple breakpoints.
*/
removeBreakpoints(breakIds: number[]): Promise<void> {
// 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.
*/
enableBreakpoint(breakId: number): Promise<void> {
return this.executeCommand('break-enable ' + breakId);
}
/**
* Enables multiple breakpoints.
*/
enableBreakpoints(breakIds: number[]): Promise<void> {
return this.executeCommand('break-enable ' + breakIds.join(' '));
}
/**
* Disables a breakpoint.
*/
disableBreakpoint(breakId: number): Promise<void> {
return this.executeCommand('break-disable ' + breakId);
}
/**
* Disables multiple breakpoints.
*/
disableBreakpoints(breakIds: number[]): Promise<void> {
return this.executeCommand('break-disable ' + breakIds.join(' '));
}
/**
* 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.
*/
ignoreBreakpoint(
breakId: number, ignoreCount: number): Promise<IBreakpointInfo> {
return this.getCommandOutput<IBreakpointInfo>(
`break-after ${breakId} ${ignoreCount}`, null, extractBreakpointInfo
);
}
/**
* 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.
*/
setBreakpointCondition(
breakId: number, condition: string): Promise<void> {
return this.executeCommand(`break-condition ${breakId} ${condition}`);
}
//
// Program Execution Commands
//
/**
* Sets the commandline arguments to be passed to the inferior next time it is started
* using [[startInferior]].
*/
setInferiorArguments(args: string): Promise<void> {
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.
*/
startInferior(
options?: { threadGroup?: string; stopAtStart?: boolean}): Promise<void> {
var fullCmd: string = '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.
*/
startAllInferiors(stopAtStart?: boolean): Promise<void> {
var fullCmd: string = 'exec-run --all';
if (stopAtStart) {
fullCmd = fullCmd + ' --start';
}
return this.executeCommand(fullCmd, null);
}
/**
* Kills the currently selected inferior.
*/
abortInferior(): Promise<void> {
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.
*/
resumeInferior(
options?: { threadGroup?: string; reverse?: boolean }): Promise<void> {
var fullCmd: string = '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.
*/
resumeAllInferiors(reverse?: boolean): Promise<void> {
var fullCmd: string = '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.
*/
interruptInferior(threadGroup?: string): Promise<void> {
var fullCmd: string = '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.
*/
interruptAllInferiors(): Promise<void> {
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.
*/
stepIntoLine(options?: { threadId?: number; reverse?: boolean }): Promise<void> {
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.
*/
stepOverLine(options?: { threadId?: number; reverse?: boolean }): Promise<void> {
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.
*/
stepIntoInstruction(
options?: { threadId?: number; reverse?: boolean }): Promise<void> {
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.
*/
stepOverInstruction(
options?: { threadId?: number; reverse?: boolean }): Promise<void> {
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.
*/
stepOut(options?: { threadId?: number; reverse?: boolean }): Promise<void> {
return this.executeCommand(appendExecCmdOptions('exec-finish', options));
}
/**
* Allows to set internal GDB variables
*/
gdbSet(variable: string, value: string): Promise<void> {
return this.executeCommand(`gdb-set ${variable} ${value}`);
}
/**
* Break when the expression changes, behaves like a breakpoint
*/
breakExpression(expression: string): Promise<IBreakpointInfo> {
return this.getCommandOutput<{id: number}>(
`break-watch ${expression}`,
null,
(data: {wpt: {number: string, exp: string}}) => {
return {id: parseInt(data.wpt.number, 10)};
}
);
}
//
// 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.
*/
getStackFrame(
options?: { threadId?: number; frameLevel?: number }): Promise<IStackFrameInfo> {
let 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, (output: any) => {
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.
*/
getStackDepth(
options?: { threadId?: number; maxDepth?: number }): Promise<number> {
var fullCmd: string = '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, (output: any) => {
return parseInt(output.depth, 10);
});
}
/**
* 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.
*/
getStackFrames(
options?: { threadId?: number; lowFrame?: number; highFrame?: number; noFrameFilters?: boolean })
: Promise<IStackFrameInfo[]> {
var fullCmd: string = '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, (output: any) => {
var data = output.stack.frame;
if (Array.isArray(data)) {
return data.map((frame: any) => { 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.
*/
getStackFrameArgs(
detail: VariableDetailLevel,
options?: {
threadId?: number;
noFrameFilters?: boolean;
skipUnavailable?: boolean;
lowFrame?: number;
highFrame?: number;
}
): Promise<IStackFrameArgsInfo[]> {
var fullCmd: string = '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, (output: any) => {
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((frame: any): IStackFrameArgsInfo => {
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.
*/
getStackFrameVariables(
detail: VariableDetailLevel,
options?: {
threadId?: number;
frameLevel: number;
noFrameFilters?: boolean;
skipUnavailable?: boolean;
}
): Promise<IStackFrameVariablesInfo> {
let fullCmd: string = '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, (output: any) => {
let args: IVariableInfo[] = [];
let locals: IVariableInfo[] = [];
output.variables.forEach((varInfo: any) => {
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`.
*/
addWatch(
expression: string,
options?: {
id?: string;
threadId?: number;
threadGroup?: string;
frameLevel?: number;
frameAddress?: string;
isFloating?: boolean;
}
): Promise<IWatchInfo> {
var fullCmd: string = '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, (output: any) => {
return {
id: output.name,
childCount: parseInt(output.numchild, 10),
value: output.value,
expressionType: output['type'],
threadId: parseInt(output['thread-id'], 10),
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.
*/
removeWatch(id: string): Promise<void> {
return this.executeCommand('var-delete ' + id);
}
/**
* Updates the state of an existing watch.
*
* @param id Identifier of the watch to update.
*/
updateWatch(id: string, detail?: VariableDetailLevel): Promise<IWatchUpdateInfo[]> {
var fullCmd: string = 'var-update';
if (detail !== undefined) {
fullCmd = fullCmd + ' ' + detail;
}
fullCmd = fullCmd + ' ' + id;
return this.getCommandOutput(fullCmd, null, (output: any) => {
return output.changelist.map((data: any) => {
return {
id: data.name,
childCount: (data.new_num_children ? parseInt(data.new_num_children, 10) : undefined),
value: data.value,
expressionType: data.new_type,
isInScope: data.in_scope === 'true',
isObsolete: data.in_scope === 'invalid',
hasTypeChanged: data.type_changed === 'true',
isDynamic: data.dynamic === '1',
displayHint: data.displayhint,
hasMoreChildren: data.has_more === '1',
newChildren: data.new_children
};
});
});
}
/**
* Retrieves a list of direct children of the specified watch.
*
* A watch is automatically created for each child that is retrieved (if one doesn't already exist).
* The `from` and `to` options can be used to retrieve a subset of children starting from child
* index `from` and up to (but excluding) child index `to`, note that this currently doesn't work
* on LLDB.
*
* @param id Identifier of the watch whose children should be retrieved.
* @param options.detail One of:
* - [[VariableDetailLevel.None]]: Do not retrieve values of children, this is the default.
* - [[VariableDetailLevel.All]]: Retrieve values for all children.
* - [[VariableDetailLevel.Simple]]: Only retrieve values of children that have a simple type.
* @param options.from Zero-based index of the first child to retrieve, if less than zero the
* range is reset. `to` must also be set in order for this option to have any
* effect.
* @param options.to Zero-based index +1 of the last child to retrieve, if less than zero the
* range is reset. `from` must also be set in order for this option to have any
* effect.
*/
getWatchChildren(
id: string,
options?: {
detail?: VariableDetailLevel;
from?: number;
to?: number;
}): Promise<IWatchChildInfo[]> {
var fullCmd: string = 'var-list-children';
if (options && (options.detail !== undefined)) {
fullCmd = fullCmd + ' ' + options.detail;
}
fullCmd = fullCmd + ' ' + id;
if (options && (options.from !== undefined) && (options.to !== undefined)) {
fullCmd = fullCmd + ' ' + options.from + ' ' + options.to;
}
return this.getCommandOutput(fullCmd, null, (output: any) => {
return extractWatchChildren(output.children);
});
}
/**
* Sets the output format for the value of a watch.
*
* @param id Identifier of the watch for which the format specifier should be set.
* @param formatSpec The output format for the watch value.
* @returns A promise that will be resolved with the value of the watch formatted using the
* provided `formatSpec`.
*/
setWatchValueFormat(id: string, formatSpec: WatchFormatSpec): Promise<string> {
var fullCmd: string = `var-set-format ${id} ` + watchFormatSpecToStringMap.get(formatSpec);
return this.getCommandOutput<string>(fullCmd, null, (output: any) => {
if (output.value) {
return output.value; // GDB-MI
} else {
return output.changelist[0].value; // LLDB-MI
}
});
}
/**
* Evaluates the watch expression and returns the result.
*
* @param id Identifier of the watch whose value should be retrieved.
* @param formatSpec The output format for the watch value.
* @returns A promise that will be resolved with the value of the watch.
*/
getWatchValue(id: string, formatSpec?: WatchFormatSpec): Promise<string> {
var fullCmd: string = 'var-evaluate-expression';
if (formatSpec !== undefined) {
fullCmd = fullCmd + ' -f ' + watchFormatSpecToStringMap.get(formatSpec);
}
fullCmd = fullCmd + ' ' + id;
return this.getCommandOutput(fullCmd, null, (output: any) => {
return output.value;
});
}
/**
* Sets the value of the watch expression to the value of the given expression.
*
* @param id Identifier of the watch whose value should be modified.
* @param expression The value of this expression will be assigned to the watch expression.
* @returns A promise that will be resolved with the new value of the watch.
*/
setWatchValue(id: string, expression: string): Promise<string> {
return this.getCommandOutput(`var-assign ${id} "${expression}"`, null, (output: any) => {
return output.value;
});
}
/**
* Retrives a list of attributes for the given watch.
*
* @param id Identifier of the watch whose attributes should be retrieved.
* @returns A promise that will be resolved with the list of watch attributes.
*/
getWatchAttributes(id: string): Promise<WatchAttribute[]> {
var cmd = 'var-show-attributes ' + id;
return this.getCommandOutput(cmd, null, (output: any) => {
if (output.status) { // LLDB-MI
return [stringToWatchAttributeMap.get(output.status)];
} else if (output.attr) { // GDB-MI
if (Array.isArray(output.attr)) {
return output.attr.map((attr: string) => {
return stringToWatchAttributeMap.get(attr);
});
} else {
return [stringToWatchAttributeMap.get(output.attr)];
}
}
throw new MalformedResponseError(
'Expected to find "status" or "attr", found neither.', output, cmd
);
});
}
/**
* Retrieves an expression that can be evaluated in the current context to obtain the watch value.
*
* @param id Identifier of the watch whose path expression should be retrieved.
* @returns A promise that will be resolved with the path expression of the watch.
*/
getWatchExpression(id: string): Promise<string> {
var cmd = 'var-info-path-expression ' + id;
return this.getCommandOutput(cmd, null, (output: any) => {
if (output.path_expr) {
return output.path_expr;
}
throw new MalformedResponseError('Expected to find "path_expr".', output, cmd);
});
}
//
// Data Inspection & Manipulation
//
/**
* Evaluates the given expression within the target process and returns the result.
*
* The expression may contain function calls, which will be executed synchronously.
*
* @param expression The expression to evaluate.
* @param options.threadId The thread within which the expression should be evaluated.
* *Default*: the currently selected thread.
* @param options.frameLevel The index of the stack frame within which the expression should
* be evaluated, zero for the innermost stack frame. Note that
* if `frameLevel` is specified then `threadId` must also be specified.
* *Default*: the currently selected frame.
* @returns A promise that will be resolved with the value of the expression.
*/
evaluateExpression(
expression: string, options?: { threadId?: number; frameLevel?: number }): Promise<string> {
var fullCmd = 'data-evaluate-expression';
if (options) {
if (options.threadId !== undefined) {
fullCmd = fullCmd + ' --thread ' + options.threadId;
}
if (options.frameLevel !== undefined) {
fullCmd = fullCmd + ' --frame ' + options.frameLevel;
}
}
fullCmd = fullCmd + ` "${expression}"`;
return this.getCommandOutput(fullCmd, null, (output: any) => {
if (output.value) {
return output.value;
}
throw new MalformedResponseError('Expected to find "value".', output, fullCmd);
});
}
/**
* Attempts to read all accessible memory regions in the given range.
*
* @param address Start of the range from which memory should be read, this can be a literal
* address (e.g. `0x00007fffffffed30`) or an expression (e.g. `&someBuffer`) that
* evaluates to the desired address.
* @param numBytesToRead Number of bytes that should be read.
* @param options.byteOffset Offset in bytes relative to `address` from which to begin reading.
* @returns A promise that will be resolved with a list of memory blocks that were read.
*/
readMemory(address: string, numBytesToRead: number, options?: { byteOffset?: number })
: Promise<IMemoryBlock[]> {
var fullCmd = 'data-read-memory-bytes';
if (options && options.byteOffset) {
fullCmd = fullCmd + ' -o ' + options.byteOffset;
}
fullCmd = fullCmd + ` "${address}" ${numBytesToRead}`;
return this.getCommandOutput(fullCmd, null, (output: any) => {
if (output.memory) {
return output.memory;
}
throw new MalformedResponseError('Expected to find "memory".', output, fullCmd);
});
}
/**
* Retrieves a list of register names for the current target.
*
* @param registers List of numbers corresponding to the register names to be retrieved.
* If this argument is omitted all register names will be retrieved.
* @returns A promise that will be resolved with a list of register names.
*/
getRegisterNames(registers?: number[]): Promise<string[]> {
var fullCmd = 'data-list-register-names';
if (registers && (registers.length > 0)) {
fullCmd = fullCmd + ' ' + registers.join(' ');
}
return this.getCommandOutput(fullCmd, null, (output: any) => {
if (output['register-names']) {
return output['register-names'];
}
throw new MalformedResponseError('Expected to find "register-names".', output, fullCmd);
});
}
/**
* Retrieves the values of registers.
*
* @param formatSpec Specifies how the register values should be formatted.
* @param options.registers Register numbers of the registers for which values should be retrieved.
* If this option is omitted the values of all registers will be retrieved.
* @param options.skipUnavailable *(GDB specific)* If `true` only values of available registers
* will be retrieved.
* @param options.threadId Identifier of the thread from which register values should be retrieved.
* If this option is omitted it will default to the currently selected thread.
* NOTE: This option is not currently supported by LLDB-MI.
* @param options.frameLevel Index of the frame from which register values should be retrieved.
* This is a zero-based index, zero corresponds to the innermost frame
* on the stack. If this option is omitted it will default to the
* currently selected frame.
* NOTE: This option is not currently supported by LLDB-MI.
* @returns A promise that will be resolved with a map of register numbers to register values.
*/
getRegisterValues(
formatSpec: RegisterValueFormatSpec,
options?: {
registers?: number[];
skipUnavailable?: boolean;
threadId?: number;
frameLevel?: number
}
): Promise<Map<number, string>> {
var