dbgmits
Version:
Provides the ability to control GDB and LLDB programmatically via GDB/MI.
1,466 lines (1,338 loc) • 93.5 kB
text/typescript
// 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