UNPKG

dbgmits

Version:

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

1,466 lines (1,338 loc) 93.5 kB
// Copyright (c) 2015 Vadim Macagon // MIT License, see LICENSE file for full terms. /// <reference path="../typings/lib-tsd.d.ts" /> import { spawn, ChildProcess } from 'child_process'; import * as readline from 'readline'; import * as os from 'os'; import * as path from 'path'; import * as events from 'events'; import * as stream from 'stream'; import * as parser from './mi_output_parser'; import { RecordType } from './mi_output'; import * as pty from 'pty.js'; import * as bunyan from 'bunyan'; // 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; } } /** Frame-specific information returned by breakpoint and stepping MI commands. */ export interface IFrameInfo { /** Name of the function corresponding to the frame. */ func?: string; /** Arguments of the function corresponding to the frame. */ args?: any; /** Code address of the frame. */ address: string; /** Name of the source file corresponding to the frame's code address. */ filename?: string; /** Full path of the source file corresponding to the frame's code address. */ fullname?: string; /** Source line corresponding to the frame's code address. */ line?: number; } /** Frame-specific information returned by stack related MI commands. */ export interface IStackFrameInfo { /** Level of the stack frame, zero for the innermost frame. */ level: number; /** Name of the function corresponding to the frame. */ func?: string; /** Code address of the frame. */ address: string; /** Name of the source file corresponding to the frame's code address. */ filename?: string; /** Full path of the source file corresponding to the frame's code address. */ fullname?: string; /** Source line corresponding to the frame's code address. */ line?: number; /** Name of the binary file that corresponds to the frame's code address. */ from?: string; } /** Breakpoint-specific information returned by various MI commands. */ export interface IBreakpointInfo { id: string; breakpointType: string; catchpointType?: string; isTemp?: boolean; isEnabled?: boolean; address?: string; func?: string; filename?: string; fullname?: string; line?: number; at?: string; pending?: string; evaluatedBy?: string; threadId?: number; condition?: string; ignoreCount?: number; enableCount?: number; mask?: string; passCount?: number; originalLocation?: string; hitCount?: number; isInstalled?: boolean; what?: string; } /** * Used to indicate failure of a MI command sent to the debugger. */ export class CommandFailedError implements Error { /** The name of this error class. */ name: string; /** The error message sent back by the debugger. */ message: string; /** Optional error code sent by the debugger. */ code: string; /** The command text that was sent to the debugger (minus token and dash prefix). */ command: string; /** Optional token for the failed command (if the command had one). */ token: string; constructor(message: string, command: string, code?: string, token?: string) { this.name = "CommandFailedError"; this.message = message; this.code = code; this.command = command; this.token = token; } } /** * Used to indicate the response to an MI command didn't match the expected format. */ export class MalformedResponseError implements Error { /** The name of this error class. */ name: string; /** * @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). */ constructor( public message: string, public response: string, public command?: string, public token?: string) { this.name = "MalformedResponseError"; } } export interface ThreadGroupAddedNotify { id: string; } export interface ThreadGroupRemovedNotify { id: string; } export interface ThreadGroupStartedNotify { id: string; pid: string; } export interface ThreadGroupExitedNotify { id: string; exitCode: string; } export interface ThreadCreatedNotify { id: string; groupId: string; } export interface ThreadExitedNotify { id: string; groupId: string; } export interface ThreadSelectedNotify { id: string; } /** Notification sent whenever a library is loaded or unloaded by an inferior. */ interface LibNotify { id: string; /** Name of the library file on the target system. */ targetName: string; /** * Name of the library file on the host system. * When debugging locally this should be the same as `targetName`. */ hostName: string; /** * Optional identifier of the thread group within which the library was loaded. */ threadGroup: string; /** * Optional load address. * This field is not part of the GDB MI spec. and is only set by LLDB MI driver. */ loadAddress: string; /** * Optional path to a file containing additional debug information. * This field is not part of the GDB MI spec. and is only set by LLDB MI driver. * The LLDB MI driver gets the value for this field from SBModule::GetSymbolFileSpec(). */ symbolsPath: string; } export interface LibLoadedNotify extends LibNotify { } export interface LibUnloadedNotify extends LibNotify { } export enum TargetStopReason { /** A breakpoint was hit. */ BreakpointHit, /** A step instruction finished. */ EndSteppingRange, /** A step-out instruction finished. */ FunctionFinished, /** The target finished executing and terminated normally. */ ExitedNormally, /** The target was signalled. */ SignalReceived, /** The target encountered an exception (this is LLDB specific). */ ExceptionReceived, /** Catch-all for any of the other numerous reasons. */ Unrecognized } export interface TargetStoppedNotify { reason: TargetStopReason; /** Identifier of the thread that caused the target to stop. */ threadId: number; /** * Identifiers of the threads that were stopped, * if all threads were stopped this array will be empty. */ stoppedThreads: number[]; /** Processor core on which the stop event occured. */ processorCore?: string; } export interface BreakpointHitNotify extends TargetStoppedNotify { breakpointId: number; frame: IFrameInfo; } export interface StepFinishedNotify extends TargetStoppedNotify { frame: IFrameInfo; } export interface StepOutFinishedNotify extends TargetStoppedNotify { frame: IFrameInfo; resultVar?: string; returnValue?: string; } export interface SignalReceivedNotify extends TargetStoppedNotify { signalCode?: string; signalName?: string; signalMeaning?: string; } export interface ExceptionReceivedNotify extends TargetStoppedNotify { exception: string; } export interface IVariableInfo { /** Variable name. */ name: string; /** String representation of the value of the variable. */ value?: string; /** Type of the variable. */ type?: string; } /** Contains information about the arguments of a stack frame. */ export interface IStackFrameArgsInfo { /** Index of the frame on the stack, zero for the innermost frame. */ level: number; /** List of arguments for the frame. */ args: IVariableInfo[]; } /** Contains information about the arguments and locals of a stack frame. */ export interface IStackFrameVariablesInfo { args: IVariableInfo[]; locals: IVariableInfo[]; } /** Indicates how much information should be retrieved when calling * [[DebugSession.getLocalVariables]]. */ export enum VariableDetailLevel { /** Only variable names will be retrieved, not their types or values. */ None = 0, // specifying the value is redundant, but is used here to emphasise its importance /** Only variable names and values will be retrieved, not their types. */ All = 1, /** * 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). */ Simple = 2 } /** Contains information about a newly created watch. */ export interface IWatchInfo { id: string; childCount: number; value: string; expressionType: string; threadId: number; isDynamic: boolean; displayHint: string; hasMoreChildren: boolean; } export interface IWatchChildInfo extends IWatchInfo { /** The expression the front-end should display to identify this child. */ expression: string; /** `true` if the watch state is not implicitely updated. */ isFrozen: boolean; } /** Contains information about the changes in the state of a watch. */ export interface IWatchUpdateInfo { /** Unique identifier of the watch whose state changed. */ id: string; /** * If the number of children changed this is the updated count, * otherwise this field is undefined. */ childCount?: number; /** The value of the watch expression after the update. */ value?: string; /** * If the type of the watch expression changed this will be the new type, * otherwise this field is undefined. */ expressionType?: string; /** * If `true` the watch expression is in-scope and has a valid value after the update. * If `false' the watch expression is not in-scope and has no valid value, but if [[isObsolete]] * is likewise `false` then the value may become valid at some point in the future if the watch * expression comes back into scope. */ isInScope: boolean; /** * `true` if the value of the watch expression is permanently unavailable, possibly because * the target has changed or has been recompiled. Obsolete watches should be removed by the * front-end. */ isObsolete: boolean; /** `true` iff the value if the type of the watch expression has changed. */ hasTypeChanged?: boolean; /** `true` iff the watch relies on a Python-based visualizer. */ isDynamic?: boolean; /** * If `isDynamic` is `true` this field may contain a hint for the front-end on how the value of * the watch expression should be displayed. Otherwise this field is undefined. */ displayHint?: string; /** `true` iff there are more children outside the update range. */ hasMoreChildren: boolean; /** * If `isDynamic` is `true` and new children were added within the update range this will * be a list of those new children. Otherwise this field is undefined. */ newChildren?: string; } /** Output format specifiers for watch values. */ export enum WatchFormatSpec { Binary, Decimal, Hexadecimal, 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. */ Default } /** A watch may have one or more of these attributes associated with it. */ export enum WatchAttribute { /** Indicates the watch value can be modified. */ 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*). */ NonEditable } /** Contains the contents of a block of memory from the target process. */ export interface IMemoryBlock { /** Start address of the memory block (hex literal). */ begin: string; /** End address of the memory block (hex literal). */ end: string; /** * Offset of the memory block (in bytes, as a hex literal) from the start address passed into * [[DebugSession.readMemory]]. */ offset: string; /** Contents of the memory block in hexadecimal. */ contents: string; } /** Contains information about an ASM instruction. */ export interface IAsmInstruction { /** Address at which this instruction was disassembled. */ address: string; /** Name of the function this instruction came from. */ func: string; /** Offset of this instruction from the start of `func` (as a decimal). */ offset: number; /** Text disassembly of this instruction. */ inst: string; /** * Raw opcode bytes for this instruction. * NOTE: This field is currently not filled in by LLDB-MI. */ opcodes?: string; /** * Size of the raw opcode in bytes. * NOTE: This field is an LLDB-MI specific extension. */ size?: number; } /** Contains ASM instructions for a single source line. */ export interface ISourceLineAsm { /** Source filename from the compilation unit, may be absolute or relative. */ file: string; /** * Absolute filename of `file` (with all symbolic links resolved). * If the source file can't be found this field will populated from the debug information. * NOTE: This field is currently not filled in by LLDB-MI. */ fullname: string; /** Source line number in `file`. */ line: number; /** ASM instructions corresponding to `line` in `file`. */ instructions: IAsmInstruction[]; } /** Output format specifiers for register values. */ export enum RegisterValueFormatSpec { Binary, Decimal, Hexadecimal, Octal, Raw, /** * This specifier is used to indicate that one of the other ones should be automatically chosen. */ Default } /** * 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 class DebugSession extends events.EventEmitter { /** * Emitted when a thread group is added by the debugger, it's possible the thread group * hasn't yet been associated with a running program. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadGroupAddedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_GROUP_ADDED: string = 'thdgrpa'; /** * Emitted when a thread group is removed by the debugger. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadGroupRemovedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_GROUP_REMOVED: string = 'thdgrpr'; /** * Emitted when a thread group is associated with a running program, * either because the program was started or the debugger was attached to it. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadGroupStartedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_GROUP_STARTED: string = 'thdgrps'; /** * Emitted when a thread group ceases to be associated with a running program, * either because the program terminated or the debugger was dettached from it. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadGroupExitedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_GROUP_EXITED: string = 'thdgrpe'; /** * Emitted when a thread is created. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadCreatedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_CREATED: string = 'thdc'; /** * Emitted when a thread exits. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadExitedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_EXITED: string = 'thde'; /** * Emitted when the debugger changes the current thread selection. * * Listener function should have the signature: * ~~~ * (notification: [[ThreadSelectedNotify]]) => void * ~~~ * @event */ static EVENT_THREAD_SELECTED: string = 'thds'; /** * Emitted when a new library is loaded by the program being debugged. * * Listener function should have the signature: * ~~~ * (notification: [[LibLoadedNotify]]) => void * ~~~ * @event */ static EVENT_LIB_LOADED: string = 'libload'; /** * Emitted when a library is unloaded by the program being debugged. * * Listener function should have the signature: * ~~~ * (notification: [[LibUnloadedNotify]]) => void * ~~~ * @event */ static EVENT_LIB_UNLOADED: string = 'libunload'; /** * Emitted when some console output from the debugger becomes available, * usually in response to a CLI command. * * Listener function should have the signature: * ~~~ * (output: string) => void * ~~~ * @event */ static EVENT_DBG_CONSOLE_OUTPUT: string = 'conout'; /** * Emitted when some console output from the target becomes available. * * Listener function should have the signature: * ~~~ * (output: string) => void * ~~~ * @event */ static EVENT_TARGET_OUTPUT: string = 'targetout'; /** * Emitted when log output from the debugger becomes available. * * Listener function should have the signature: * ~~~ * (output: string) => void * ~~~ * @event */ static EVENT_DBG_LOG_OUTPUT: string = 'dbgout'; /** * Emitted when the target starts running. * * The `threadId` passed to the listener indicates which specific thread is now running, * a value of **"all"** indicates all threads are running. According to the GDB/MI spec. * no interaction with a running thread is possible after this notification is produced until * it is stopped again. * * Listener function should have the signature: * ~~~ * (threadId: string) => void * ~~~ * @event */ static EVENT_TARGET_RUNNING: string = 'targetrun'; /** * Emitted when the target stops running. * * Listener function should have the signature: * ~~~ * (notification: [[TargetStoppedNotify]]) => void * ~~~ * @event */ static EVENT_TARGET_STOPPED: string = 'targetstop'; /** * Emitted when the target stops running because a breakpoint was hit. * * Listener function should have the signature: * ~~~ * (notification: [[BreakpointHitNotify]]) => void * ~~~ * @event */ static EVENT_BREAKPOINT_HIT: string = 'brkpthit'; /** * Emitted when the target stops due to a stepping operation finishing. * * Listener function should have the signature: * ~~~ * (notification: [[StepFinishedNotify]]) => void * ~~~ * @event */ static EVENT_STEP_FINISHED: string = 'endstep'; /** * Emitted when the target stops due to a step-out operation finishing. * * NOTE: Currently this event will not be emitted by LLDB-MI, it will only be emitted by GDB-MI, * so for the time being use [[EVENT_STEP_FINISHED]] with LLDB-MI. * * Listener function should have the signature: * ~~~ * (notification: [[StepOutFinishedNotify]]) => void * ~~~ * @event */ static EVENT_FUNCTION_FINISHED: string = 'endfunc'; /** * Emitted when the target stops running because it received a signal. * * Listener function should have the signature: * ~~~ * (notification: [[SignalReceivedNotify]]) => void * ~~~ * @event */ static EVENT_SIGNAL_RECEIVED: string = 'signal'; /** * Emitted when the target stops running due to an exception. * * Listener function should have the signature: * ~~~ * (notification: [[ExceptionReceivedNotify]]) => void * ~~~ * @event */ static EVENT_EXCEPTION_RECEIVED: string = 'exception'; // 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) { 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: TargetStoppedNotify = { 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: BreakpointHitNotify = { 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: StepFinishedNotify = { 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: StepOutFinishedNotify = { 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: SignalReceivedNotify = { 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: ExceptionReceivedNotify = { 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; } } private emitAsyncNotification(name: string, data: any) { var shlibInfo: any; 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. */ 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(DebugSession.EVENT_DBG_CONSOLE_OUTPUT, result.data); break; case RecordType.TargetOutput: this.emit(DebugSession.EVENT_TARGET_OUTPUT, result.data); break; case 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. */ 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, (output: any) => { return extractBreakpointInfo(output); }); } /** * 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); } /** * Disables a breakpoint. */ disableBreakpoint(breakId: number): Promise<void> { 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. */ ignoreBreakpoint( breakId: number, ignoreCount: number): Promise<IBreakpointInfo> { return this.getCommandOutput<IBreakpointInfo>(`break-after ${breakId} ${ignoreCount}`, null, (output: any) => { 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. */ 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 target process next time it is started * using [[startTarget]]. */ setTargetArguments(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)); } // // 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