import createDebugger from 'debug'
import { EventEmitter } from 'events'
import _ from 'highland'
// Custom error class.
import GDBError from './error.js'
// Thread object class.
import Thread from './thread.js'
// Thread group object class.
import ThreadGroup from './group.js'
// Breakpoint object class.
import Breakpoint from './breakpoint.js'
// Frame object class.
import Frame from './frame.js'
// Variable object class.
import Variable from './variable.js'
// Parser for the GDB/MI output syntax.
import { parse as parseMI } from './parsers/gdbmi.pegjs'
// Base class for custom GDB commands.
import baseCommand from './scripts/base.py'
// Command that executes CLI commands and returns them as a string.
import execCommand from './scripts/exec.py'
// Command that lists all symbols (e.g. locals, globals) in the current context.
import contextCommand from './scripts/context.py'
// Command that searches source files using regex.
import sourcesCommand from './scripts/sources.py'
// Command that returns the current thread group.
import groupCommand from './scripts/group.py'
// Command that returns the current thread.
import threadCommand from './scripts/thread.py'
// Base handler for custom GDB events.
import baseEvent from './scripts/event.py'
// Event that emits when new objfile is added.
import objfileEvent from './scripts/objfile.py'
// Debug logging.
let debugOutput = createDebugger('gdb-js:output')
let debugCLIInput = createDebugger('gdb-js:input:cli')
let debugMIInput = createDebugger('gdb-js:input:mi')
let debugCLIResluts = createDebugger('gdb-js:results:cli')
let debugMIResluts = createDebugger('gdb-js:results:mi')
let debugEvents = createDebugger('gdb-js:events')
/**
* Converts string to integer.
*
* @param {string} str The input string.
* @returns {number} The output integer.
*
* @ignore
*/
function toInt (str) {
return parseInt(str, 10)
}
/**
* Escapes symbols in python code so that we can send it using inline mode.
*
* @param {string} script The Python script.
* @returns {string} The escaped python script.
*
* @ignore
*/
function escape (script) {
return script.replace(/\\/g, '\\\\').replace(/\n/g, '\\n')
.replace(/\r/g, '\\r').replace(/\t/g, '\\t').replace(/"/g, '\\"')
}
/**
* Task to execute.
*
* @name Task
* @function
* @returns {Promise<any, GDBError>|any} Whatever.
*
* @ignore
*/
/**
* Class representing a GDB abstraction.
*
* @extends EventEmitter
* @public
*/
class GDB extends EventEmitter {
/**
* Create a GDB wrapper.
*
* @param {object} childProcess A Node.js child process or just an
* object with `stdin`, `stdout`, `stderr` properties that are Node.js streams.
* If you're using GDB all-stop mode, then it should also have implementation of
* `kill` method that is able to send signals (such as `SIGINT`).
*/
constructor (childProcess) {
super()
this._process = childProcess
/**
* The main queue of commands sent to GDB.
*
* @ignore
*/
this._queue = _()
/**
* The mutex to make simultaneous execution of public methods impossible.
*
* @ignore
*/
this._lock = Promise.resolve()
let stream = _(this._process.stdout)
.map((chunk) => chunk.toString())
.splitBy(/\r\n|\n/)
.tap(debugOutput)
.map(parseMI)
// Basically, we're just branching our stream to the messages that should
// be emitted and the results which we then zip with the sent commands.
// Results can be either result records or framed console records.
let results = stream.observe()
.filter((msg) => msg.type === 'result')
.zip(this._queue)
.map((msg) => Object.assign({}, msg[0], msg[1]))
results.fork()
.filter((msg) => msg.state === 'error')
.each((msg) => {
let { data, cmd, reject } = msg
let text = `Error while executing "${cmd}". ${data.msg}`
let err = new GDBError(cmd, text, toInt(data.code))
reject(err)
})
let success = results.fork()
.filter((msg) => msg.state !== 'error')
success.fork()
.filter((msg) => msg.interpreter === 'mi')
.tap((msg) => debugMIResluts(msg.data))
.each((msg) => { msg.resolve(msg.data) })
let commands = stream.observe()
.filter((msg) => msg.type === 'console')
// It's not possible for a command message to be split into multiple
// console records, so we can safely just regex every record.
.map((msg) => /<gdbjs:cmd:[a-z-]+ (.*?) [a-z-]+:cmd:gdbjs>/.exec(msg.data))
.compact()
.map((msg) => JSON.parse(msg[1]))
.tap(debugCLIResluts)
success.observe()
.filter((msg) => msg.interpreter === 'cli')
.zip(commands)
.each((msg) => { msg[0].resolve(msg[1]) })
// Emitting raw async records.
/**
* Raw output of GDB/MI notify records.
* Contains supplementary information that the client should handle.
* Please, see
* {@link https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Async-Records.html|the official GDB/MI documentation}.
*
* @event GDB#notify
* @type {object}
* @property {string} state The class of the notify record (e.g. `thread-created`).
* @property {object} data JSON representation of GDB/MI message.
*/
/**
* Raw output of GDB/MI status records.
* Contains on-going status information about the progress of a slow operation.
*
* @event GDB#status
* @type {object}
* @property {string} state The class of the status record.
* @property {object} data JSON representation of GDB/MI message.
*/
/**
* Raw output of GDB/MI exec records.
* Contains asynchronous state change on the target.
*
* @event GDB#exec
* @type {object}
* @property {string} state The class of the exec record (e.g. `stopped`).
* @property {object} data JSON representation of GDB/MI message.
*/
stream.fork()
.filter((msg) => ['exec', 'notify', 'status'].includes(msg.type))
.each((msg) => { this.emit(msg.type, { state: msg.state, data: msg.data }) })
// Exposing streams of raw stream records.
/**
* Raw output of GDB/MI console records.
*
* @type {Readable<string>}
*/
this.consoleStream = stream.observe()
.filter((msg) => msg.type === 'console')
.map((msg) => msg.data.replace(/<gdbjs:.*?:gdbjs>/g, ''))
/**
* Raw output of GDB/MI log records.
* The log stream contains debugging messages being produced by gdb's internals.
*
* @type {Readable<string>}
*/
this.logStream = stream.observe()
.filter((msg) => msg.type === 'log')
.map((msg) => msg.data)
/**
* Raw output of GDB/MI target records.
* The target output stream contains any textual output from the running target.
* Please, note that it's currently impossible
* to distinguish the target and the MI output correctly due to a bug in GDB/MI. Thus,
* it's recommended to use `--tty` option with your GDB process.
*
* @type {Readable<string>}
*/
this.targetStream = stream.observe()
.filter((msg) => msg.type === 'target')
.map((msg) => msg.data)
// Emitting defined events.
/**
* This event is emitted when target or one of its threads has stopped due to some reason.
* Note that `thread` property indicates the thread that caused the stop. In an all-stop mode
* all threads will be stopped.
*
* @event GDB#stopped
* @type {object}
* @property {string} reason The reason of why target has stopped (see
* {@link https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Async-Records.html|the official GDB/MI documentation}) for more information.
* @property {Thread} [thread] The thread that caused the stop.
* @property {Breakpoint} [breakpoint] Breakpoint is provided if the reason is
* `breakpoint-hit`.
*/
stream.fork()
.filter((msg) => msg.type === 'exec' && msg.state === 'stopped')
.each((msg) => {
let { data } = msg
let thread = data['thread-id']
let event = { reason: data.reason }
if (thread) {
event.thread = new Thread(toInt(thread), {
frame: new Frame({
file: data.frame.fullname,
line: toInt(data.frame.line),
func: data.frame.func
}),
status: 'stopped'
})
}
if (data.reason === 'breakpoint-hit') {
event.breakpoint = new Breakpoint(toInt(data.bkptno))
}
this.emit('stopped', event)
})
/**
* This event is emitted when target changes state to running.
*
* @event GDB#running
* @type {object}
* @property {Thread} [thread] The thread that has changed its state.
* If it's not provided, all threads have changed their states.
*/
stream.fork()
.filter((msg) => msg.type === 'exec' && msg.state === 'running')
.each((msg) => {
let { data } = msg
let thread = data['thread-id']
let event = {}
if (thread !== 'all') {
event.thread = new Thread(toInt(thread), { status: 'running' })
}
this.emit('running', event)
})
/**
* This event is emitted when new thread spawns.
*
* @event GDB#thread-created
* @type {Thread}
*/
/**
* This event is emitted when thread exits.
*
* @event GDB#thread-exited
* @type {Thread}
*/
stream.fork()
.filter((msg) => msg.type === 'notify' &&
['thread-created', 'thread-exited'].includes(msg.state))
.each((msg) => {
let { state, data } = msg
this.emit(state, new Thread(toInt(data.id), {
// GDB/MI stores group id as `i<id>` string.
group: new ThreadGroup(toInt(data['group-id'].slice(1)))
}))
})
/**
* This event is emitted when thread group starts.
*
* @event GDB#thread-group-started
* @type {ThreadGroup}
*/
/**
* This event is emitted when thread group exits.
*
* @event GDB#thread-group-exited
* @type {ThreadGroup}
*/
stream.fork()
.filter((msg) => msg.type === 'notify' &&
['thread-group-started', 'thread-group-exited'].includes(msg.state))
.each((msg) => {
let { state, data } = msg
this.emit(state, new ThreadGroup(toInt(data.id.slice(1)), {
pid: data.pid ? toInt(data.pid) : null
}))
})
/**
* This event is emitted with the full path to executable
* when the new objfile is added.
*
* @event GDB#new-objfile
* @type {string}
*/
stream.fork()
.filter((msg) => msg.type === 'console')
.flatMap((msg) => msg.data.match(/<gdbjs:event:.*?:event:gdbjs>/g) || [])
.map((msg) => /<gdbjs:event:([a-z-]+) (.*?) [a-z-]+:event:gdbjs>/g.exec(msg))
.tap((msg) => debugEvents(msg[1], msg[2]))
.each((msg) => { this.emit(msg[1], msg[2]) })
}
// Public methods.
// Note, that it's really important to not call public methods
// inside other public methods, because it may cause blocking!
/**
* Get the child process object.
*
* @type {object}
* @readonly
*/
get process () {
return this._process
}
/**
* Extend GDB CLI interface with some useful commands that are
* necessary for executing some methods of this GDB wrapper
* (e.g. {@link GDB#context|context}, {@link GDB#execCLI|execCLI}).
* It also enables custom actions (like {@link GDB#new-objfile|`new-objfile` event}).
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
init () {
return this._sync(async () => {
let scripts = [baseCommand, baseEvent, execCommand, contextCommand,
sourcesCommand, groupCommand, threadCommand, objfileEvent]
for (let s of scripts) {
await this._execMI(`-interpreter-exec console "python\\n${escape(s)}"`)
}
})
}
/**
* Set internal GDB variable.
*
* @param {string} param The name of a GDB variable.
* @param {string} value The value of a GDB variable.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
set (param, value) {
return this._sync(() => this._set(param, value))
}
/**
* Enable the `detach-on-fork` option which will automatically
* attach GDB to any of forked processes. Please, note that it makes
* sense only for systems that support `fork` and `vfork` calls.
* It won't work for Windows, for example.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
attachOnFork () {
return this._sync(() => this._set('detach-on-fork', 'off'))
}
/**
* Enable async and non-stop modes in GDB. This mode is *highly* recommended!
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
enableAsync () {
return this._sync(async () => {
try {
await this._set('mi-async', 'on')
} catch (e) {
// For gdb <= 7.7 (which not support `mi-async`).
await this._set('target-async', 'on')
}
await this._set('non-stop', 'on')
this._async = true
})
}
/**
* Attach a new target (inferior) to GDB.
*
* @param {number} pid The process id or to attach.
*
* @returns {Promise<ThreadGroup, GDBError>} A promise that resolves/rejects
* with the added thread group.
*/
attach (pid) {
return this._sync(() => async () => {
let res = await this._execCMD('exec add-inferior')
let id = toInt(/Added inferior (\d+)/.exec(res)[1])
let group = new ThreadGroup(id)
await this._execMI('-target-attach ' + pid, group)
return group
})
}
/**
* Detache a target (inferior) from GDB.
*
* @param {ThreadGroup|number} process The process id or the thread group to detach.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
detach (process) {
return this._sync(() => this._execMI('-target-detach ' +
(process instanceof ThreadGroup ? 'i' + process.id : process)))
}
/**
* Interrupt the target. In all-stop mode and in non-stop mode without arguments
* it interrupts all threads. In non-stop mode it can interrupt only specific thread or
* a thread group.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread-group to interrupt.
* If this parameter is omitted, it will interrupt all threads.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
interrupt (scope) {
return this._sync(() => {
if (!this._async) {
this._process.kill('SIGINT')
} else {
return this._execMI(scope
? '-exec-interrupt' : '-exec-interrupt --all', scope)
}
})
}
/**
* Get the information about all the threads or about specific threads.
*
* @param {Thread|ThreadGroup} [scope] Get information about threads of the specific
* thread group or even about the specific thread (if it doesn't have enough information
* or it's outdated). If this parameter is absent, then information about all
* threads is returned.
*
* @returns {Promise<Thread[]|Thread, GDBError>} A promise that resolves with an array
* of threads or a single thread.
*/
threads (scope) {
return this._sync(async () => {
let mapToThread = (t) => {
let options = { status: t.state }
if (t.frame) {
options.frame = new Frame({
file: t.frame.fullname,
line: toInt(t.frame.line),
level: toInt(t.frame.level),
func: t.frame.func
})
}
return new Thread(toInt(t.id), options)
}
if (scope instanceof Thread) {
let { threads } = await this._execMI('-thread-info ' + scope.id)
return mapToThread(threads[0])
} else if (scope instanceof ThreadGroup) {
let { threads } = await this._execMI(`-list-thread-groups i${scope.id}`)
return threads.map(mapToThread)
} else {
let { threads } = await this._execMI('-thread-info')
return threads.map(mapToThread)
}
})
}
/**
* Get the current thread.
*
* @returns {Promise<Thread, GDBError>} A promise that resolves with a thread.
*/
currentThread () {
return this._sync(() => this._currentThread())
}
/**
* Although you can pass scope to commands, you can also explicitly change
* the context of command execution. Sometimes it might be slightly faster.
*
* @param {Thread} thread The thread that should be selected.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
selectThread (thread) {
return this._sync(() => this._selectThread(thread))
}
/**
* Get thread groups.
*
* @returns {Promise<ThreadGroup[], GDBError>} A promise that resolves with
* an array thread groups.
*/
threadGroups () {
return this._sync(() => this._threadGroups())
}
/**
* Get the current thread group.
*
* @returns {Promise<ThreadGroup, GDBError>} A promise that resolves with the thread group.
*/
currentThreadGroup () {
return this._sync(() => this._currentThreadGroup())
}
/**
* Although you can pass scope to commands, you can also explicitly change
* the context of command execution. Sometimes it might be slightly faster.
*
* @param {ThreadGroup} group The thread group that should be selected.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
selectThreadGroup (group) {
return this._sync(() => this._selectThreadGroup(group))
}
/**
* Insert a breakpoint at the specified position.
*
* @param {string} file The full name or just a file name.
* @param {number|string} pos The function name or a line number.
* @param {Thread} [thread] The thread where breakpoint should be set.
* If this field is absent, breakpoint applies to all threads.
*
* @returns {Promise<Breakpoint, GDBError>} A promise that resolves with a breakpoint.
*/
addBreak (file, pos, thread) {
return this._sync(async () => {
let opt = thread ? '-p ' + thread.id : ''
let { bkpt } = await this._execMI(`-break-insert ${opt} ${file}:${pos}`)
if (Array.isArray(bkpt)) {
return new Breakpoint(toInt(bkpt[0].number), {
file: bkpt[1].fullname,
line: bkpt[1].line,
func: bkpt.map((b) => b.func).filter((f) => !!f),
thread
})
} else {
return new Breakpoint(toInt(bkpt.number), {
file: bkpt.fullname,
line: toInt(bkpt.line),
func: bkpt.func,
thread
})
}
})
}
/**
* Removes a specific breakpoint.
*
* @param {Breakpoint} [bp] The breakpoint.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
removeBreak (bp) {
return this._sync(() => this._execMI('-break-delete ' + bp.id))
}
/**
* Step in.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the stepping should be done.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
stepIn (scope) {
return this._sync(() => this._execMI('-exec-step', scope))
}
/**
* Step back in.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the stepping should be done.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
reverseStepIn (scope) {
return this._sync(() => this._execMI('-exec-step --reverse', scope))
}
/**
* Step out.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the stepping should be done.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
stepOut (scope) {
return this._sync(() => this._execMI('-exec-finish', scope))
}
/**
* Execute to the next line.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the stepping should be done.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
next (scope) {
return this._sync(() => this._execMI('-exec-next', scope))
}
/**
* Execute to the previous line.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the stepping should be done.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
reverseNext (scope) {
return this._sync(() => this._execMI('-exec-next --reverse', scope))
}
/**
* Run the current target.
*
* @param {ThreadGroup} [group] The thread group to run.
* If this parameter is omitted, current thread group will be run.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
run (group) {
// XXX: seems like MI command `-exec-run` has a bug that makes it
// run in the foreground mode (although the opposite is stated in the docs).
// This can cause blocking even in `target-async` mode.
return this._sync(() => this._async ? this._execCMD('exec run&', group)
: this._execMI('-exec-run', group))
}
/**
* Continue execution.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group that should be continued.
* If this parameter is omitted, all threads are continued.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
proceed (scope) {
return this._sync(() => this._execMI(scope
? '-exec-continue' : '-exec-continue --all', scope))
}
/**
* Continue reverse execution.
*
* @param {Thread|ThreadGroup} [scope] The thread or thread group that should be continued.
* If this parameter is omitted, all threads are continued.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
reverseProceed (scope) {
return this._sync(() => this._execMI(scope
? '-exec-continue --reverse' : '-exec-continue --all --reverse', scope))
}
/**
* List all symbols in the current context (i.e. all global, static, local
* variables and constants in the current file).
*
* @param {Thread} [thread] The thread from which the context should be taken.
*
* @returns {Promise<Variable[], GDBError>} A promise that resolves with
* an array of variables.
*/
context (thread) {
return this._sync(async () => {
let res = await this._execCMD('context', thread)
return res.map((v) => new Variable(v))
})
}
/**
* Get the callstack.
*
* @param {Thread} [thread] The thread from which the callstack should be taken.
*
* @returns {Promise<Frame[], GDBError>} A promise that resolves with an array of frames.
*/
callstack (thread) {
return this._sync(async () => {
let { stack } = await this._execMI('-stack-list-frames', thread)
return stack.map((f) => new Frame({
file: f.value.fullname,
line: toInt(f.value.line),
level: toInt(f.value.level),
func: f.value.func
}))
})
}
/**
* Get list of source files or a subset of source files that match
* the regular expression. Please, note that it doesn't return sources.
*
* @example
* let headers = await gdb.sourceFiles({ pattern: '\.h$' })
*
* @param {object} [options] The options object.
* @param {ThreadGroup} [options.group] The thread group (i.e. target) for
* which source files are needed. If this parameter is absent, then
* source files are returned for all targets.
* @param {string} [options.pattern] The regular expression (see
* {@link https://docs.python.org/2/library/re.html|Python regex syntax}).
* This option is useful when the project has a lot of files so that
* it's not desirable to send them all in one chunk along the wire.
*
* @returns {Promise<string[], GDBError>} A promise that resolves with
* an array of source files.
*/
sourceFiles (options = {}) {
return this._sync(async () => {
let files = []
let group = options.group
let pattern = options.pattern || ''
let cmd = 'sources ' + pattern
if (group) {
files = await this._execCMD(cmd, group)
} else {
let groups = await this._threadGroups()
for (let g of groups) {
files = files.concat(await this._execCMD(cmd, g))
}
files = files.filter((f, index) => files.indexOf(f) === index)
}
return files
})
}
/**
* Evaluate a GDB expression.
*
* @param {string} expr The expression to evaluate.
* @param {Thread|ThreadGroup} [scope] The thread or thread group where
* the expression should be evaluated.
*
* @returns {Promise<string, GDBError>} A promise that resolves with the result of expression.
*/
evaluate (expr, scope) {
return this._sync(async () => {
let res = await this._execMI('-data-evaluate-expression ' + expr, scope)
return res.value
})
}
/**
* Exit GDB.
*
* @returns {Promise<undefined, GDBError>} A promise that resolves/rejects
* after completion of a GDB command.
*/
exit () {
return this._sync(() => this._execMI('-gdb-exit'))
}
/**
* Execute a custom python script and get the results of its excecution.
* If your python script is asynchronous and you're interested in its output, you should
* either define a new event (refer to the *Extending* section in the main page) or
* read the {@link GDB#consoleStream|console stream}. Here's the example below.
*
* By the way, with this method you can define your own CLI commands and then call
* them via {@link GDB#execCLI|execCLI} method. For more examples, refer to the *Extending*
* section on the main page and read
* {@link https://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html|official GDB Python API}
* and {@link https://sourceware.org/gdb/wiki/PythonGdbTutorial|PythonGdbTutorial}.
*
* @example
* let script = `
* import gdb
* import threading
*
*
* def foo():
* sys.stdout.write('bar')
* sys.stdout.flush()
*
* timer = threading.Timer(5.0, foo)
* timer.start()
* `
* gdb.consoleStream.on('data', (str) => {
* if (str === 'bar') console.log('yep')
* })
* await gdb.execPy(script)
*
* @param {string} src The python script.
* @param {Thread} [thread] The thread where the script should be executed.
*
* @returns {Promise<string, GDBError>} A promise that resolves with the output of
* python script execution.
*/
execPy (src, scope) {
return this._sync(() => this._execCMD(`exec python\\n${escape(src)}`, scope))
}
/**
* Execute a CLI command.
*
* @param {string} cmd The CLI command.
* @param {Thread|ThreadGroup} [scope] The thread where the command should be executed.
*
* @returns {Promise<string, GDBError>} A promise that resolves with
* the result of command execution.
*/
execCLI (cmd, scope) {
return this._sync(() => this._execCMD(`exec ${cmd}`, scope))
}
/**
* Execute a custom defined command. Refer to the *Extending* section on the main
* page of the documentation.
*
* @param {string} cmd The name of the command.
* @param {Thread|ThreadGroup} [scope] The thread or thread-group where
* the command should be executed. If this parameter is omitted,
* it executes in the current thread.
*
* @returns {Promise<object, GDBError>} A promise that resolves with
* the JSON representation of the result of command execution.
*/
execCMD (cmd, scope) {
return this._sync(() => this._execCMD(cmd, scope))
}
/**
* Execute a MI command.
*
* @param {string} cmd The MI command.
* @param {Thread|ThreadGroup} [scope] The thread or thread-group where
* the command should be executed. If this parameter is omitted,
* it executes in the current thread.
*
* @returns {Promise<object, GDBError>} A promise that resolves with
* the JSON representation of the result of command execution.
*/
execMI (cmd, scope) {
return this._sync(() => this._execMI(cmd, scope))
}
// Private methods
// Note that it's necessary to not call public methods and {@link GDB#_sync}
// method in these methods since it may cause blocking.
/**
* Internal method for setting values. See {@link GDB#set}.
*
* @ignore
*/
async _set (param, value) {
await this._execMI(`-gdb-set ${param} ${value}`)
}
/**
* Internal method for getting the current thread. See {@link GDB#currentThread}.
*
* @ignore
*/
async _currentThread () {
let { id, group } = await this._execCMD('thread')
return id ? new Thread(id, { group }) : null
}
/**
* Internal method for getting the current thread group. See {@link GDB#currentThreadGroup}.
*
* @ignore
*/
async _currentThreadGroup () {
let { id, pid } = await this._execCMD('group')
return new ThreadGroup(id, { pid })
}
/**
* Internal method for selecting the thread groups. See {@link GDB#selectThread}.
*
* @ignore
*/
async _selectThread (thread) {
await this._execMI('-thread-select ' + thread.id)
}
/**
* Internal method for selecting the thread group. See {@link GDB#selectThreadGroup}.
*
* @ignore
*/
async _selectThreadGroup (group) {
await this._execCMD('exec inferior ' + group.id)
}
/**
* Internal method for getting thread groups. See {@link GDB#threadGroups}.
*
* @ignore
*/
async _threadGroups () {
let { groups } = await this._execMI('-list-thread-groups')
return groups.map((g) => new ThreadGroup(toInt(g.id.slice(1)), {
pid: toInt(g.pid),
executable: g.executable
}))
}
/**
* Helps to restore the current thread between operations and avoid side effect.
*
* @param {Task} [task] The task to execute.
*
* @returns {Promise<any, GDBError>} A promise that resolves with task results.
*
* @ignore
*/
async _preserveThread (task) {
let thread = await this._currentThread()
let res = await task()
if (thread) await this._selectThread(thread)
return res
}
/**
* Internal method for calling defined Python commands. See {@link GDB#execCMD}.
*
* @ignore
*/
_execCMD (cmd, scope) {
if (scope instanceof Thread) {
return this._preserveThread(() =>
this._selectThread(scope).then(() => this._exec(cmd, 'cli')))
} else if (scope instanceof ThreadGroup) {
return this._preserveThread(() =>
this._selectThreadGroup(scope).then(() => this._exec(cmd, 'cli')))
} else {
return this._exec(cmd, 'cli')
}
}
/**
* Internal method for calling MI commands. See {@link GDB#execMI}.
*
* @ignore
*/
_execMI (cmd, scope) {
let [, name, options] = /([^ ]+)( .*|)/.exec(cmd)
if (scope instanceof Thread) {
return this._exec(`${name} --thread ${scope.id} ${options}`, 'mi')
} else if (scope instanceof ThreadGroup) {
// `--thread-group` option changes thread.
return this._preserveThread(() =>
this._exec(`${name} --thread-group i${scope.id} ${options}`, 'mi'))
} else {
return this._exec(cmd, 'mi')
}
}
/**
* Internal method that executes a MI command and add it to the queue where it
* waits for the results of execution.
*
* @param {string} cmd The command (eaither a MI or a defined Python command).
* @param {string} interpreter The interpreter that should execute the command.
*
* @returns {Promise<object, GDBError>} A promise that resolves with
* the JSON representation of the result of command execution.
*
* @ignore
*/
_exec (cmd, interpreter) {
if (interpreter === 'mi') {
debugMIInput(cmd)
} else {
debugCLIInput(`gdbjs-${cmd}`)
cmd = `-interpreter-exec console "gdbjs-${cmd}"`
}
this._process.stdin.write(cmd + '\n', { binary: true })
return new Promise((resolve, reject) => {
this._queue.write({ cmd, interpreter, resolve, reject })
})
}
/**
* This routine makes it impossible to run multiple punlic methods
* simultaneously. Why this matter? It's really important for public
* methods to not interfere with each other, because they can change
* the state of GDB during execution. They should be atomic,
* meaning that calling them simultaneously should produce the same
* results as calling them in order. One way to ensure that is to block
* execution of public methods until other methods complete.
*
* @param {Task} task The task to execute.
*
* @returns {Promise<any, GDBError>} A promise that resolves with task results.
*
* @ignore
*/
_sync (task) {
this._lock = this._lock.then(task, task)
return this._lock
}
}
export { GDB, Thread, ThreadGroup, Breakpoint,
Frame, Variable, parseMI as _parseMI }