diff options
Diffstat (limited to 'src/civetweb/src/third_party/duktape-1.5.2/debugger/duk_debug.js')
-rw-r--r-- | src/civetweb/src/third_party/duktape-1.5.2/debugger/duk_debug.js | 2473 |
1 files changed, 2473 insertions, 0 deletions
diff --git a/src/civetweb/src/third_party/duktape-1.5.2/debugger/duk_debug.js b/src/civetweb/src/third_party/duktape-1.5.2/debugger/duk_debug.js new file mode 100644 index 00000000..ab0623fa --- /dev/null +++ b/src/civetweb/src/third_party/duktape-1.5.2/debugger/duk_debug.js @@ -0,0 +1,2473 @@ +/* + * Minimal debug web console for Duktape command line tool + * + * See debugger/README.rst. + * + * The web UI socket.io communication can easily become a bottleneck and + * it's important to ensure that the web UI remains responsive. Basic rate + * limiting mechanisms (token buckets, suppressing identical messages, etc) + * are used here now. Ideally the web UI would pull data on its own terms + * which would provide natural rate limiting. + * + * Promises are used to structure callback chains. + * + * https://github.com/petkaantonov/bluebird + * https://github.com/petkaantonov/bluebird/blob/master/API.md + * https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns + */ + +var Promise = require('bluebird'); +var events = require('events'); +var stream = require('stream'); +var path = require('path'); +var fs = require('fs'); +var net = require('net'); +var byline = require('byline'); +var util = require('util'); +var readline = require('readline'); +var sprintf = require('sprintf').sprintf; +var utf8 = require('utf8'); +var wrench = require('wrench'); // https://github.com/ryanmcgrath/wrench-js +var yaml = require('yamljs'); + +// Command line options (defaults here, overwritten if necessary) +var optTargetHost = '127.0.0.1'; +var optTargetPort = 9091; +var optHttpPort = 9092; +var optJsonProxyPort = 9093; +var optJsonProxy = false; +var optSourceSearchDirs = [ '../tests/ecmascript' ]; +var optDumpDebugRead = null; +var optDumpDebugWrite = null; +var optDumpDebugPretty = null; +var optLogMessages = false; + +// Constants +var UI_MESSAGE_CLIPLEN = 128; +var LOCALS_CLIPLEN = 64; +var EVAL_CLIPLEN = 4096; +var GETVAR_CLIPLEN = 4096; + +// Commands initiated by Duktape +var CMD_STATUS = 0x01; +var CMD_PRINT = 0x02; +var CMD_ALERT = 0x03; +var CMD_LOG = 0x04; +var CMD_THROW = 0x05; +var CMD_DETACHING = 0x06; + +// Commands initiated by the debug client (= us) +var CMD_BASICINFO = 0x10; +var CMD_TRIGGERSTATUS = 0x11; +var CMD_PAUSE = 0x12; +var CMD_RESUME = 0x13; +var CMD_STEPINTO = 0x14; +var CMD_STEPOVER = 0x15; +var CMD_STEPOUT = 0x16; +var CMD_LISTBREAK = 0x17; +var CMD_ADDBREAK = 0x18; +var CMD_DELBREAK = 0x19; +var CMD_GETVAR = 0x1a; +var CMD_PUTVAR = 0x1b; +var CMD_GETCALLSTACK = 0x1c; +var CMD_GETLOCALS = 0x1d; +var CMD_EVAL = 0x1e; +var CMD_DETACH = 0x1f; +var CMD_DUMPHEAP = 0x20; +var CMD_GETBYTECODE = 0x21; + +// Errors +var ERR_UNKNOWN = 0x00; +var ERR_UNSUPPORTED = 0x01; +var ERR_TOOMANY = 0x02; +var ERR_NOTFOUND = 0x03; + +// Marker objects for special protocol values +var DVAL_EOM = { type: 'eom' }; +var DVAL_REQ = { type: 'req' }; +var DVAL_REP = { type: 'rep' }; +var DVAL_ERR = { type: 'err' }; +var DVAL_NFY = { type: 'nfy' }; + +// String map for commands (debug dumping). A single map works (instead of +// separate maps for each direction) because command numbers don't currently +// overlap. So merge the YAML metadata. +var debugCommandMeta = yaml.load('duk_debugcommands.yaml'); +var debugCommandNames = []; // list of command names, merged client/target +debugCommandMeta.target_commands.forEach(function (k, i) { + debugCommandNames[i] = k; +}); +debugCommandMeta.client_commands.forEach(function (k, i) { // override + debugCommandNames[i] = k; +}); +var debugCommandNumbers = {}; // map from (merged) command name to number +debugCommandNames.forEach(function (k, i) { + debugCommandNumbers[k] = i; +}); + +// Duktape heaphdr type constants, must match C headers +var DUK_HTYPE_STRING = 1; +var DUK_HTYPE_OBJECT = 2; +var DUK_HTYPE_BUFFER = 3; + +// Duktape internal class numbers, must match C headers +var dukClassNameMeta = yaml.load('duk_classnames.yaml'); +var dukClassNames = dukClassNameMeta.class_names; + +// Bytecode opcode/extraop metadata +var dukOpcodes = yaml.load('duk_opcodes.yaml'); +if (dukOpcodes.opcodes.length != 64) { + throw new Error('opcode metadata length incorrect'); +} +if (dukOpcodes.extra.length != 256) { + throw new Error('extraop metadata length incorrect'); +} + +/* + * Miscellaneous helpers + */ + +var nybbles = '0123456789abcdef'; + +/* Convert a buffer into a string using Unicode codepoints U+0000...U+00FF. + * This is the NodeJS 'binary' encoding, but since it's being deprecated, + * reimplement it here. We need to avoid parsing strings as e.g. UTF-8: + * although Duktape strings are usually UTF-8/CESU-8 that's not always the + * case, e.g. for internal strings. Buffer values are also represented as + * strings in the debug protocol, so we must deal accurately with arbitrary + * byte arrays. + */ +function bufferToDebugString(buf) { + var cp = []; + var i, n; + +/* + // This fails with "RangeError: Maximum call stack size exceeded" for some + // reason, so use a much slower variant. + + for (i = 0, n = buf.length; i < n; i++) { + cp[i] = buf[i]; + } + + return String.fromCharCode.apply(String, cp); +*/ + + for (i = 0, n = buf.length; i < n; i++) { + cp[i] = String.fromCharCode(buf[i]); + } + + return cp.join(''); +} + +/* Write a string into a buffer interpreting codepoints U+0000...U+00FF + * as bytes. Drop higher bits. + */ +function writeDebugStringToBuffer(str, buf, off) { + var i, n; + + for (i = 0, n = str.length; i < n; i++) { + buf[off + i] = str.charCodeAt(i) & 0xff; // truncate higher bits + } +} + +/* Encode an ordinary Unicode string into a dvalue compatible format, i.e. + * into a byte array represented as codepoints U+0000...U+00FF. Concretely, + * encode with UTF-8 and then represent the bytes with U+0000...U+00FF. + */ +function stringToDebugString(str) { + return utf8.encode(str); +} + +/* Pretty print a dvalue. Useful for dumping etc. */ +function prettyDebugValue(x) { + if (typeof x === 'object' && x !== null) { + if (x.type === 'eom') { + return 'EOM'; + } else if (x.type === 'req') { + return 'REQ'; + } else if (x.type === 'rep') { + return 'REP'; + } else if (x.type === 'err') { + return 'ERR'; + } else if (x.type === 'nfy') { + return 'NFY'; + } + } + return JSON.stringify(x); +} + +/* Pretty print a number for UI usage. Types and values should be easy to + * read and typing should be obvious. For numbers, support Infinity, NaN, + * and signed zeroes properly. + */ +function prettyUiNumber(x) { + if (x === 1 / 0) { return 'Infinity'; } + if (x === -1 / 0) { return '-Infinity'; } + if (Number.isNaN(x)) { return 'NaN'; } + if (x === 0 && 1 / x > 0) { return '0'; } + if (x === 0 && 1 / x < 0) { return '-0'; } + return x.toString(); +} + +/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF) + * for UI usage. Try UTF-8 decoding to get a nice Unicode string (JSON + * encoded) but if that fails, ensure that bytes are encoded transparently. + * The result is a quoted string with a special quote marker for a "raw" + * string when UTF-8 decoding fails. Very long strings are optionally + * clipped. + */ +function prettyUiString(x, cliplen) { + var ret; + + if (typeof x !== 'string') { + throw new Error('invalid input to prettyUiString: ' + typeof x); + } + try { + // Here utf8.decode() is better than decoding using NodeJS buffer + // operations because we want strict UTF-8 interpretation. + ret = JSON.stringify(utf8.decode(x)); + } catch (e) { + // When we fall back to representing bytes, indicate that the string + // is "raw" with a 'r"' prefix (a somewhat arbitrary convention). + // U+0022 = ", U+0027 = ' + ret = 'r"' + x.replace(/[\u0022\u0027\u0000-\u001f\u0080-\uffff]/g, function (match) { + var cp = match.charCodeAt(0); + return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f]; + }) + '"'; + } + + if (cliplen && ret.length > cliplen) { + ret = ret.substring(0, cliplen) + '...'; // trailing '"' intentionally missing + } + return ret; +} + +/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF) + * for UI usage without quotes. + */ +function prettyUiStringUnquoted(x, cliplen) { + var ret; + + if (typeof x !== 'string') { + throw new Error('invalid input to prettyUiStringUnquoted: ' + typeof x); + } + + try { + // Here utf8.decode() is better than decoding using NodeJS buffer + // operations because we want strict UTF-8 interpretation. + + // XXX: unprintable characters etc? In some UI cases we'd want to + // e.g. escape newlines and in others not. + ret = utf8.decode(x); + } catch (e) { + // For the unquoted version we don't need to escape single or double + // quotes. + ret = x.replace(/[\u0000-\u001f\u0080-\uffff]/g, function (match) { + var cp = match.charCodeAt(0); + return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f]; + }); + } + + if (cliplen && ret.length > cliplen) { + ret = ret.substring(0, cliplen) + '...'; + } + return ret; +} + +/* Pretty print a dvalue for UI usage. Everything comes out as a ready-to-use + * string. + * + * XXX: Currently the debug client formats all values for UI use. A better + * solution would be to pass values in typed form and let the UI format them, + * so that styling etc. could take typing into account. + */ +function prettyUiDebugValue(x, cliplen) { + if (typeof x === 'object' && x !== null) { + // Note: typeof null === 'object', so null special case explicitly + if (x.type === 'eom') { + return 'EOM'; + } else if (x.type === 'req') { + return 'REQ'; + } else if (x.type === 'rep') { + return 'REP'; + } else if (x.type === 'err') { + return 'ERR'; + } else if (x.type === 'nfy') { + return 'NFY'; + } else if (x.type === 'unused') { + return 'unused'; + } else if (x.type === 'undefined') { + return 'undefined'; + } else if (x.type === 'buffer') { + return '|' + x.data + '|'; + } else if (x.type === 'object') { + return '[object ' + (dukClassNames[x.class] || ('class ' + x.class)) + ' ' + x.pointer + ']'; + } else if (x.type === 'pointer') { + return '<pointer ' + x.pointer + '>'; + } else if (x.type === 'lightfunc') { + return '<lightfunc 0x' + x.flags.toString(16) + ' ' + x.pointer + '>'; + } else if (x.type === 'number') { + // duk_tval number, any IEEE double + var tmp = new Buffer(x.data, 'hex'); // decode into hex + var val = tmp.readDoubleBE(0); // big endian ieee double + return prettyUiNumber(val); + } + } else if (x === null) { + return 'null'; + } else if (typeof x === 'boolean') { + return x ? 'true' : 'false'; + } else if (typeof x === 'string') { + return prettyUiString(x, cliplen); + } else if (typeof x === 'number') { + // Debug protocol integer + return prettyUiNumber(x); + } + + // We shouldn't come here, but if we do, JSON is a reasonable default. + return JSON.stringify(x); +} + +/* Pretty print a debugger message given as an array of parsed dvalues. + * Result should be a pure ASCII one-liner. + */ +function prettyDebugMessage(msg) { + return msg.map(prettyDebugValue).join(' '); +} + +/* Pretty print a debugger command. */ +function prettyDebugCommand(cmd) { + return debugCommandNames[cmd] || String(cmd); +} + +/* Decode and normalize source file contents: UTF-8, tabs to 8, + * CR LF to LF. + */ +function decodeAndNormalizeSource(data) { + var tmp; + var lines, line, repl; + var i, n; + var j, m; + + try { + tmp = data.toString('utf8'); + } catch (e) { + console.log('Failed to UTF-8 decode source file, ignoring: ' + e); + tmp = String(data); + } + + lines = tmp.split(/\r?\n/); + for (i = 0, n = lines.length; i < n; i++) { + line = lines[i]; + if (/\t/.test(line)) { + repl = ''; + for (j = 0, m = line.length; j < m; j++) { + if (line.charAt(j) === '\t') { + repl += ' '; + while ((repl.length % 8) != 0) { + repl += ' '; + } + } else { + repl += line.charAt(j); + } + } + lines[i] = repl; + } + } + + // XXX: normalize last newline (i.e. force a newline if contents don't + // end with a newline)? + + return lines.join('\n'); +} + +/* Token bucket rate limiter for a given callback. Calling code calls + * trigger() to request 'cb' to be called, and the rate limiter ensures + * that 'cb' is not called too often. + */ +function RateLimited(tokens, rate, cb) { + var _this = this; + this.maxTokens = tokens; + this.tokens = this.maxTokens; + this.rate = rate; + this.cb = cb; + this.delayedCb = false; + + // Right now the implementation is setInterval-based, but could also be + // made timerless. There are so few rate limited resources that this + // doesn't matter in practice. + + this.tokenAdder = setInterval(function () { + if (_this.tokens < _this.maxTokens) { + _this.tokens++; + } + if (_this.delayedCb) { + _this.delayedCb = false; + _this.tokens--; + _this.cb(); + } + }, this.rate); +} +RateLimited.prototype.trigger = function () { + if (this.tokens > 0) { + this.tokens--; + this.cb(); + } else { + this.delayedCb = true; + } +}; + +/* + * Source file manager + * + * Scan the list of search directories for Ecmascript source files and + * build an index of them. Provides a mechanism to find a source file + * based on a raw 'fileName' property provided by the debug target, and + * to provide a file list for the web UI. + * + * NOTE: it's tempting to do loose matching for filenames, but this does + * not work in practice. Filenames must match 1:1 with the debug target + * so that e.g. breakpoints assigned based on filenames found from the + * search paths will match 1:1 on the debug target. If this is not the + * case, breakpoints won't work as expected. + */ + +function SourceFileManager(directories) { + this.directories = directories; + this.extensions = { '.js': true, '.jsm': true }; + this.files; +} + +SourceFileManager.prototype.scan = function () { + var _this = this; + var fileMap = {}; // absFn -> true + var files; + + this.directories.forEach(function (dir) { + console.log('Scanning source files: ' + dir); + try { + wrench.readdirSyncRecursive(dir).forEach(function (fn) { + var absFn = path.normalize(path.join(dir, fn)); // './foo/bar.js' -> 'foo/bar.js' + var ent; + + if (fs.existsSync(absFn) && + fs.lstatSync(absFn).isFile() && + _this.extensions[path.extname(fn)]) { + // We want the fileMap to contain the filename relative to + // the search dir root. + fileMap[fn] = true; + } + }); + } catch (e) { + console.log('Failed to scan ' + dir + ': ' + e); + } + }); + + files = Object.keys(fileMap); + files.sort(); + this.files = files; + + console.log('Found ' + files.length + ' source files in ' + this.directories.length + ' search directories'); +}; + +SourceFileManager.prototype.getFiles = function () { + return this.files; +}; + +SourceFileManager.prototype.search = function (fileName) { + var _this = this; + + // Loose matching is tempting but counterproductive: filenames must + // match 1:1 between the debug client and the debug target for e.g. + // breakpoints to work as expected. Note that a breakpoint may be + // assigned by selecting a file from a dropdown populated by scanning + // the filesystem for available sources and there's no way of knowing + // if the debug target uses the exact same name. + + function tryLookup() { + var i, fn, data; + + for (i = 0; i < _this.directories.length; i++) { + fn = path.join(_this.directories[i], fileName); + if (fs.existsSync(fn) && fs.lstatSync(fn).isFile()) { + data = fs.readFileSync(fn); // Raw bytes + return decodeAndNormalizeSource(data); // Unicode string + } + } + return null; + } + + return tryLookup(fileName); +}; + +/* + * Debug protocol parser + * + * The debug protocol parser is an EventEmitter which parses debug messages + * from an input stream and emits 'debug-message' events for completed + * messages ending in an EOM. The parser also provides debug dumping, stream + * logging functionality, and statistics gathering functionality. + * + * This parser is used to parse both incoming and outgoing messages. For + * outgoing messages the only function is to validate and debug dump the + * messages we're about to send. The downside of dumping at this low level + * is that we can't match request and reply/error messages here. + * + * http://www.sitepoint.com/nodejs-events-and-eventemitter/ + */ + +function DebugProtocolParser(inputStream, + protocolVersion, + rawDumpFileName, + textDumpFileName, + textDumpFilePrefix, + hexDumpConsolePrefix, + textDumpConsolePrefix) { + var _this = this; + this.inputStream = inputStream; + this.closed = false; // stream is closed/broken, don't parse anymore + this.bytes = 0; + this.dvalues = 0; + this.messages = 0; + this.requests = 0; + this.prevBytes = 0; + this.bytesPerSec = 0; + this.statsTimer = null; + this.readableNumberValue = true; + + events.EventEmitter.call(this); + + var buf = new Buffer(0); // accumulate data + var msg = []; // accumulated message until EOM + var versionIdentification; + + var statsInterval = 2000; + var statsIntervalSec = statsInterval / 1000; + this.statsTimer = setInterval(function () { + _this.bytesPerSec = (_this.bytes - _this.prevBytes) / statsIntervalSec; + _this.prevBytes = _this.bytes; + _this.emit('stats-update'); + }, statsInterval); + + function consume(n) { + var tmp = new Buffer(buf.length - n); + buf.copy(tmp, 0, n); + buf = tmp; + } + + inputStream.on('data', function (data) { + var i, n, x, v, gotValue, len, t, tmpbuf, verstr; + var prettyMsg; + + if (_this.closed || !_this.inputStream) { + console.log('Ignoring incoming data from closed input stream, len ' + data.length); + return; + } + + _this.bytes += data.length; + if (rawDumpFileName) { + fs.appendFileSync(rawDumpFileName, data); + } + if (hexDumpConsolePrefix) { + console.log(hexDumpConsolePrefix + data.toString('hex')); + } + + buf = Buffer.concat([ buf, data ]); + + // Protocol version handling. When dumping an output stream, the + // caller gives a non-null protocolVersion so we don't read one here. + if (protocolVersion == null) { + if (buf.length > 1024) { + _this.emit('transport-error', 'Parse error (version identification too long), dropping connection'); + _this.close(); + return; + } + + for (i = 0, n = buf.length; i < n; i++) { + if (buf[i] == 0x0a) { + tmpbuf = new Buffer(i); + buf.copy(tmpbuf, 0, 0, i); + consume(i + 1); + verstr = tmpbuf.toString('utf-8'); + t = verstr.split(' '); + protocolVersion = Number(t[0]); + versionIdentification = verstr; + + _this.emit('protocol-version', { + protocolVersion: protocolVersion, + versionIdentification: versionIdentification + }); + break; + } + } + + if (protocolVersion == null) { + // Still waiting for version identification to complete. + return; + } + } + + // Parse complete dvalues (quite inefficient now) by trial parsing. + // Consume a value only when it's fully present in 'buf'. + // See doc/debugger.rst for format description. + + while (buf.length > 0) { + x = buf[0]; + v = undefined; + gotValue = false; // used to flag special values like undefined + + if (x >= 0xc0) { + // 0xc0...0xff: integers 0-16383 + if (buf.length >= 2) { + v = ((x - 0xc0) << 8) + buf[1]; + consume(2); + } + } else if (x >= 0x80) { + // 0x80...0xbf: integers 0-63 + v = x - 0x80; + consume(1); + } else if (x >= 0x60) { + // 0x60...0x7f: strings with length 0-31 + len = x - 0x60; + if (buf.length >= 1 + len) { + v = new Buffer(len); + buf.copy(v, 0, 1, 1 + len); + v = bufferToDebugString(v); + consume(1 + len); + } + } else { + switch (x) { + case 0x00: v = DVAL_EOM; consume(1); break; + case 0x01: v = DVAL_REQ; consume(1); break; + case 0x02: v = DVAL_REP; consume(1); break; + case 0x03: v = DVAL_ERR; consume(1); break; + case 0x04: v = DVAL_NFY; consume(1); break; + case 0x10: // 4-byte signed integer + if (buf.length >= 5) { + v = buf.readInt32BE(1); + consume(5); + } + break; + case 0x11: // 4-byte string + if (buf.length >= 5) { + len = buf.readUInt32BE(1); + if (buf.length >= 5 + len) { + v = new Buffer(len); + buf.copy(v, 0, 5, 5 + len); + v = bufferToDebugString(v); + consume(5 + len); + } + } + break; + case 0x12: // 2-byte string + if (buf.length >= 3) { + len = buf.readUInt16BE(1); + if (buf.length >= 3 + len) { + v = new Buffer(len); + buf.copy(v, 0, 3, 3 + len); + v = bufferToDebugString(v); + consume(3 + len); + } + } + break; + case 0x13: // 4-byte buffer + if (buf.length >= 5) { + len = buf.readUInt32BE(1); + if (buf.length >= 5 + len) { + v = new Buffer(len); + buf.copy(v, 0, 5, 5 + len); + v = { type: 'buffer', data: v.toString('hex') }; + consume(5 + len); + // Value could be a Node.js buffer directly, but + // we prefer all dvalues to be JSON compatible + } + } + break; + case 0x14: // 2-byte buffer + if (buf.length >= 3) { + len = buf.readUInt16BE(1); + if (buf.length >= 3 + len) { + v = new Buffer(len); + buf.copy(v, 0, 3, 3 + len); + v = { type: 'buffer', data: v.toString('hex') }; + consume(3 + len); + // Value could be a Node.js buffer directly, but + // we prefer all dvalues to be JSON compatible + } + } + break; + case 0x15: // unused/none + v = { type: 'unused' }; + consume(1); + break; + case 0x16: // undefined + v = { type: 'undefined' }; + gotValue = true; // indicate 'v' is actually set + consume(1); + break; + case 0x17: // null + v = null; + gotValue = true; // indicate 'v' is actually set + consume(1); + break; + case 0x18: // true + v = true; + consume(1); + break; + case 0x19: // false + v = false; + consume(1); + break; + case 0x1a: // number (IEEE double), big endian + if (buf.length >= 9) { + v = new Buffer(8); + buf.copy(v, 0, 1, 9); + v = { type: 'number', data: v.toString('hex') }; + + if (_this.readableNumberValue) { + // The value key should not be used programmatically, + // it is just there to make the dumps more readable. + v.value = buf.readDoubleBE(1); + } + consume(9); + } + break; + case 0x1b: // object + if (buf.length >= 3) { + len = buf[2]; + if (buf.length >= 3 + len) { + v = new Buffer(len); + buf.copy(v, 0, 3, 3 + len); + v = { type: 'object', 'class': buf[1], pointer: v.toString('hex') }; + consume(3 + len); + } + } + break; + case 0x1c: // pointer + if (buf.length >= 2) { + len = buf[1]; + if (buf.length >= 2 + len) { + v = new Buffer(len); + buf.copy(v, 0, 2, 2 + len); + v = { type: 'pointer', pointer: v.toString('hex') }; + consume(2 + len); + } + } + break; + case 0x1d: // lightfunc + if (buf.length >= 4) { + len = buf[3]; + if (buf.length >= 4 + len) { + v = new Buffer(len); + buf.copy(v, 0, 4, 4 + len); + v = { type: 'lightfunc', flags: buf.readUInt16BE(1), pointer: v.toString('hex') }; + consume(4 + len); + } + } + break; + case 0x1e: // heapptr + if (buf.length >= 2) { + len = buf[1]; + if (buf.length >= 2 + len) { + v = new Buffer(len); + buf.copy(v, 0, 2, 2 + len); + v = { type: 'heapptr', pointer: v.toString('hex') }; + consume(2 + len); + } + } + break; + default: + _this.emit('transport-error', 'Parse error, dropping connection'); + _this.close(); + } + } + + if (typeof v === 'undefined' && !gotValue) { + break; + } + msg.push(v); + _this.dvalues++; + + // Could emit a 'debug-value' event here, but that's not necessary + // because the receiver will just collect statistics which can also + // be done using the finished message. + + if (v === DVAL_EOM) { + _this.messages++; + + if (textDumpFileName || textDumpConsolePrefix) { + prettyMsg = prettyDebugMessage(msg); + if (textDumpFileName) { + fs.appendFileSync(textDumpFileName, (textDumpFilePrefix || '') + prettyMsg + '\n'); + } + if (textDumpConsolePrefix) { + console.log(textDumpConsolePrefix + prettyMsg); + } + } + + _this.emit('debug-message', msg); + msg = []; // new object, old may be in circulation for a while + } + } + }); + + // Not all streams will emit this. + inputStream.on('error', function (err) { + _this.emit('transport-error', err); + _this.close(); + }); + + // Not all streams will emit this. + inputStream.on('close', function () { + _this.close(); + }); +} +DebugProtocolParser.prototype = Object.create(events.EventEmitter.prototype); + +DebugProtocolParser.prototype.close = function () { + // Although the underlying transport may not have a close() or destroy() + // method or even a 'close' event, this method is always available and + // will generate a 'transport-close'. + // + // The caller is responsible for closing the underlying stream if that + // is necessary. + + if (this.closed) { return; } + + this.closed = true; + if (this.statsTimer) { + clearInterval(this.statsTimer); + this.statsTimer = null; + } + this.emit('transport-close'); +}; + +/* + * Debugger output formatting + */ + +function formatDebugValue(v) { + var buf, dec, len; + + // See doc/debugger.rst for format description. + + if (typeof v === 'object' && v !== null) { + // Note: typeof null === 'object', so null special case explicitly + if (v.type === 'eom') { + return new Buffer([ 0x00 ]); + } else if (v.type === 'req') { + return new Buffer([ 0x01 ]); + } else if (v.type === 'rep') { + return new Buffer([ 0x02 ]); + } else if (v.type === 'err') { + return new Buffer([ 0x03 ]); + } else if (v.type === 'nfy') { + return new Buffer([ 0x04 ]); + } else if (v.type === 'unused') { + return new Buffer([ 0x15 ]); + } else if (v.type === 'undefined') { + return new Buffer([ 0x16 ]); + } else if (v.type === 'number') { + dec = new Buffer(v.data, 'hex'); + len = dec.length; + if (len !== 8) { + throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v)); + } + buf = new Buffer(1 + len); + buf[0] = 0x1a; + dec.copy(buf, 1); + return buf; + } else if (v.type === 'buffer') { + dec = new Buffer(v.data, 'hex'); + len = dec.length; + if (len <= 0xffff) { + buf = new Buffer(3 + len); + buf[0] = 0x14; + buf[1] = (len >> 8) & 0xff; + buf[2] = (len >> 0) & 0xff; + dec.copy(buf, 3); + return buf; + } else { + buf = new Buffer(5 + len); + buf[0] = 0x13; + buf[1] = (len >> 24) & 0xff; + buf[2] = (len >> 16) & 0xff; + buf[3] = (len >> 8) & 0xff; + buf[4] = (len >> 0) & 0xff; + dec.copy(buf, 5); + return buf; + } + } else if (v.type === 'object') { + dec = new Buffer(v.pointer, 'hex'); + len = dec.length; + buf = new Buffer(3 + len); + buf[0] = 0x1b; + buf[1] = v.class; + buf[2] = len; + dec.copy(buf, 3); + return buf; + } else if (v.type === 'pointer') { + dec = new Buffer(v.pointer, 'hex'); + len = dec.length; + buf = new Buffer(2 + len); + buf[0] = 0x1c; + buf[1] = len; + dec.copy(buf, 2); + return buf; + } else if (v.type === 'lightfunc') { + dec = new Buffer(v.pointer, 'hex'); + len = dec.length; + buf = new Buffer(4 + len); + buf[0] = 0x1d; + buf[1] = (v.flags >> 8) & 0xff; + buf[2] = v.flags & 0xff; + buf[3] = len; + dec.copy(buf, 4); + return buf; + } else if (v.type === 'heapptr') { + dec = new Buffer(v.pointer, 'hex'); + len = dec.length; + buf = new Buffer(2 + len); + buf[0] = 0x1e; + buf[1] = len; + dec.copy(buf, 2); + return buf; + } + } else if (v === null) { + return new Buffer([ 0x17 ]); + } else if (typeof v === 'boolean') { + return new Buffer([ v ? 0x18 : 0x19 ]); + } else if (typeof v === 'number') { + if (Math.floor(v) === v && /* whole */ + (v !== 0 || 1 / v > 0) && /* not negative zero */ + v >= -0x80000000 && v <= 0x7fffffff) { + // Represented signed 32-bit integers as plain integers. + // Debugger code expects this for all fields that are not + // duk_tval representations (e.g. command numbers and such). + if (v >= 0x00 && v <= 0x3f) { + return new Buffer([ 0x80 + v ]); + } else if (v >= 0x0000 && v <= 0x3fff) { + return new Buffer([ 0xc0 + (v >> 8), v & 0xff ]); + } else if (v >= -0x80000000 && v <= 0x7fffffff) { + return new Buffer([ 0x10, + (v >> 24) & 0xff, + (v >> 16) & 0xff, + (v >> 8) & 0xff, + (v >> 0) & 0xff ]); + } else { + throw new Error('internal error when encoding integer to dvalue: ' + v); + } + } else { + // Represent non-integers as IEEE double dvalues + buf = new Buffer(1 + 8); + buf[0] = 0x1a; + buf.writeDoubleBE(v, 1); + return buf; + } + } else if (typeof v === 'string') { + if (v.length < 0 || v.length > 0xffffffff) { + // Not possible in practice. + throw new TypeError('cannot convert to dvalue, invalid string length: ' + v.length); + } + if (v.length <= 0x1f) { + buf = new Buffer(1 + v.length); + buf[0] = 0x60 + v.length; + writeDebugStringToBuffer(v, buf, 1); + return buf; + } else if (v.length <= 0xffff) { + buf = new Buffer(3 + v.length); + buf[0] = 0x12; + buf[1] = (v.length >> 8) & 0xff; + buf[2] = (v.length >> 0) & 0xff; + writeDebugStringToBuffer(v, buf, 3); + return buf; + } else { + buf = new Buffer(5 + v.length); + buf[0] = 0x11; + buf[1] = (v.length >> 24) & 0xff; + buf[2] = (v.length >> 16) & 0xff; + buf[3] = (v.length >> 8) & 0xff; + buf[4] = (v.length >> 0) & 0xff; + writeDebugStringToBuffer(v, buf, 5); + return buf; + } + } + + // Shouldn't come here. + throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v)); +} + +/* + * Debugger implementation + * + * A debugger instance communicates with the debug target and maintains + * persistent debug state so that the current state can be resent to the + * socket.io client (web UI) if it reconnects. Whenever the debugger state + * is changed an event is generated. The socket.io handler will listen to + * state change events and push the necessary updates to the web UI, often + * in a rate limited fashion or using a client pull to ensure the web UI + * is not overloaded. + * + * The debugger instance assumes that if the debug protocol connection is + * re-established, it is always to the same target. There is no separate + * abstraction for a debugger session. + */ + +function Debugger() { + events.EventEmitter.call(this); + + this.web = null; // web UI singleton + this.targetStream = null; // transport connection to target + this.outputPassThroughStream = null; // dummy passthrough for message dumping + this.inputParser = null; // parser for incoming debug messages + this.outputParser = null; // parser for outgoing debug messages (stats, dumping) + this.protocolVersion = null; + this.dukVersion = null; + this.dukGitDescribe = null; + this.targetInfo = null; + this.attached = false; + this.handshook = false; + this.reqQueue = null; + this.stats = { // stats for current debug connection + rxBytes: 0, rxDvalues: 0, rxMessages: 0, rxBytesPerSec: 0, + txBytes: 0, txDvalues: 0, txMessages: 0, txBytesPerSec: 0 + }; + this.execStatus = { + attached: false, + state: 'detached', + fileName: '', + funcName: '', + line: 0, + pc: 0 + }; + this.breakpoints = []; + this.callstack = []; + this.locals = []; + this.messageLines = []; + this.messageScrollBack = 100; +} +Debugger.prototype = events.EventEmitter.prototype; + +Debugger.prototype.decodeBytecodeFromBuffer = function (buf, consts, funcs) { + var i, j, n, m, ins, pc; + var res = []; + var op, str, args, comments; + + // XXX: add constants inline to preformatted output (e.g. for strings, + // add a short escaped snippet as a comment on the line after the + // compact argument list). + + for (i = 0, n = buf.length; i < n; i += 4) { + pc = i / 4; + + // shift forces unsigned + if (this.endianness === 'little') { + ins = buf.readInt32LE(i) >>> 0; + } else { + ins = buf.readInt32BE(i) >>> 0; + } + + op = dukOpcodes.opcodes[ins & 0x3f]; + if (op.extra) { + op = dukOpcodes.extra[(ins >> 6) & 0xff]; + } + + args = []; + comments = []; + if (op.args) { + for (j = 0, m = op.args.length; j < m; j++) { + switch (op.args[j]) { + case 'A_R': args.push('r' + ((ins >>> 6) & 0xff)); break; + case 'A_RI': args.push('r' + ((ins >>> 6) & 0xff) + '(indirect)'); break; + case 'A_C': args.push('c' + ((ins >>> 6) & 0xff)); break; + case 'A_H': args.push('0x' + ((ins >>> 6) & 0xff).toString(16)); break; + case 'A_I': args.push(((ins >>> 6) & 0xff).toString(10)); break; + case 'A_B': args.push(((ins >>> 6) & 0xff) ? 'true' : 'false'); break; + case 'B_RC': args.push((ins & (1 << 22) ? 'c' : 'r') + ((ins >>> 14) & 0x0ff)); break; + case 'B_R': args.push('r' + ((ins >>> 14) & 0x1ff)); break; + case 'B_RI': args.push('r' + ((ins >>> 14) & 0x1ff) + '(indirect)'); break; + case 'B_C': args.push('c' + ((ins >>> 14) & 0x1ff)); break; + case 'B_H': args.push('0x' + ((ins >>> 14) & 0x1ff).toString(16)); break; + case 'B_I': args.push(((ins >>> 14) & 0x1ff).toString(10)); break; + case 'C_RC': args.push((ins & (1 << 31) ? 'c' : 'r') + ((ins >>> 23) & 0x0ff)); break; + case 'C_R': args.push('r' + ((ins >>> 23) & 0x1ff)); break; + case 'C_RI': args.push('r' + ((ins >>> 23) & 0x1ff) + '(indirect)'); break; + case 'C_C': args.push('c' + ((ins >>> 23) & 0x1ff)); break; + case 'C_H': args.push('0x' + ((ins >>> 23) & 0x1ff).toString(16)); break; + case 'C_I': args.push(((ins >>> 23) & 0x1ff).toString(10)); break; + case 'BC_R': args.push('r' + ((ins >>> 14) & 0x3ffff)); break; + case 'BC_C': args.push('c' + ((ins >>> 14) & 0x3ffff)); break; + case 'BC_H': args.push('0x' + ((ins >>> 14) & 0x3ffff).toString(16)); break; + case 'BC_I': args.push(((ins >>> 14) & 0x3ffff).toString(10)); break; + case 'ABC_H': args.push(((ins >>> 6) & 0x03ffffff).toString(16)); break; + case 'ABC_I': args.push(((ins >>> 6) & 0x03ffffff).toString(10)); break; + case 'BC_LDINT': args.push(((ins >>> 14) & 0x3ffff) - (1 << 17)); break; + case 'BC_LDINTX': args.push(((ins >>> 14) & 0x3ffff) - 0); break; // no bias in LDINTX + case 'ABC_JUMP': { + var pc_add = ((ins >>> 6) & 0x03ffffff) - (1 << 25) + 1; // pc is preincremented before adding + var pc_dst = pc + pc_add; + args.push(pc_dst + ' (' + (pc_add >= 0 ? '+' : '') + pc_add + ')'); + break; + } + default: args.push('?'); break; + } + } + } + if (op.flags) { + for (j = 0, m = op.flags.length; j < m; j++) { + if (ins & op.flags[j].mask) { + comments.push(op.flags[j].name); + } + } + } + + if (args.length > 0) { + str = sprintf('%05d %08x %-10s %s', pc, ins, op.name, args.join(', ')); + } else { + str = sprintf('%05d %08x %-10s', pc, ins, op.name); + } + if (comments.length > 0) { + str = sprintf('%-44s ; %s', str, comments.join(', ')); + } + + res.push({ + str: str, + ins: ins + }); + } + + return res; +}; + +Debugger.prototype.uiMessage = function (type, val) { + var msg; + if (typeof type === 'object') { + msg = type; + } else if (typeof type === 'string') { + msg = { type: type, message: val }; + } else { + throw new TypeError('invalid ui message: ' + type); + } + this.messageLines.push(msg); + while (this.messageLines.length > this.messageScrollBack) { + this.messageLines.shift(); + } + this.emit('ui-message-update'); // just trigger a sync, gets rate limited +}; + +Debugger.prototype.sendRequest = function (msg) { + var _this = this; + return new Promise(function (resolve, reject) { + var dvals = []; + var dval; + var data; + var i; + + if (!_this.attached || !_this.handshook || !_this.reqQueue || !_this.targetStream) { + throw new Error('invalid state for sendRequest'); + } + + for (i = 0; i < msg.length; i++) { + try { + dval = formatDebugValue(msg[i]); + } catch (e) { + console.log('Failed to format dvalue, dropping connection: ' + e); + console.log(e.stack || e); + _this.targetStream.destroy(); + throw new Error('failed to format dvalue'); + } + dvals.push(dval); + } + + data = Buffer.concat(dvals); + + _this.targetStream.write(data); + _this.outputPassThroughStream.write(data); // stats and dumping + + if (optLogMessages) { + console.log('Request ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg)); + } + + if (!_this.reqQueue) { + throw new Error('no reqQueue'); + } + + _this.reqQueue.push({ + reqMsg: msg, + reqCmd: msg[1], + resolveCb: resolve, + rejectCb: reject + }); + }); +}; + +Debugger.prototype.sendBasicInfoRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_BASICINFO, DVAL_EOM ]).then(function (msg) { + _this.dukVersion = msg[1]; + _this.dukGitDescribe = msg[2]; + _this.targetInfo = msg[3]; + _this.endianness = { 1: 'little', 2: 'mixed', 3: 'big' }[msg[4]] || 'unknown'; + _this.emit('basic-info-update'); + return msg; + }); +}; + +Debugger.prototype.sendGetVarRequest = function (varname, level) { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_GETVAR, varname, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) { + return { found: msg[1] === 1, value: msg[2] }; + }); +}; + +Debugger.prototype.sendPutVarRequest = function (varname, varvalue, level) { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_PUTVAR, varname, varvalue, (typeof level === 'number' ? level : -1), DVAL_EOM ]); +}; + +Debugger.prototype.sendInvalidCommandTestRequest = function () { + // Intentional invalid command + var _this = this; + return this.sendRequest([ DVAL_REQ, 0xdeadbeef, DVAL_EOM ]); +}; + +Debugger.prototype.sendStatusRequest = function () { + // Send a status request to trigger a status notify, result is ignored: + // target sends a status notify instead of a meaningful reply + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_TRIGGERSTATUS, DVAL_EOM ]); +}; + +Debugger.prototype.sendBreakpointListRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_LISTBREAK, DVAL_EOM ]).then(function (msg) { + var i, n; + var breakpts = []; + + for (i = 1, n = msg.length - 1; i < n; i += 2) { + breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] }); + } + + _this.breakpoints = breakpts; + _this.emit('breakpoints-update'); + return msg; + }); +}; + +Debugger.prototype.sendGetLocalsRequest = function (level) { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_GETLOCALS, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) { + var i; + var locals = []; + + for (i = 1; i <= msg.length - 2; i += 2) { + // XXX: do pretty printing in debug client for now + locals.push({ key: msg[i], value: prettyUiDebugValue(msg[i + 1], LOCALS_CLIPLEN) }); + } + + _this.locals = locals; + _this.emit('locals-update'); + return msg; + }); +}; + +Debugger.prototype.sendGetCallStackRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_GETCALLSTACK, DVAL_EOM ]).then(function (msg) { + var i; + var stack = []; + + for (i = 1; i + 3 <= msg.length - 1; i += 4) { + stack.push({ + fileName: msg[i], + funcName: msg[i + 1], + lineNumber: msg[i + 2], + pc: msg[i + 3] + }); + } + + _this.callstack = stack; + _this.emit('callstack-update'); + return msg; + }); +}; + +Debugger.prototype.sendStepIntoRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_STEPINTO, DVAL_EOM ]); +}; + +Debugger.prototype.sendStepOverRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_STEPOVER, DVAL_EOM ]); +}; + +Debugger.prototype.sendStepOutRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_STEPOUT, DVAL_EOM ]); +}; + +Debugger.prototype.sendPauseRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_PAUSE, DVAL_EOM ]); +}; + +Debugger.prototype.sendResumeRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_RESUME, DVAL_EOM ]); +}; + +Debugger.prototype.sendEvalRequest = function (evalInput, level) { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_EVAL, evalInput, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) { + return { error: msg[1] === 1 /*error*/, value: msg[2] }; + }); +}; + +Debugger.prototype.sendDetachRequest = function () { + var _this = this; + return this.sendRequest([ DVAL_REQ, CMD_DETACH, DVAL_EOM ]); +}; + +Debugger.prototype.sendDumpHeapRequest = function () { + var _this = this; + + return this.sendRequest([ DVAL_REQ, CMD_DUMPHEAP, DVAL_EOM ]).then(function (msg) { + var res = {}; + var objs = []; + var i, j, n, m, o, prop; + + res.type = 'heapDump'; + res.heapObjects = objs; + + for (i = 1, n = msg.length - 1; i < n; /*nop*/) { + o = {}; + o.ptr = msg[i++]; + o.type = msg[i++]; + o.flags = msg[i++] >>> 0; /* unsigned */ + o.refc = msg[i++]; + + if (o.type === DUK_HTYPE_STRING) { + o.blen = msg[i++]; + o.clen = msg[i++]; + o.hash = msg[i++] >>> 0; /* unsigned */ + o.data = msg[i++]; + } else if (o.type === DUK_HTYPE_BUFFER) { + o.len = msg[i++]; + o.data = msg[i++]; + } else if (o.type === DUK_HTYPE_OBJECT) { + o['class'] = msg[i++]; + o.proto = msg[i++]; + o.esize = msg[i++]; + o.enext = msg[i++]; + o.asize = msg[i++]; + o.hsize = msg[i++]; + o.props = []; + for (j = 0, m = o.enext; j < m; j++) { + prop = {}; + prop.flags = msg[i++]; + prop.key = msg[i++]; + prop.accessor = (msg[i++] == 1); + if (prop.accessor) { + prop.getter = msg[i++]; + prop.setter = msg[i++]; + } else { + prop.value = msg[i++]; + } + o.props.push(prop); + } + o.array = []; + for (j = 0, m = o.asize; j < m; j++) { + prop = {}; + prop.value = msg[i++]; + o.array.push(prop); + } + } else { + console.log('invalid htype: ' + o.type + ', disconnect'); + _this.disconnectDebugger(); + throw new Error('invalid htype'); + return; + } + + objs.push(o); + } + + return res; + }); +}; + +Debugger.prototype.sendGetBytecodeRequest = function () { + var _this = this; + + return this.sendRequest([ DVAL_REQ, CMD_GETBYTECODE, DVAL_EOM ]).then(function (msg) { + var idx = 1; + var nconst; + var nfunc; + var val; + var buf; + var i, n; + var consts = []; + var funcs = []; + var bcode; + var preformatted; + var ret; + var idxPreformattedInstructions; + + //console.log(JSON.stringify(msg)); + + nconst = msg[idx++]; + for (i = 0; i < nconst; i++) { + val = msg[idx++]; + consts.push(val); + } + + nfunc = msg[idx++]; + for (i = 0; i < nfunc; i++) { + val = msg[idx++]; + funcs.push(val); + } + val = msg[idx++]; + + // Right now bytecode is a string containing a direct dump of the + // bytecode in target endianness. Decode here so that the web UI + // doesn't need to. + + buf = new Buffer(val.length); + writeDebugStringToBuffer(val, buf, 0); + bcode = _this.decodeBytecodeFromBuffer(buf, consts, funcs); + + preformatted = []; + consts.forEach(function (v, i) { + preformatted.push('; c' + i + ' ' + JSON.stringify(v)); + }); + preformatted.push(''); + idxPreformattedInstructions = preformatted.length; + bcode.forEach(function (v) { + preformatted.push(v.str); + }); + preformatted = preformatted.join('\n') + '\n'; + + ret = { + constants: consts, + functions: funcs, + bytecode: bcode, + preformatted: preformatted, + idxPreformattedInstructions: idxPreformattedInstructions + }; + + return ret; + }); +}; + +Debugger.prototype.changeBreakpoint = function (fileName, lineNumber, mode) { + var _this = this; + + return this.sendRequest([ DVAL_REQ, CMD_LISTBREAK, DVAL_EOM ]).then(function (msg) { + var i, n; + var breakpts = []; + var deleted = false; + + // Up-to-date list of breakpoints on target + for (i = 1, n = msg.length - 1; i < n; i += 2) { + breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] }); + } + + // Delete matching breakpoints in reverse order so that indices + // remain valid. We do this for all operations so that duplicates + // are eliminated if present. + for (i = breakpts.length - 1; i >= 0; i--) { + var bp = breakpts[i]; + if (mode === 'deleteall' || (bp.fileName === fileName && bp.lineNumber === lineNumber)) { + deleted = true; + _this.sendRequest([ DVAL_REQ, CMD_DELBREAK, i, DVAL_EOM ], function (msg) { + // nop + }, function (err) { + // nop + }); + } + } + + // Technically we should wait for each delbreak reply but because + // target processes the requests in order, it doesn't matter. + if ((mode === 'add') || (mode === 'toggle' && !deleted)) { + _this.sendRequest([ DVAL_REQ, CMD_ADDBREAK, fileName, lineNumber, DVAL_EOM ], function (msg) { + // nop + }, function (err) { + _this.uiMessage('debugger-info', 'Failed to add breakpoint: ' + err); + }); + } + + // Read final, effective breakpoints from the target + _this.sendBreakpointListRequest(); + }); +}; + +Debugger.prototype.disconnectDebugger = function () { + if (this.targetStream) { + // We require a destroy() method from the actual target stream + this.targetStream.destroy(); + this.targetStream = null; + } + if (this.inputParser) { + this.inputParser.close(); + this.inputParser = null; + } + if (this.outputPassThroughStream) { + // There is no close() or destroy() for a passthrough stream, so just + // close the outputParser which will cancel timers etc. + } + if (this.outputParser) { + this.outputParser.close(); + this.outputParser = null; + } + + this.attached = false; + this.handshook = false; + this.reqQueue = null; + this.execStatus = { + attached: false, + state: 'detached', + fileName: '', + funcName: '', + line: 0, + pc: 0 + }; +}; + +Debugger.prototype.connectDebugger = function () { + var _this = this; + + this.disconnectDebugger(); // close previous target connection + + // CUSTOMTRANSPORT: to use a custom transport, change this.targetStream to + // use your custom transport. + + console.log('Connecting to ' + optTargetHost + ':' + optTargetPort + '...'); + this.targetStream = new net.Socket(); + this.targetStream.connect(optTargetPort, optTargetHost, function () { + console.log('Debug transport connected'); + _this.attached = true; + _this.reqQueue = []; + _this.uiMessage('debugger-info', 'Debug transport connected'); + }); + + this.inputParser = new DebugProtocolParser( + this.targetStream, + null, + optDumpDebugRead, + optDumpDebugPretty, + optDumpDebugPretty ? 'Recv: ' : null, + null, + null // console logging is done at a higher level to match request/response + ); + + // Use a PassThrough stream to debug dump and get stats for output messages. + // Simply write outgoing data to both the targetStream and this passthrough + // separately. + this.outputPassThroughStream = stream.PassThrough(); + this.outputParser = new DebugProtocolParser( + this.outputPassThroughStream, + 1, + optDumpDebugWrite, + optDumpDebugPretty, + optDumpDebugPretty ? 'Send: ' : null, + null, + null // console logging is done at a higher level to match request/response + ); + + this.inputParser.on('transport-close', function () { + _this.uiMessage('debugger-info', 'Debug transport closed'); + _this.disconnectDebugger(); + _this.emit('exec-status-update'); + _this.emit('detached'); + }); + + this.inputParser.on('transport-error', function (err) { + _this.uiMessage('debugger-info', 'Debug transport error: ' + err); + _this.disconnectDebugger(); + }); + + this.inputParser.on('protocol-version', function (msg) { + var ver = msg.protocolVersion; + console.log('Debug version identification:', msg.versionIdentification); + _this.protocolVersion = ver; + _this.uiMessage('debugger-info', 'Debug version identification: ' + msg.versionIdentification); + if (ver !== 1) { + _this.uiMessage('debugger-info', 'Protocol version ' + ver + ' unsupported, dropping connection'); + _this.targetStream.destroy(); + } else { + _this.uiMessage('debugger-info', 'Debug protocol version: ' + ver); + _this.handshook = true; + _this.execStatus = { + attached: true, + state: 'attached', + fileName: '', + funcName: '', + line: 0, + pc: 0 + }; + _this.emit('exec-status-update'); + _this.emit('attached'); // inform web UI + + // Fetch basic info right away + _this.sendBasicInfoRequest(); + } + }); + + this.inputParser.on('debug-message', function (msg) { + _this.processDebugMessage(msg); + }); + + this.inputParser.on('stats-update', function () { + _this.stats.rxBytes = this.bytes; + _this.stats.rxDvalues = this.dvalues; + _this.stats.rxMessages = this.messages; + _this.stats.rxBytesPerSec = this.bytesPerSec; + _this.emit('debug-stats-update'); + }); + + this.outputParser.on('stats-update', function () { + _this.stats.txBytes = this.bytes; + _this.stats.txDvalues = this.dvalues; + _this.stats.txMessages = this.messages; + _this.stats.txBytesPerSec = this.bytesPerSec; + _this.emit('debug-stats-update'); + }); +}; + +Debugger.prototype.processDebugMessage = function (msg) { + var req; + var prevState, newState; + var err; + + if (msg[0] === DVAL_REQ) { + // No actual requests sent by the target right now (just notifys). + console.log('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg)); + } else if (msg[0] === DVAL_REP) { + if (this.reqQueue.length <= 0) { + console.log('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg)); + this.targetStream.destroy(); + } + req = this.reqQueue.shift(); + + if (optLogMessages) { + console.log('Reply for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg)); + } + + if (req.resolveCb) { + req.resolveCb(msg); + } else { + // nop: no callback + } + } else if (msg[0] === DVAL_ERR) { + if (this.reqQueue.length <= 0) { + console.log('Unsolicited error message, dropping connection: ' + prettyDebugMessage(msg)); + this.targetStream.destroy(); + } + err = new Error(String(msg[2]) + ' (code ' + String(msg[1]) + ')'); + err.errorCode = msg[1] || 0; + req = this.reqQueue.shift(); + + if (optLogMessages) { + console.log('Error for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg)); + } + + if (req.rejectCb) { + req.rejectCb(err); + } else { + // nop: no callback + } + } else if (msg[0] === DVAL_NFY) { + if (optLogMessages) { + console.log('Notify ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg)); + } + + if (msg[1] === CMD_STATUS) { + prevState = this.execStatus.state; + newState = msg[2] === 0 ? 'running' : 'paused'; + this.execStatus = { + attached: true, + state: newState, + fileName: msg[3], + funcName: msg[4], + line: msg[5], + pc: msg[6] + }; + + if (prevState !== newState && newState === 'paused') { + // update run state now that we're paused + this.sendBreakpointListRequest(); + this.sendGetLocalsRequest(); + this.sendGetCallStackRequest(); + } + + this.emit('exec-status-update'); + } else if (msg[1] === CMD_PRINT) { + this.uiMessage('print', prettyUiStringUnquoted(msg[2], UI_MESSAGE_CLIPLEN)); + } else if (msg[1] === CMD_ALERT) { + this.uiMessage('alert', prettyUiStringUnquoted(msg[2], UI_MESSAGE_CLIPLEN)); + } else if (msg[1] === CMD_LOG) { + this.uiMessage({ type: 'log', level: msg[2], message: prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN) }); + } else if (msg[1] === CMD_THROW) { + this.uiMessage({ type: 'throw', fatal: msg[2], message: (msg[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN), fileName: msg[4], lineNumber: msg[5] }); + } else if (msg[1] === CMD_DETACHING) { + this.uiMessage({ type: 'detaching', reason: msg[2], message: 'DETACH: ' + (msg.length >= 5 ? prettyUiStringUnquoted(msg[3]) : 'detaching') }); + } else { + // Ignore unknown notify messages + console.log('Unknown notify, ignoring: ' + prettyDebugMessage(msg)); + + //this.targetStream.destroy(); + } + } else { + console.log('Invalid initial dvalue, dropping connection: ' + prettyDebugMessage(msg)); + this.targetStream.destroy(); + } +}; + +Debugger.prototype.run = function () { + var _this = this; + + // Initial debugger connection + + this.connectDebugger(); + + // Poll various state items when running + + var sendRound = 0; + var statusPending = false; + var bplistPending = false; + var localsPending = false; + var callStackPending = false; + + setInterval(function () { + if (_this.execStatus.state !== 'running') { + return; + } + + // Could also check for an empty request queue, but that's probably + // too strict? + + // Pending flags are used to avoid requesting the same thing twice + // while a previous request is pending. The flag-based approach is + // quite awkward. Rework to use promises. + + switch (sendRound) { + case 0: + if (!statusPending) { + statusPending = true; + _this.sendStatusRequest().finally(function () { statusPending = false; }); + } + break; + case 1: + if (!bplistPending) { + bplistPending = true; + _this.sendBreakpointListRequest().finally(function () { bplistPending = false; }); + } + break; + case 2: + if (!localsPending) { + localsPending = true; + _this.sendGetLocalsRequest().finally(function () { localsPending = false; }); + } + break; + case 3: + if (!callStackPending) { + callStackPending = true; + _this.sendGetCallStackRequest().finally(function () { callStackPending = false; }); + } + break; + } + sendRound = (sendRound + 1) % 4; + }, 500); +}; + +/* + * Express setup and socket.io + */ + +function DebugWebServer() { + this.dbg = null; // debugger singleton + this.socket = null; // current socket (or null) + this.keepaliveTimer = null; + this.uiMessageLimiter = null; + this.cachedJson = {}; // cache to avoid resending identical data + this.sourceFileManager = new SourceFileManager(optSourceSearchDirs); + this.sourceFileManager.scan(); +} + +DebugWebServer.prototype.handleSourcePost = function (req, res) { + var fileName = req.body && req.body.fileName; + var fileData; + + console.log('Source request: ' + fileName); + + if (typeof fileName !== 'string') { + res.status(500).send('invalid request'); + return; + } + fileData = this.sourceFileManager.search(fileName, optSourceSearchDirs); + if (typeof fileData !== 'string') { + res.status(404).send('not found'); + return; + } + res.status(200).send(fileData); // UTF-8 +}; + +DebugWebServer.prototype.handleSourceListPost = function (req, res) { + console.log('Source list request'); + + var files = this.sourceFileManager.getFiles(); + res.header('Content-Type', 'application/json'); + res.status(200).json(files); +}; + +DebugWebServer.prototype.handleHeapDumpGet = function (req, res) { + console.log('Heap dump get'); + + this.dbg.sendDumpHeapRequest().then(function (val) { + res.header('Content-Type', 'application/json'); + //res.status(200).json(val); + res.status(200).send(JSON.stringify(val, null, 4)); + }).catch(function (err) { + res.status(500).send('Failed to get heap dump: ' + (err.stack || err)); + }); +}; + +DebugWebServer.prototype.run = function () { + var _this = this; + + var express = require('express'); + var bodyParser = require('body-parser'); + var app = express(); + var http = require('http').Server(app); + var io = require('socket.io')(http); + + app.use(bodyParser.json()); + app.post('/source', this.handleSourcePost.bind(this)); + app.post('/sourceList', this.handleSourceListPost.bind(this)); + app.get('/heapDump.json', this.handleHeapDumpGet.bind(this)); + app.use('/', express.static(__dirname + '/static')); + + http.listen(optHttpPort, function () { + console.log('Listening on *:' + optHttpPort); + }); + + io.on('connection', this.handleNewSocketIoConnection.bind(this)); + + this.dbg.on('attached', function () { + console.log('Debugger attached'); + }); + + this.dbg.on('detached', function () { + console.log('Debugger detached'); + }); + + this.dbg.on('debug-stats-update', function () { + _this.debugStatsLimiter.trigger(); + }); + + this.dbg.on('ui-message-update', function () { + // Explicit rate limiter because this is a source of a lot of traffic. + _this.uiMessageLimiter.trigger(); + }); + + this.dbg.on('basic-info-update', function () { + _this.emitBasicInfo(); + }); + + this.dbg.on('breakpoints-update', function () { + _this.emitBreakpoints(); + }); + + this.dbg.on('exec-status-update', function () { + // Explicit rate limiter because this is a source of a lot of traffic. + _this.execStatusLimiter.trigger(); + }); + + this.dbg.on('locals-update', function () { + _this.emitLocals(); + }); + + this.dbg.on('callstack-update', function () { + _this.emitCallStack(); + }); + + this.uiMessageLimiter = new RateLimited(10, 1000, this.uiMessageLimiterCallback.bind(this)); + this.execStatusLimiter = new RateLimited(50, 500, this.execStatusLimiterCallback.bind(this)); + this.debugStatsLimiter = new RateLimited(1, 2000, this.debugStatsLimiterCallback.bind(this)); + + this.keepaliveTimer = setInterval(this.emitKeepalive.bind(this), 30000); +}; + +DebugWebServer.prototype.handleNewSocketIoConnection = function (socket) { + var _this = this; + + console.log('Socket.io connected'); + if (this.socket) { + console.log('Closing previous socket.io socket'); + this.socket.emit('replaced'); + } + this.socket = socket; + + this.emitKeepalive(); + + socket.on('disconnect', function () { + console.log('Socket.io disconnected'); + if (_this.socket === socket) { + _this.socket = null; + } + }); + + socket.on('keepalive', function (msg) { + // nop + }); + + socket.on('attach', function (msg) { + if (_this.dbg.targetStream) { + console.log('Attach request when debugger already has a connection, ignoring'); + } else { + _this.dbg.connectDebugger(); + } + }); + + socket.on('detach', function (msg) { + // Try to detach cleanly, timeout if no response + Promise.any([ + _this.dbg.sendDetachRequest(), + Promise.delay(3000) + ]).finally(function () { + _this.dbg.disconnectDebugger(); + }); + }); + + socket.on('stepinto', function (msg) { + _this.dbg.sendStepIntoRequest(); + }); + + socket.on('stepover', function (msg) { + _this.dbg.sendStepOverRequest(); + }); + + socket.on('stepout', function (msg) { + _this.dbg.sendStepOutRequest(); + }); + + socket.on('pause', function (msg) { + _this.dbg.sendPauseRequest(); + }); + + socket.on('resume', function (msg) { + _this.dbg.sendResumeRequest(); + }); + + socket.on('eval', function (msg) { + // msg.input is a proper Unicode strings here, and needs to be + // converted into a protocol string (U+0000...U+00FF). + var input = stringToDebugString(msg.input); + _this.dbg.sendEvalRequest(input, msg.level).then(function (v) { + socket.emit('eval-result', { error: v.error, result: prettyUiDebugValue(v.value, EVAL_CLIPLEN) }); + }); + + // An eval call quite possibly changes the local variables so always + // re-read locals afterwards. We don't need to wait for Eval to + // complete here; the requests will pipeline automatically and be + // executed in order. + + // XXX: move this to the web UI so that the UI can control what + // locals are listed (or perhaps show locals for all levels with + // an expandable tree view). + _this.dbg.sendGetLocalsRequest(); + }); + + socket.on('getvar', function (msg) { + // msg.varname is a proper Unicode strings here, and needs to be + // converted into a protocol string (U+0000...U+00FF). + var varname = stringToDebugString(msg.varname); + _this.dbg.sendGetVarRequest(varname, msg.level) + .then(function (v) { + socket.emit('getvar-result', { found: v.found, result: prettyUiDebugValue(v.value, GETVAR_CLIPLEN) }); + }); + }); + + socket.on('putvar', function (msg) { + // msg.varname and msg.varvalue are proper Unicode strings here, they + // need to be converted into protocol strings (U+0000...U+00FF). + var varname = stringToDebugString(msg.varname); + var varvalue = msg.varvalue; + + // varvalue is JSON parsed by the web UI for now, need special string + // encoding here. + if (typeof varvalue === 'string') { + varvalue = stringToDebugString(msg.varvalue); + } + + _this.dbg.sendPutVarRequest(varname, varvalue, msg.level) + .then(function (v) { + console.log('putvar done'); // XXX: signal success to UI? + }); + + // A PutVar call quite possibly changes the local variables so always + // re-read locals afterwards. We don't need to wait for PutVar to + // complete here; the requests will pipeline automatically and be + // executed in order. + + // XXX: make the client do this? + _this.dbg.sendGetLocalsRequest(); + }); + + socket.on('add-breakpoint', function (msg) { + _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'add'); + }); + + socket.on('delete-breakpoint', function (msg) { + _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'delete'); + }); + + socket.on('toggle-breakpoint', function (msg) { + _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'toggle'); + }); + + socket.on('delete-all-breakpoints', function (msg) { + _this.dbg.changeBreakpoint(null, null, 'deleteall'); + }); + + socket.on('get-bytecode', function (msg) { + _this.dbg.sendGetBytecodeRequest().then(function (res) { + socket.emit('bytecode', res); + }); + }); + + // Resend all debugger state for new client + this.cachedJson = {}; // clear client state cache + this.emitBasicInfo(); + this.emitStats(); + this.emitExecStatus(); + this.emitUiMessages(); + this.emitBreakpoints(); + this.emitCallStack(); + this.emitLocals(); +}; + +// Check if 'msg' would encode to the same JSON which was previously sent +// to the web client. The caller then avoid resending unnecessary stuff. +DebugWebServer.prototype.cachedJsonCheck = function (cacheKey, msg) { + var newJson = JSON.stringify(msg); + if (this.cachedJson[cacheKey] === newJson) { + return true; // cached + } + this.cachedJson[cacheKey] = newJson; + return false; // not cached, send (cache already updated) +}; + +DebugWebServer.prototype.uiMessageLimiterCallback = function () { + this.emitUiMessages(); +}; + +DebugWebServer.prototype.execStatusLimiterCallback = function () { + this.emitExecStatus(); +}; + +DebugWebServer.prototype.debugStatsLimiterCallback = function () { + this.emitStats(); +}; + +DebugWebServer.prototype.emitKeepalive = function () { + if (!this.socket) { return; } + + this.socket.emit('keepalive', { nodeVersion: process.version }); +}; + +DebugWebServer.prototype.emitBasicInfo = function () { + if (!this.socket) { return; } + + var newMsg = { + duk_version: this.dbg.dukVersion, + duk_git_describe: this.dbg.dukGitDescribe, + target_info: this.dbg.targetInfo, + endianness: this.dbg.endianness + }; + if (this.cachedJsonCheck('basic-info', newMsg)) { + return; + } + this.socket.emit('basic-info', newMsg); +}; + +DebugWebServer.prototype.emitStats = function () { + if (!this.socket) { return; } + + this.socket.emit('debug-stats', this.dbg.stats); +}; + +DebugWebServer.prototype.emitExecStatus = function () { + if (!this.socket) { return; } + + var newMsg = this.dbg.execStatus; + if (this.cachedJsonCheck('exec-status', newMsg)) { + return; + } + this.socket.emit('exec-status', newMsg); +}; + +DebugWebServer.prototype.emitUiMessages = function () { + if (!this.socket) { return; } + + var newMsg = this.dbg.messageLines; + if (this.cachedJsonCheck('output-lines', newMsg)) { + return; + } + this.socket.emit('output-lines', newMsg); +}; + +DebugWebServer.prototype.emitBreakpoints = function () { + if (!this.socket) { return; } + + var newMsg = { breakpoints: this.dbg.breakpoints }; + if (this.cachedJsonCheck('breakpoints', newMsg)) { + return; + } + this.socket.emit('breakpoints', newMsg); +}; + +DebugWebServer.prototype.emitCallStack = function () { + if (!this.socket) { return; } + + var newMsg = { callstack: this.dbg.callstack }; + if (this.cachedJsonCheck('callstack', newMsg)) { + return; + } + this.socket.emit('callstack', newMsg); +}; + +DebugWebServer.prototype.emitLocals = function () { + if (!this.socket) { return; } + + var newMsg = { locals: this.dbg.locals }; + if (this.cachedJsonCheck('locals', newMsg)) { + return; + } + this.socket.emit('locals', newMsg); +}; + +/* + * JSON debug proxy + */ + +function DebugProxy(serverPort) { + this.serverPort = serverPort; + this.server = null; + this.socket = null; + this.targetStream = null; + this.inputParser = null; + + // preformatted dvalues + this.dval_eom = formatDebugValue(DVAL_EOM); + this.dval_req = formatDebugValue(DVAL_REQ); + this.dval_rep = formatDebugValue(DVAL_REP); + this.dval_nfy = formatDebugValue(DVAL_NFY); + this.dval_err = formatDebugValue(DVAL_ERR); +} + +DebugProxy.prototype.determineCommandNumber = function (cmdName, cmdNumber) { + var ret; + if (typeof cmdName === 'string') { + ret = debugCommandNumbers[cmdName]; + } else if (typeof cmdName === 'number') { + ret = cmdName; + } + ret = ret || cmdNumber; + if (typeof ret !== 'number') { + throw Error('cannot figure out command number for "' + cmdName + '" (' + cmdNumber + ')'); + } + return ret; +}; + +DebugProxy.prototype.commandNumberToString = function (id) { + return debugCommandNames[id] || String(id); +}; + +DebugProxy.prototype.formatDvalues = function (args) { + if (!args) { + return []; + } + return args.map(function (v) { + return formatDebugValue(v); + }); +}; + +DebugProxy.prototype.writeJson = function (val) { + this.socket.write(JSON.stringify(val) + '\n'); +}; + +DebugProxy.prototype.writeJsonSafe = function (val) { + try { + this.writeJson(val); + } catch (e) { + console.log('Failed to write JSON in writeJsonSafe, ignoring: ' + e); + } +}; + +DebugProxy.prototype.disconnectJsonClient = function () { + if (this.socket) { + this.socket.destroy(); + this.socket = null; + } +}; + +DebugProxy.prototype.disconnectTarget = function () { + if (this.inputParser) { + this.inputParser.close(); + this.inputParser = null; + } + if (this.targetStream) { + this.targetStream.destroy(); + this.targetStream = null; + } +}; + +DebugProxy.prototype.run = function () { + var _this = this; + + console.log('Waiting for client connections on port ' + this.serverPort); + this.server = net.createServer(function (socket) { + console.log('JSON proxy client connected'); + + _this.disconnectJsonClient(); + _this.disconnectTarget(); + + // A byline-parser is simple and good enough for now (assume + // compact JSON with no newlines). + var socketByline = byline(socket); + _this.socket = socket; + + socketByline.on('data', function (line) { + try { + // console.log('Received json proxy input line: ' + line.toString('utf8')); + var msg = JSON.parse(line.toString('utf8')); + var first_dval; + var args_dvalues = _this.formatDvalues(msg.args); + var last_dval = _this.dval_eom; + var cmd; + + if (msg.request) { + // "request" can be a string or "true" + first_dval = _this.dval_req; + cmd = _this.determineCommandNumber(msg.request, msg.command); + } else if (msg.reply) { + first_dval = _this.dval_rep; + } else if (msg.notify) { + // "notify" can be a string or "true" + first_dval = _this.dval_nfy; + cmd = _this.determineCommandNumber(msg.notify, msg.command); + } else if (msg.error) { + first_dval = _this.dval_err; + } else { + throw new Error('Invalid input JSON message: ' + JSON.stringify(msg)); + } + + _this.targetStream.write(first_dval); + if (cmd) { + _this.targetStream.write(formatDebugValue(cmd)); + } + args_dvalues.forEach(function (v) { + _this.targetStream.write(v); + }); + _this.targetStream.write(last_dval); + } catch (e) { + console.log(e); + + _this.writeJsonSafe({ + notify: '_Error', + args: [ 'Failed to handle input json message: ' + e ] + }); + + _this.disconnectJsonClient(); + _this.disconnectTarget(); + } + }); + + _this.connectToTarget(); + }).listen(this.serverPort); +}; + +DebugProxy.prototype.connectToTarget = function () { + var _this = this; + + console.log('Connecting to ' + optTargetHost + ':' + optTargetPort + '...'); + this.targetStream = new net.Socket(); + this.targetStream.connect(optTargetPort, optTargetHost, function () { + console.log('Debug transport connected'); + }); + + this.inputParser = new DebugProtocolParser( + this.targetStream, + null, + optDumpDebugRead, + optDumpDebugPretty, + optDumpDebugPretty ? 'Recv: ' : null, + null, + null // console logging is done at a higher level to match request/response + ); + + // Don't add a 'value' key to numbers. + this.inputParser.readableNumberValue = false; + + this.inputParser.on('transport-close', function () { + console.log('Debug transport closed'); + + _this.writeJsonSafe({ + notify: '_Disconnecting' + }); + + _this.disconnectJsonClient(); + _this.disconnectTarget(); + }); + + this.inputParser.on('transport-error', function (err) { + console.log('Debug transport error', err); + + _this.writeJsonSafe({ + notify: '_Error', + args: [ String(err) ] + }); + }); + + this.inputParser.on('protocol-version', function (msg) { + var ver = msg.protocolVersion; + console.log('Debug version identification:', msg.versionIdentification); + + _this.writeJson({ + notify: '_TargetConnected', + args: [ msg.versionIdentification ] // raw identification string + }); + + if (ver !== 1) { + console.log('Protocol version ' + ver + ' unsupported, dropping connection'); + } + }); + + this.inputParser.on('debug-message', function (msg) { + var t; + + //console.log(msg); + + if (typeof msg[0] !== 'object' || msg[0] === null) { + throw new Error('unexpected initial dvalue: ' + msg[0]); + } else if (msg[0].type === 'eom') { + throw new Error('unexpected initial dvalue: ' + msg[0]); + } else if (msg[0].type === 'req') { + if (typeof msg[1] !== 'number') { + throw new Error('unexpected request command number: ' + msg[1]); + } + t = { + request: _this.commandNumberToString(msg[1]), + command: msg[1], + args: msg.slice(2, msg.length - 1) + }; + _this.writeJson(t); + } else if (msg[0].type === 'rep') { + t = { + reply: true, + args: msg.slice(1, msg.length - 1) + }; + _this.writeJson(t); + } else if (msg[0].type === 'err') { + t = { + error: true, + args: msg.slice(1, msg.length - 1) + }; + _this.writeJson(t); + } else if (msg[0].type === 'nfy') { + if (typeof msg[1] !== 'number') { + throw new Error('unexpected notify command number: ' + msg[1]); + } + t = { + notify: _this.commandNumberToString(msg[1]), + command: msg[1], + args: msg.slice(2, msg.length - 1) + }; + _this.writeJson(t); + } else { + throw new Error('unexpected initial dvalue: ' + msg[0]); + } + }); + + this.inputParser.on('stats-update', function () { + }); +}; + +/* + * Command line parsing and initialization + */ + +function main() { + console.log('((o) Duktape debugger'); + + // Parse arguments. + + var argv = require('minimist')(process.argv.slice(2)); + //console.dir(argv); + if (argv['target-host']) { + optTargetHost = argv['target-host']; + } + if (argv['target-port']) { + optTargetPort = argv['target-port']; + } + if (argv['http-port']) { + optHttpPort = argv['http-port']; + } + if (argv['json-proxy-port']) { + optJsonProxyPort = argv['json-proxy-port']; + } + if (argv['json-proxy']) { + optJsonProxy = argv['json-proxy']; + } + if (argv['source-dirs']) { + optSourceSearchDirs = argv['source-dirs'].split(path.delimiter); + } + if (argv['dump-debug-read']) { + optDumpDebugRead = argv['dump-debug-read']; + } + if (argv['dump-debug-write']) { + optDumpDebugWrite = argv['dump-debug-write']; + } + if (argv['dump-debug-pretty']) { + optDumpDebugPretty = argv['dump-debug-pretty']; + } + if (argv['log-messages']) { + optLogMessages = true; + } + + // Dump effective options. Also provides a list of option names. + + console.log(''); + console.log('Effective options:'); + console.log(' --target-host: ' + optTargetHost); + console.log(' --target-port: ' + optTargetPort); + console.log(' --http-port: ' + optHttpPort); + console.log(' --json-proxy-port: ' + optJsonProxyPort); + console.log(' --json-proxy: ' + optJsonProxy); + console.log(' --source-dirs: ' + optSourceSearchDirs.join(' ')); + console.log(' --dump-debug-read: ' + optDumpDebugRead); + console.log(' --dump-debug-write: ' + optDumpDebugWrite); + console.log(' --dump-debug-pretty: ' + optDumpDebugPretty); + console.log(' --log-messages: ' + optLogMessages); + console.log(''); + + // Create debugger and web UI singletons, tie them together and + // start them. + + if (optJsonProxy) { + console.log('Starting in JSON proxy mode, JSON port: ' + optJsonProxyPort); + + var prx = new DebugProxy(optJsonProxyPort); + prx.run(); + } else { + var dbg = new Debugger(); + var web = new DebugWebServer(); + dbg.web = web; + web.dbg = dbg; + dbg.run(); + web.run(); + } +} + +main(); |