2 * Minimal debug web console for Duktape command line tool
4 * See debugger/README.rst.
6 * The web UI socket.io communication can easily become a bottleneck and
7 * it's important to ensure that the web UI remains responsive. Basic rate
8 * limiting mechanisms (token buckets, suppressing identical messages, etc)
9 * are used here now. Ideally the web UI would pull data on its own terms
10 * which would provide natural rate limiting.
12 * Promises are used to structure callback chains.
14 * https://github.com/petkaantonov/bluebird
15 * https://github.com/petkaantonov/bluebird/blob/master/API.md
16 * https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns
19 var Promise
= require('bluebird');
20 var events
= require('events');
21 var stream
= require('stream');
22 var path
= require('path');
23 var fs
= require('fs');
24 var net
= require('net');
25 var byline
= require('byline');
26 var util
= require('util');
27 var readline
= require('readline');
28 var sprintf
= require('sprintf').sprintf
;
29 var utf8
= require('utf8');
30 var wrench
= require('wrench'); // https://github.com/ryanmcgrath/wrench-js
31 var yaml
= require('yamljs');
33 // Command line options (defaults here, overwritten if necessary)
34 var optTargetHost
= '127.0.0.1';
35 var optTargetPort
= 9091;
36 var optHttpPort
= 9092;
37 var optJsonProxyPort
= 9093;
38 var optJsonProxy
= false;
39 var optSourceSearchDirs
= [ '../tests/ecmascript' ];
40 var optDumpDebugRead
= null;
41 var optDumpDebugWrite
= null;
42 var optDumpDebugPretty
= null;
43 var optLogMessages
= false;
46 var UI_MESSAGE_CLIPLEN
= 128;
47 var LOCALS_CLIPLEN
= 64;
48 var EVAL_CLIPLEN
= 4096;
49 var GETVAR_CLIPLEN
= 4096;
51 // Commands initiated by Duktape
52 var CMD_STATUS
= 0x01;
57 var CMD_DETACHING
= 0x06;
59 // Commands initiated by the debug client (= us)
60 var CMD_BASICINFO
= 0x10;
61 var CMD_TRIGGERSTATUS
= 0x11;
63 var CMD_RESUME
= 0x13;
64 var CMD_STEPINTO
= 0x14;
65 var CMD_STEPOVER
= 0x15;
66 var CMD_STEPOUT
= 0x16;
67 var CMD_LISTBREAK
= 0x17;
68 var CMD_ADDBREAK
= 0x18;
69 var CMD_DELBREAK
= 0x19;
70 var CMD_GETVAR
= 0x1a;
71 var CMD_PUTVAR
= 0x1b;
72 var CMD_GETCALLSTACK
= 0x1c;
73 var CMD_GETLOCALS
= 0x1d;
75 var CMD_DETACH
= 0x1f;
76 var CMD_DUMPHEAP
= 0x20;
77 var CMD_GETBYTECODE
= 0x21;
80 var ERR_UNKNOWN
= 0x00;
81 var ERR_UNSUPPORTED
= 0x01;
82 var ERR_TOOMANY
= 0x02;
83 var ERR_NOTFOUND
= 0x03;
85 // Marker objects for special protocol values
86 var DVAL_EOM
= { type
: 'eom' };
87 var DVAL_REQ
= { type
: 'req' };
88 var DVAL_REP
= { type
: 'rep' };
89 var DVAL_ERR
= { type
: 'err' };
90 var DVAL_NFY
= { type
: 'nfy' };
92 // String map for commands (debug dumping). A single map works (instead of
93 // separate maps for each direction) because command numbers don't currently
94 // overlap. So merge the YAML metadata.
95 var debugCommandMeta
= yaml
.load('duk_debugcommands.yaml');
96 var debugCommandNames
= []; // list of command names, merged client/target
97 debugCommandMeta
.target_commands
.forEach(function (k
, i
) {
98 debugCommandNames
[i
] = k
;
100 debugCommandMeta
.client_commands
.forEach(function (k
, i
) { // override
101 debugCommandNames
[i
] = k
;
103 var debugCommandNumbers
= {}; // map from (merged) command name to number
104 debugCommandNames
.forEach(function (k
, i
) {
105 debugCommandNumbers
[k
] = i
;
108 // Duktape heaphdr type constants, must match C headers
109 var DUK_HTYPE_STRING
= 1;
110 var DUK_HTYPE_OBJECT
= 2;
111 var DUK_HTYPE_BUFFER
= 3;
113 // Duktape internal class numbers, must match C headers
114 var dukClassNameMeta
= yaml
.load('duk_classnames.yaml');
115 var dukClassNames
= dukClassNameMeta
.class_names
;
117 // Bytecode opcode/extraop metadata
118 var dukOpcodes
= yaml
.load('duk_opcodes.yaml');
119 if (dukOpcodes
.opcodes
.length
!= 64) {
120 throw new Error('opcode metadata length incorrect');
122 if (dukOpcodes
.extra
.length
!= 256) {
123 throw new Error('extraop metadata length incorrect');
127 * Miscellaneous helpers
130 var nybbles
= '0123456789abcdef';
132 /* Convert a buffer into a string using Unicode codepoints U+0000...U+00FF.
133 * This is the NodeJS 'binary' encoding, but since it's being deprecated,
134 * reimplement it here. We need to avoid parsing strings as e.g. UTF-8:
135 * although Duktape strings are usually UTF-8/CESU-8 that's not always the
136 * case, e.g. for internal strings. Buffer values are also represented as
137 * strings in the debug protocol, so we must deal accurately with arbitrary
140 function bufferToDebugString(buf
) {
145 // This fails with "RangeError: Maximum call stack size exceeded" for some
146 // reason, so use a much slower variant.
148 for (i = 0, n = buf.length; i < n; i++) {
152 return String.fromCharCode.apply(String, cp);
155 for (i
= 0, n
= buf
.length
; i
< n
; i
++) {
156 cp
[i
] = String
.fromCharCode(buf
[i
]);
162 /* Write a string into a buffer interpreting codepoints U+0000...U+00FF
163 * as bytes. Drop higher bits.
165 function writeDebugStringToBuffer(str
, buf
, off
) {
168 for (i
= 0, n
= str
.length
; i
< n
; i
++) {
169 buf
[off
+ i
] = str
.charCodeAt(i
) & 0xff; // truncate higher bits
173 /* Encode an ordinary Unicode string into a dvalue compatible format, i.e.
174 * into a byte array represented as codepoints U+0000...U+00FF. Concretely,
175 * encode with UTF-8 and then represent the bytes with U+0000...U+00FF.
177 function stringToDebugString(str
) {
178 return utf8
.encode(str
);
181 /* Pretty print a dvalue. Useful for dumping etc. */
182 function prettyDebugValue(x
) {
183 if (typeof x
=== 'object' && x
!== null) {
184 if (x
.type
=== 'eom') {
186 } else if (x
.type
=== 'req') {
188 } else if (x
.type
=== 'rep') {
190 } else if (x
.type
=== 'err') {
192 } else if (x
.type
=== 'nfy') {
196 return JSON
.stringify(x
);
199 /* Pretty print a number for UI usage. Types and values should be easy to
200 * read and typing should be obvious. For numbers, support Infinity, NaN,
201 * and signed zeroes properly.
203 function prettyUiNumber(x
) {
204 if (x
=== 1 / 0) { return 'Infinity'; }
205 if (x
=== -1 / 0) { return '-Infinity'; }
206 if (Number
.isNaN(x
)) { return 'NaN'; }
207 if (x
=== 0 && 1 / x
> 0) { return '0'; }
208 if (x
=== 0 && 1 / x
< 0) { return '-0'; }
212 /* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
213 * for UI usage. Try UTF-8 decoding to get a nice Unicode string (JSON
214 * encoded) but if that fails, ensure that bytes are encoded transparently.
215 * The result is a quoted string with a special quote marker for a "raw"
216 * string when UTF-8 decoding fails. Very long strings are optionally
219 function prettyUiString(x
, cliplen
) {
222 if (typeof x
!== 'string') {
223 throw new Error('invalid input to prettyUiString: ' + typeof x
);
226 // Here utf8.decode() is better than decoding using NodeJS buffer
227 // operations because we want strict UTF-8 interpretation.
228 ret
= JSON
.stringify(utf8
.decode(x
));
230 // When we fall back to representing bytes, indicate that the string
231 // is "raw" with a 'r"' prefix (a somewhat arbitrary convention).
232 // U+0022 = ", U+0027 = '
233 ret
= 'r"' + x
.replace(/[\u0022\u0027\u0000-\u001f\u0080-\uffff]/g, function (match
) {
234 var cp
= match
.charCodeAt(0);
235 return '\\x' + nybbles
[(cp
>> 4) & 0x0f] + nybbles
[cp
& 0x0f];
239 if (cliplen
&& ret
.length
> cliplen
) {
240 ret
= ret
.substring(0, cliplen
) + '...'; // trailing '"' intentionally missing
245 /* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
246 * for UI usage without quotes.
248 function prettyUiStringUnquoted(x
, cliplen
) {
251 if (typeof x
!== 'string') {
252 throw new Error('invalid input to prettyUiStringUnquoted: ' + typeof x
);
256 // Here utf8.decode() is better than decoding using NodeJS buffer
257 // operations because we want strict UTF-8 interpretation.
259 // XXX: unprintable characters etc? In some UI cases we'd want to
260 // e.g. escape newlines and in others not.
261 ret
= utf8
.decode(x
);
263 // For the unquoted version we don't need to escape single or double
265 ret
= x
.replace(/[\u0000-\u001f\u0080-\uffff]/g, function (match
) {
266 var cp
= match
.charCodeAt(0);
267 return '\\x' + nybbles
[(cp
>> 4) & 0x0f] + nybbles
[cp
& 0x0f];
271 if (cliplen
&& ret
.length
> cliplen
) {
272 ret
= ret
.substring(0, cliplen
) + '...';
277 /* Pretty print a dvalue for UI usage. Everything comes out as a ready-to-use
280 * XXX: Currently the debug client formats all values for UI use. A better
281 * solution would be to pass values in typed form and let the UI format them,
282 * so that styling etc. could take typing into account.
284 function prettyUiDebugValue(x
, cliplen
) {
285 if (typeof x
=== 'object' && x
!== null) {
286 // Note: typeof null === 'object', so null special case explicitly
287 if (x
.type
=== 'eom') {
289 } else if (x
.type
=== 'req') {
291 } else if (x
.type
=== 'rep') {
293 } else if (x
.type
=== 'err') {
295 } else if (x
.type
=== 'nfy') {
297 } else if (x
.type
=== 'unused') {
299 } else if (x
.type
=== 'undefined') {
301 } else if (x
.type
=== 'buffer') {
302 return '|' + x
.data
+ '|';
303 } else if (x
.type
=== 'object') {
304 return '[object ' + (dukClassNames
[x
.class] || ('class ' + x
.class)) + ' ' + x
.pointer
+ ']';
305 } else if (x
.type
=== 'pointer') {
306 return '<pointer ' + x
.pointer
+ '>';
307 } else if (x
.type
=== 'lightfunc') {
308 return '<lightfunc 0x' + x
.flags
.toString(16) + ' ' + x
.pointer
+ '>';
309 } else if (x
.type
=== 'number') {
310 // duk_tval number, any IEEE double
311 var tmp
= new Buffer(x
.data
, 'hex'); // decode into hex
312 var val
= tmp
.readDoubleBE(0); // big endian ieee double
313 return prettyUiNumber(val
);
315 } else if (x
=== null) {
317 } else if (typeof x
=== 'boolean') {
318 return x
? 'true' : 'false';
319 } else if (typeof x
=== 'string') {
320 return prettyUiString(x
, cliplen
);
321 } else if (typeof x
=== 'number') {
322 // Debug protocol integer
323 return prettyUiNumber(x
);
326 // We shouldn't come here, but if we do, JSON is a reasonable default.
327 return JSON
.stringify(x
);
330 /* Pretty print a debugger message given as an array of parsed dvalues.
331 * Result should be a pure ASCII one-liner.
333 function prettyDebugMessage(msg
) {
334 return msg
.map(prettyDebugValue
).join(' ');
337 /* Pretty print a debugger command. */
338 function prettyDebugCommand(cmd
) {
339 return debugCommandNames
[cmd
] || String(cmd
);
342 /* Decode and normalize source file contents: UTF-8, tabs to 8,
345 function decodeAndNormalizeSource(data
) {
347 var lines
, line
, repl
;
352 tmp
= data
.toString('utf8');
354 console
.log('Failed to UTF-8 decode source file, ignoring: ' + e
);
358 lines
= tmp
.split(/\r?\n/);
359 for (i
= 0, n
= lines
.length
; i
< n
; i
++) {
361 if (/\t/.test(line
)) {
363 for (j
= 0, m
= line
.length
; j
< m
; j
++) {
364 if (line
.charAt(j
) === '\t') {
366 while ((repl
.length
% 8) != 0) {
370 repl
+= line
.charAt(j
);
377 // XXX: normalize last newline (i.e. force a newline if contents don't
378 // end with a newline)?
380 return lines
.join('\n');
383 /* Token bucket rate limiter for a given callback. Calling code calls
384 * trigger() to request 'cb' to be called, and the rate limiter ensures
385 * that 'cb' is not called too often.
387 function RateLimited(tokens
, rate
, cb
) {
389 this.maxTokens
= tokens
;
390 this.tokens
= this.maxTokens
;
393 this.delayedCb
= false;
395 // Right now the implementation is setInterval-based, but could also be
396 // made timerless. There are so few rate limited resources that this
397 // doesn't matter in practice.
399 this.tokenAdder
= setInterval(function () {
400 if (_this
.tokens
< _this
.maxTokens
) {
403 if (_this
.delayedCb
) {
404 _this
.delayedCb
= false;
410 RateLimited
.prototype.trigger = function () {
411 if (this.tokens
> 0) {
415 this.delayedCb
= true;
420 * Source file manager
422 * Scan the list of search directories for Ecmascript source files and
423 * build an index of them. Provides a mechanism to find a source file
424 * based on a raw 'fileName' property provided by the debug target, and
425 * to provide a file list for the web UI.
427 * NOTE: it's tempting to do loose matching for filenames, but this does
428 * not work in practice. Filenames must match 1:1 with the debug target
429 * so that e.g. breakpoints assigned based on filenames found from the
430 * search paths will match 1:1 on the debug target. If this is not the
431 * case, breakpoints won't work as expected.
434 function SourceFileManager(directories
) {
435 this.directories
= directories
;
436 this.extensions
= { '.js': true, '.jsm': true };
440 SourceFileManager
.prototype.scan = function () {
442 var fileMap
= {}; // absFn -> true
445 this.directories
.forEach(function (dir
) {
446 console
.log('Scanning source files: ' + dir
);
448 wrench
.readdirSyncRecursive(dir
).forEach(function (fn
) {
449 var absFn
= path
.normalize(path
.join(dir
, fn
)); // './foo/bar.js' -> 'foo/bar.js'
452 if (fs
.existsSync(absFn
) &&
453 fs
.lstatSync(absFn
).isFile() &&
454 _this
.extensions
[path
.extname(fn
)]) {
455 // We want the fileMap to contain the filename relative to
456 // the search dir root.
461 console
.log('Failed to scan ' + dir
+ ': ' + e
);
465 files
= Object
.keys(fileMap
);
469 console
.log('Found ' + files
.length
+ ' source files in ' + this.directories
.length
+ ' search directories');
472 SourceFileManager
.prototype.getFiles = function () {
476 SourceFileManager
.prototype.search = function (fileName
) {
479 // Loose matching is tempting but counterproductive: filenames must
480 // match 1:1 between the debug client and the debug target for e.g.
481 // breakpoints to work as expected. Note that a breakpoint may be
482 // assigned by selecting a file from a dropdown populated by scanning
483 // the filesystem for available sources and there's no way of knowing
484 // if the debug target uses the exact same name.
486 function tryLookup() {
489 for (i
= 0; i
< _this
.directories
.length
; i
++) {
490 fn
= path
.join(_this
.directories
[i
], fileName
);
491 if (fs
.existsSync(fn
) && fs
.lstatSync(fn
).isFile()) {
492 data
= fs
.readFileSync(fn
); // Raw bytes
493 return decodeAndNormalizeSource(data
); // Unicode string
499 return tryLookup(fileName
);
503 * Debug protocol parser
505 * The debug protocol parser is an EventEmitter which parses debug messages
506 * from an input stream and emits 'debug-message' events for completed
507 * messages ending in an EOM. The parser also provides debug dumping, stream
508 * logging functionality, and statistics gathering functionality.
510 * This parser is used to parse both incoming and outgoing messages. For
511 * outgoing messages the only function is to validate and debug dump the
512 * messages we're about to send. The downside of dumping at this low level
513 * is that we can't match request and reply/error messages here.
515 * http://www.sitepoint.com/nodejs-events-and-eventemitter/
518 function DebugProtocolParser(inputStream
,
523 hexDumpConsolePrefix
,
524 textDumpConsolePrefix
) {
526 this.inputStream
= inputStream
;
527 this.closed
= false; // stream is closed/broken, don't parse anymore
533 this.bytesPerSec
= 0;
534 this.statsTimer
= null;
535 this.readableNumberValue
= true;
537 events
.EventEmitter
.call(this);
539 var buf
= new Buffer(0); // accumulate data
540 var msg
= []; // accumulated message until EOM
541 var versionIdentification
;
543 var statsInterval
= 2000;
544 var statsIntervalSec
= statsInterval
/ 1000;
545 this.statsTimer
= setInterval(function () {
546 _this
.bytesPerSec
= (_this
.bytes
- _this
.prevBytes
) / statsIntervalSec
;
547 _this
.prevBytes
= _this
.bytes
;
548 _this
.emit('stats-update');
551 function consume(n
) {
552 var tmp
= new Buffer(buf
.length
- n
);
557 inputStream
.on('data', function (data
) {
558 var i
, n
, x
, v
, gotValue
, len
, t
, tmpbuf
, verstr
;
561 if (_this
.closed
|| !_this
.inputStream
) {
562 console
.log('Ignoring incoming data from closed input stream, len ' + data
.length
);
566 _this
.bytes
+= data
.length
;
567 if (rawDumpFileName
) {
568 fs
.appendFileSync(rawDumpFileName
, data
);
570 if (hexDumpConsolePrefix
) {
571 console
.log(hexDumpConsolePrefix
+ data
.toString('hex'));
574 buf
= Buffer
.concat([ buf
, data
]);
576 // Protocol version handling. When dumping an output stream, the
577 // caller gives a non-null protocolVersion so we don't read one here.
578 if (protocolVersion
== null) {
579 if (buf
.length
> 1024) {
580 _this
.emit('transport-error', 'Parse error (version identification too long), dropping connection');
585 for (i
= 0, n
= buf
.length
; i
< n
; i
++) {
586 if (buf
[i
] == 0x0a) {
587 tmpbuf
= new Buffer(i
);
588 buf
.copy(tmpbuf
, 0, 0, i
);
590 verstr
= tmpbuf
.toString('utf-8');
591 t
= verstr
.split(' ');
592 protocolVersion
= Number(t
[0]);
593 versionIdentification
= verstr
;
595 _this
.emit('protocol-version', {
596 protocolVersion
: protocolVersion
,
597 versionIdentification
: versionIdentification
603 if (protocolVersion
== null) {
604 // Still waiting for version identification to complete.
609 // Parse complete dvalues (quite inefficient now) by trial parsing.
610 // Consume a value only when it's fully present in 'buf'.
611 // See doc/debugger.rst for format description.
613 while (buf
.length
> 0) {
616 gotValue
= false; // used to flag special values like undefined
619 // 0xc0...0xff: integers 0-16383
620 if (buf
.length
>= 2) {
621 v
= ((x
- 0xc0) << 8) + buf
[1];
624 } else if (x
>= 0x80) {
625 // 0x80...0xbf: integers 0-63
628 } else if (x
>= 0x60) {
629 // 0x60...0x7f: strings with length 0-31
631 if (buf
.length
>= 1 + len
) {
633 buf
.copy(v
, 0, 1, 1 + len
);
634 v
= bufferToDebugString(v
);
639 case 0x00: v
= DVAL_EOM
; consume(1); break;
640 case 0x01: v
= DVAL_REQ
; consume(1); break;
641 case 0x02: v
= DVAL_REP
; consume(1); break;
642 case 0x03: v
= DVAL_ERR
; consume(1); break;
643 case 0x04: v
= DVAL_NFY
; consume(1); break;
644 case 0x10: // 4-byte signed integer
645 if (buf
.length
>= 5) {
646 v
= buf
.readInt32BE(1);
650 case 0x11: // 4-byte string
651 if (buf
.length
>= 5) {
652 len
= buf
.readUInt32BE(1);
653 if (buf
.length
>= 5 + len
) {
655 buf
.copy(v
, 0, 5, 5 + len
);
656 v
= bufferToDebugString(v
);
661 case 0x12: // 2-byte string
662 if (buf
.length
>= 3) {
663 len
= buf
.readUInt16BE(1);
664 if (buf
.length
>= 3 + len
) {
666 buf
.copy(v
, 0, 3, 3 + len
);
667 v
= bufferToDebugString(v
);
672 case 0x13: // 4-byte buffer
673 if (buf
.length
>= 5) {
674 len
= buf
.readUInt32BE(1);
675 if (buf
.length
>= 5 + len
) {
677 buf
.copy(v
, 0, 5, 5 + len
);
678 v
= { type
: 'buffer', data
: v
.toString('hex') };
680 // Value could be a Node.js buffer directly, but
681 // we prefer all dvalues to be JSON compatible
685 case 0x14: // 2-byte buffer
686 if (buf
.length
>= 3) {
687 len
= buf
.readUInt16BE(1);
688 if (buf
.length
>= 3 + len
) {
690 buf
.copy(v
, 0, 3, 3 + len
);
691 v
= { type
: 'buffer', data
: v
.toString('hex') };
693 // Value could be a Node.js buffer directly, but
694 // we prefer all dvalues to be JSON compatible
698 case 0x15: // unused/none
699 v
= { type
: 'unused' };
702 case 0x16: // undefined
703 v
= { type
: 'undefined' };
704 gotValue
= true; // indicate 'v' is actually set
709 gotValue
= true; // indicate 'v' is actually set
720 case 0x1a: // number (IEEE double), big endian
721 if (buf
.length
>= 9) {
723 buf
.copy(v
, 0, 1, 9);
724 v
= { type
: 'number', data
: v
.toString('hex') };
726 if (_this
.readableNumberValue
) {
727 // The value key should not be used programmatically,
728 // it is just there to make the dumps more readable.
729 v
.value
= buf
.readDoubleBE(1);
735 if (buf
.length
>= 3) {
737 if (buf
.length
>= 3 + len
) {
739 buf
.copy(v
, 0, 3, 3 + len
);
740 v
= { type
: 'object', 'class': buf
[1], pointer
: v
.toString('hex') };
745 case 0x1c: // pointer
746 if (buf
.length
>= 2) {
748 if (buf
.length
>= 2 + len
) {
750 buf
.copy(v
, 0, 2, 2 + len
);
751 v
= { type
: 'pointer', pointer
: v
.toString('hex') };
756 case 0x1d: // lightfunc
757 if (buf
.length
>= 4) {
759 if (buf
.length
>= 4 + len
) {
761 buf
.copy(v
, 0, 4, 4 + len
);
762 v
= { type
: 'lightfunc', flags
: buf
.readUInt16BE(1), pointer
: v
.toString('hex') };
767 case 0x1e: // heapptr
768 if (buf
.length
>= 2) {
770 if (buf
.length
>= 2 + len
) {
772 buf
.copy(v
, 0, 2, 2 + len
);
773 v
= { type
: 'heapptr', pointer
: v
.toString('hex') };
779 _this
.emit('transport-error', 'Parse error, dropping connection');
784 if (typeof v
=== 'undefined' && !gotValue
) {
790 // Could emit a 'debug-value' event here, but that's not necessary
791 // because the receiver will just collect statistics which can also
792 // be done using the finished message.
794 if (v
=== DVAL_EOM
) {
797 if (textDumpFileName
|| textDumpConsolePrefix
) {
798 prettyMsg
= prettyDebugMessage(msg
);
799 if (textDumpFileName
) {
800 fs
.appendFileSync(textDumpFileName
, (textDumpFilePrefix
|| '') + prettyMsg
+ '\n');
802 if (textDumpConsolePrefix
) {
803 console
.log(textDumpConsolePrefix
+ prettyMsg
);
807 _this
.emit('debug-message', msg
);
808 msg
= []; // new object, old may be in circulation for a while
813 // Not all streams will emit this.
814 inputStream
.on('error', function (err
) {
815 _this
.emit('transport-error', err
);
819 // Not all streams will emit this.
820 inputStream
.on('close', function () {
824 DebugProtocolParser
.prototype = Object
.create(events
.EventEmitter
.prototype);
826 DebugProtocolParser
.prototype.close = function () {
827 // Although the underlying transport may not have a close() or destroy()
828 // method or even a 'close' event, this method is always available and
829 // will generate a 'transport-close'.
831 // The caller is responsible for closing the underlying stream if that
834 if (this.closed
) { return; }
837 if (this.statsTimer
) {
838 clearInterval(this.statsTimer
);
839 this.statsTimer
= null;
841 this.emit('transport-close');
845 * Debugger output formatting
848 function formatDebugValue(v
) {
851 // See doc/debugger.rst for format description.
853 if (typeof v
=== 'object' && v
!== null) {
854 // Note: typeof null === 'object', so null special case explicitly
855 if (v
.type
=== 'eom') {
856 return new Buffer([ 0x00 ]);
857 } else if (v
.type
=== 'req') {
858 return new Buffer([ 0x01 ]);
859 } else if (v
.type
=== 'rep') {
860 return new Buffer([ 0x02 ]);
861 } else if (v
.type
=== 'err') {
862 return new Buffer([ 0x03 ]);
863 } else if (v
.type
=== 'nfy') {
864 return new Buffer([ 0x04 ]);
865 } else if (v
.type
=== 'unused') {
866 return new Buffer([ 0x15 ]);
867 } else if (v
.type
=== 'undefined') {
868 return new Buffer([ 0x16 ]);
869 } else if (v
.type
=== 'number') {
870 dec
= new Buffer(v
.data
, 'hex');
873 throw new TypeError('value cannot be converted to dvalue: ' + JSON
.stringify(v
));
875 buf
= new Buffer(1 + len
);
879 } else if (v
.type
=== 'buffer') {
880 dec
= new Buffer(v
.data
, 'hex');
883 buf
= new Buffer(3 + len
);
885 buf
[1] = (len
>> 8) & 0xff;
886 buf
[2] = (len
>> 0) & 0xff;
890 buf
= new Buffer(5 + len
);
892 buf
[1] = (len
>> 24) & 0xff;
893 buf
[2] = (len
>> 16) & 0xff;
894 buf
[3] = (len
>> 8) & 0xff;
895 buf
[4] = (len
>> 0) & 0xff;
899 } else if (v
.type
=== 'object') {
900 dec
= new Buffer(v
.pointer
, 'hex');
902 buf
= new Buffer(3 + len
);
908 } else if (v
.type
=== 'pointer') {
909 dec
= new Buffer(v
.pointer
, 'hex');
911 buf
= new Buffer(2 + len
);
916 } else if (v
.type
=== 'lightfunc') {
917 dec
= new Buffer(v
.pointer
, 'hex');
919 buf
= new Buffer(4 + len
);
921 buf
[1] = (v
.flags
>> 8) & 0xff;
922 buf
[2] = v
.flags
& 0xff;
926 } else if (v
.type
=== 'heapptr') {
927 dec
= new Buffer(v
.pointer
, 'hex');
929 buf
= new Buffer(2 + len
);
935 } else if (v
=== null) {
936 return new Buffer([ 0x17 ]);
937 } else if (typeof v
=== 'boolean') {
938 return new Buffer([ v
? 0x18 : 0x19 ]);
939 } else if (typeof v
=== 'number') {
940 if (Math
.floor(v
) === v
&& /* whole */
941 (v
!== 0 || 1 / v
> 0) && /* not negative zero */
942 v
>= -0x80000000 && v
<= 0x7fffffff) {
943 // Represented signed 32-bit integers as plain integers.
944 // Debugger code expects this for all fields that are not
945 // duk_tval representations (e.g. command numbers and such).
946 if (v
>= 0x00 && v
<= 0x3f) {
947 return new Buffer([ 0x80 + v
]);
948 } else if (v
>= 0x0000 && v
<= 0x3fff) {
949 return new Buffer([ 0xc0 + (v
>> 8), v
& 0xff ]);
950 } else if (v
>= -0x80000000 && v
<= 0x7fffffff) {
951 return new Buffer([ 0x10,
957 throw new Error('internal error when encoding integer to dvalue: ' + v
);
960 // Represent non-integers as IEEE double dvalues
961 buf
= new Buffer(1 + 8);
963 buf
.writeDoubleBE(v
, 1);
966 } else if (typeof v
=== 'string') {
967 if (v
.length
< 0 || v
.length
> 0xffffffff) {
968 // Not possible in practice.
969 throw new TypeError('cannot convert to dvalue, invalid string length: ' + v
.length
);
971 if (v
.length
<= 0x1f) {
972 buf
= new Buffer(1 + v
.length
);
973 buf
[0] = 0x60 + v
.length
;
974 writeDebugStringToBuffer(v
, buf
, 1);
976 } else if (v
.length
<= 0xffff) {
977 buf
= new Buffer(3 + v
.length
);
979 buf
[1] = (v
.length
>> 8) & 0xff;
980 buf
[2] = (v
.length
>> 0) & 0xff;
981 writeDebugStringToBuffer(v
, buf
, 3);
984 buf
= new Buffer(5 + v
.length
);
986 buf
[1] = (v
.length
>> 24) & 0xff;
987 buf
[2] = (v
.length
>> 16) & 0xff;
988 buf
[3] = (v
.length
>> 8) & 0xff;
989 buf
[4] = (v
.length
>> 0) & 0xff;
990 writeDebugStringToBuffer(v
, buf
, 5);
995 // Shouldn't come here.
996 throw new TypeError('value cannot be converted to dvalue: ' + JSON
.stringify(v
));
1000 * Debugger implementation
1002 * A debugger instance communicates with the debug target and maintains
1003 * persistent debug state so that the current state can be resent to the
1004 * socket.io client (web UI) if it reconnects. Whenever the debugger state
1005 * is changed an event is generated. The socket.io handler will listen to
1006 * state change events and push the necessary updates to the web UI, often
1007 * in a rate limited fashion or using a client pull to ensure the web UI
1008 * is not overloaded.
1010 * The debugger instance assumes that if the debug protocol connection is
1011 * re-established, it is always to the same target. There is no separate
1012 * abstraction for a debugger session.
1015 function Debugger() {
1016 events
.EventEmitter
.call(this);
1018 this.web
= null; // web UI singleton
1019 this.targetStream
= null; // transport connection to target
1020 this.outputPassThroughStream
= null; // dummy passthrough for message dumping
1021 this.inputParser
= null; // parser for incoming debug messages
1022 this.outputParser
= null; // parser for outgoing debug messages (stats, dumping)
1023 this.protocolVersion
= null;
1024 this.dukVersion
= null;
1025 this.dukGitDescribe
= null;
1026 this.targetInfo
= null;
1027 this.attached
= false;
1028 this.handshook
= false;
1029 this.reqQueue
= null;
1030 this.stats
= { // stats for current debug connection
1031 rxBytes
: 0, rxDvalues
: 0, rxMessages
: 0, rxBytesPerSec
: 0,
1032 txBytes
: 0, txDvalues
: 0, txMessages
: 0, txBytesPerSec
: 0
1042 this.breakpoints
= [];
1043 this.callstack
= [];
1045 this.messageLines
= [];
1046 this.messageScrollBack
= 100;
1048 Debugger
.prototype = events
.EventEmitter
.prototype;
1050 Debugger
.prototype.decodeBytecodeFromBuffer = function (buf
, consts
, funcs
) {
1051 var i
, j
, n
, m
, ins
, pc
;
1053 var op
, str
, args
, comments
;
1055 // XXX: add constants inline to preformatted output (e.g. for strings,
1056 // add a short escaped snippet as a comment on the line after the
1057 // compact argument list).
1059 for (i
= 0, n
= buf
.length
; i
< n
; i
+= 4) {
1062 // shift forces unsigned
1063 if (this.endianness
=== 'little') {
1064 ins
= buf
.readInt32LE(i
) >>> 0;
1066 ins
= buf
.readInt32BE(i
) >>> 0;
1069 op
= dukOpcodes
.opcodes
[ins
& 0x3f];
1071 op
= dukOpcodes
.extra
[(ins
>> 6) & 0xff];
1077 for (j
= 0, m
= op
.args
.length
; j
< m
; j
++) {
1078 switch (op
.args
[j
]) {
1079 case 'A_R': args
.push('r' + ((ins
>>> 6) & 0xff)); break;
1080 case 'A_RI': args
.push('r' + ((ins
>>> 6) & 0xff) + '(indirect)'); break;
1081 case 'A_C': args
.push('c' + ((ins
>>> 6) & 0xff)); break;
1082 case 'A_H': args
.push('0x' + ((ins
>>> 6) & 0xff).toString(16)); break;
1083 case 'A_I': args
.push(((ins
>>> 6) & 0xff).toString(10)); break;
1084 case 'A_B': args
.push(((ins
>>> 6) & 0xff) ? 'true' : 'false'); break;
1085 case 'B_RC': args
.push((ins
& (1 << 22) ? 'c' : 'r') + ((ins
>>> 14) & 0x0ff)); break;
1086 case 'B_R': args
.push('r' + ((ins
>>> 14) & 0x1ff)); break;
1087 case 'B_RI': args
.push('r' + ((ins
>>> 14) & 0x1ff) + '(indirect)'); break;
1088 case 'B_C': args
.push('c' + ((ins
>>> 14) & 0x1ff)); break;
1089 case 'B_H': args
.push('0x' + ((ins
>>> 14) & 0x1ff).toString(16)); break;
1090 case 'B_I': args
.push(((ins
>>> 14) & 0x1ff).toString(10)); break;
1091 case 'C_RC': args
.push((ins
& (1 << 31) ? 'c' : 'r') + ((ins
>>> 23) & 0x0ff)); break;
1092 case 'C_R': args
.push('r' + ((ins
>>> 23) & 0x1ff)); break;
1093 case 'C_RI': args
.push('r' + ((ins
>>> 23) & 0x1ff) + '(indirect)'); break;
1094 case 'C_C': args
.push('c' + ((ins
>>> 23) & 0x1ff)); break;
1095 case 'C_H': args
.push('0x' + ((ins
>>> 23) & 0x1ff).toString(16)); break;
1096 case 'C_I': args
.push(((ins
>>> 23) & 0x1ff).toString(10)); break;
1097 case 'BC_R': args
.push('r' + ((ins
>>> 14) & 0x3ffff)); break;
1098 case 'BC_C': args
.push('c' + ((ins
>>> 14) & 0x3ffff)); break;
1099 case 'BC_H': args
.push('0x' + ((ins
>>> 14) & 0x3ffff).toString(16)); break;
1100 case 'BC_I': args
.push(((ins
>>> 14) & 0x3ffff).toString(10)); break;
1101 case 'ABC_H': args
.push(((ins
>>> 6) & 0x03ffffff).toString(16)); break;
1102 case 'ABC_I': args
.push(((ins
>>> 6) & 0x03ffffff).toString(10)); break;
1103 case 'BC_LDINT': args
.push(((ins
>>> 14) & 0x3ffff) - (1 << 17)); break;
1104 case 'BC_LDINTX': args
.push(((ins
>>> 14) & 0x3ffff) - 0); break; // no bias in LDINTX
1106 var pc_add
= ((ins
>>> 6) & 0x03ffffff) - (1 << 25) + 1; // pc is preincremented before adding
1107 var pc_dst
= pc
+ pc_add
;
1108 args
.push(pc_dst
+ ' (' + (pc_add
>= 0 ? '+' : '') + pc_add
+ ')');
1111 default: args
.push('?'); break;
1116 for (j
= 0, m
= op
.flags
.length
; j
< m
; j
++) {
1117 if (ins
& op
.flags
[j
].mask
) {
1118 comments
.push(op
.flags
[j
].name
);
1123 if (args
.length
> 0) {
1124 str
= sprintf('%05d %08x %-10s %s', pc
, ins
, op
.name
, args
.join(', '));
1126 str
= sprintf('%05d %08x %-10s', pc
, ins
, op
.name
);
1128 if (comments
.length
> 0) {
1129 str
= sprintf('%-44s ; %s', str
, comments
.join(', '));
1141 Debugger
.prototype.uiMessage = function (type
, val
) {
1143 if (typeof type
=== 'object') {
1145 } else if (typeof type
=== 'string') {
1146 msg
= { type
: type
, message
: val
};
1148 throw new TypeError('invalid ui message: ' + type
);
1150 this.messageLines
.push(msg
);
1151 while (this.messageLines
.length
> this.messageScrollBack
) {
1152 this.messageLines
.shift();
1154 this.emit('ui-message-update'); // just trigger a sync, gets rate limited
1157 Debugger
.prototype.sendRequest = function (msg
) {
1159 return new Promise(function (resolve
, reject
) {
1165 if (!_this
.attached
|| !_this
.handshook
|| !_this
.reqQueue
|| !_this
.targetStream
) {
1166 throw new Error('invalid state for sendRequest');
1169 for (i
= 0; i
< msg
.length
; i
++) {
1171 dval
= formatDebugValue(msg
[i
]);
1173 console
.log('Failed to format dvalue, dropping connection: ' + e
);
1174 console
.log(e
.stack
|| e
);
1175 _this
.targetStream
.destroy();
1176 throw new Error('failed to format dvalue');
1181 data
= Buffer
.concat(dvals
);
1183 _this
.targetStream
.write(data
);
1184 _this
.outputPassThroughStream
.write(data
); // stats and dumping
1186 if (optLogMessages
) {
1187 console
.log('Request ' + prettyDebugCommand(msg
[1]) + ': ' + prettyDebugMessage(msg
));
1190 if (!_this
.reqQueue
) {
1191 throw new Error('no reqQueue');
1194 _this
.reqQueue
.push({
1203 Debugger
.prototype.sendBasicInfoRequest = function () {
1205 return this.sendRequest([ DVAL_REQ
, CMD_BASICINFO
, DVAL_EOM
]).then(function (msg
) {
1206 _this
.dukVersion
= msg
[1];
1207 _this
.dukGitDescribe
= msg
[2];
1208 _this
.targetInfo
= msg
[3];
1209 _this
.endianness
= { 1: 'little', 2: 'mixed', 3: 'big' }[msg
[4]] || 'unknown';
1210 _this
.emit('basic-info-update');
1215 Debugger
.prototype.sendGetVarRequest = function (varname
, level
) {
1217 return this.sendRequest([ DVAL_REQ
, CMD_GETVAR
, varname
, (typeof level
=== 'number' ? level
: -1), DVAL_EOM
]).then(function (msg
) {
1218 return { found
: msg
[1] === 1, value
: msg
[2] };
1222 Debugger
.prototype.sendPutVarRequest = function (varname
, varvalue
, level
) {
1224 return this.sendRequest([ DVAL_REQ
, CMD_PUTVAR
, varname
, varvalue
, (typeof level
=== 'number' ? level
: -1), DVAL_EOM
]);
1227 Debugger
.prototype.sendInvalidCommandTestRequest = function () {
1228 // Intentional invalid command
1230 return this.sendRequest([ DVAL_REQ
, 0xdeadbeef, DVAL_EOM
]);
1233 Debugger
.prototype.sendStatusRequest = function () {
1234 // Send a status request to trigger a status notify, result is ignored:
1235 // target sends a status notify instead of a meaningful reply
1237 return this.sendRequest([ DVAL_REQ
, CMD_TRIGGERSTATUS
, DVAL_EOM
]);
1240 Debugger
.prototype.sendBreakpointListRequest = function () {
1242 return this.sendRequest([ DVAL_REQ
, CMD_LISTBREAK
, DVAL_EOM
]).then(function (msg
) {
1246 for (i
= 1, n
= msg
.length
- 1; i
< n
; i
+= 2) {
1247 breakpts
.push({ fileName
: msg
[i
], lineNumber
: msg
[i
+ 1] });
1250 _this
.breakpoints
= breakpts
;
1251 _this
.emit('breakpoints-update');
1256 Debugger
.prototype.sendGetLocalsRequest = function (level
) {
1258 return this.sendRequest([ DVAL_REQ
, CMD_GETLOCALS
, (typeof level
=== 'number' ? level
: -1), DVAL_EOM
]).then(function (msg
) {
1262 for (i
= 1; i
<= msg
.length
- 2; i
+= 2) {
1263 // XXX: do pretty printing in debug client for now
1264 locals
.push({ key
: msg
[i
], value
: prettyUiDebugValue(msg
[i
+ 1], LOCALS_CLIPLEN
) });
1267 _this
.locals
= locals
;
1268 _this
.emit('locals-update');
1273 Debugger
.prototype.sendGetCallStackRequest = function () {
1275 return this.sendRequest([ DVAL_REQ
, CMD_GETCALLSTACK
, DVAL_EOM
]).then(function (msg
) {
1279 for (i
= 1; i
+ 3 <= msg
.length
- 1; i
+= 4) {
1282 funcName
: msg
[i
+ 1],
1283 lineNumber
: msg
[i
+ 2],
1288 _this
.callstack
= stack
;
1289 _this
.emit('callstack-update');
1294 Debugger
.prototype.sendStepIntoRequest = function () {
1296 return this.sendRequest([ DVAL_REQ
, CMD_STEPINTO
, DVAL_EOM
]);
1299 Debugger
.prototype.sendStepOverRequest = function () {
1301 return this.sendRequest([ DVAL_REQ
, CMD_STEPOVER
, DVAL_EOM
]);
1304 Debugger
.prototype.sendStepOutRequest = function () {
1306 return this.sendRequest([ DVAL_REQ
, CMD_STEPOUT
, DVAL_EOM
]);
1309 Debugger
.prototype.sendPauseRequest = function () {
1311 return this.sendRequest([ DVAL_REQ
, CMD_PAUSE
, DVAL_EOM
]);
1314 Debugger
.prototype.sendResumeRequest = function () {
1316 return this.sendRequest([ DVAL_REQ
, CMD_RESUME
, DVAL_EOM
]);
1319 Debugger
.prototype.sendEvalRequest = function (evalInput
, level
) {
1321 return this.sendRequest([ DVAL_REQ
, CMD_EVAL
, evalInput
, (typeof level
=== 'number' ? level
: -1), DVAL_EOM
]).then(function (msg
) {
1322 return { error
: msg
[1] === 1 /*error*/, value
: msg
[2] };
1326 Debugger
.prototype.sendDetachRequest = function () {
1328 return this.sendRequest([ DVAL_REQ
, CMD_DETACH
, DVAL_EOM
]);
1331 Debugger
.prototype.sendDumpHeapRequest = function () {
1334 return this.sendRequest([ DVAL_REQ
, CMD_DUMPHEAP
, DVAL_EOM
]).then(function (msg
) {
1337 var i
, j
, n
, m
, o
, prop
;
1339 res
.type
= 'heapDump';
1340 res
.heapObjects
= objs
;
1342 for (i
= 1, n
= msg
.length
- 1; i
< n
; /*nop*/) {
1346 o
.flags
= msg
[i
++] >>> 0; /* unsigned */
1349 if (o
.type
=== DUK_HTYPE_STRING
) {
1352 o
.hash
= msg
[i
++] >>> 0; /* unsigned */
1354 } else if (o
.type
=== DUK_HTYPE_BUFFER
) {
1357 } else if (o
.type
=== DUK_HTYPE_OBJECT
) {
1358 o
['class'] = msg
[i
++];
1365 for (j
= 0, m
= o
.enext
; j
< m
; j
++) {
1367 prop
.flags
= msg
[i
++];
1368 prop
.key
= msg
[i
++];
1369 prop
.accessor
= (msg
[i
++] == 1);
1370 if (prop
.accessor
) {
1371 prop
.getter
= msg
[i
++];
1372 prop
.setter
= msg
[i
++];
1374 prop
.value
= msg
[i
++];
1379 for (j
= 0, m
= o
.asize
; j
< m
; j
++) {
1381 prop
.value
= msg
[i
++];
1385 console
.log('invalid htype: ' + o
.type
+ ', disconnect');
1386 _this
.disconnectDebugger();
1387 throw new Error('invalid htype');
1398 Debugger
.prototype.sendGetBytecodeRequest = function () {
1401 return this.sendRequest([ DVAL_REQ
, CMD_GETBYTECODE
, DVAL_EOM
]).then(function (msg
) {
1413 var idxPreformattedInstructions
;
1415 //console.log(JSON.stringify(msg));
1417 nconst
= msg
[idx
++];
1418 for (i
= 0; i
< nconst
; i
++) {
1424 for (i
= 0; i
< nfunc
; i
++) {
1430 // Right now bytecode is a string containing a direct dump of the
1431 // bytecode in target endianness. Decode here so that the web UI
1434 buf
= new Buffer(val
.length
);
1435 writeDebugStringToBuffer(val
, buf
, 0);
1436 bcode
= _this
.decodeBytecodeFromBuffer(buf
, consts
, funcs
);
1439 consts
.forEach(function (v
, i
) {
1440 preformatted
.push('; c' + i
+ ' ' + JSON
.stringify(v
));
1442 preformatted
.push('');
1443 idxPreformattedInstructions
= preformatted
.length
;
1444 bcode
.forEach(function (v
) {
1445 preformatted
.push(v
.str
);
1447 preformatted
= preformatted
.join('\n') + '\n';
1453 preformatted
: preformatted
,
1454 idxPreformattedInstructions
: idxPreformattedInstructions
1461 Debugger
.prototype.changeBreakpoint = function (fileName
, lineNumber
, mode
) {
1464 return this.sendRequest([ DVAL_REQ
, CMD_LISTBREAK
, DVAL_EOM
]).then(function (msg
) {
1467 var deleted
= false;
1469 // Up-to-date list of breakpoints on target
1470 for (i
= 1, n
= msg
.length
- 1; i
< n
; i
+= 2) {
1471 breakpts
.push({ fileName
: msg
[i
], lineNumber
: msg
[i
+ 1] });
1474 // Delete matching breakpoints in reverse order so that indices
1475 // remain valid. We do this for all operations so that duplicates
1476 // are eliminated if present.
1477 for (i
= breakpts
.length
- 1; i
>= 0; i
--) {
1478 var bp
= breakpts
[i
];
1479 if (mode
=== 'deleteall' || (bp
.fileName
=== fileName
&& bp
.lineNumber
=== lineNumber
)) {
1481 _this
.sendRequest([ DVAL_REQ
, CMD_DELBREAK
, i
, DVAL_EOM
], function (msg
) {
1489 // Technically we should wait for each delbreak reply but because
1490 // target processes the requests in order, it doesn't matter.
1491 if ((mode
=== 'add') || (mode
=== 'toggle' && !deleted
)) {
1492 _this
.sendRequest([ DVAL_REQ
, CMD_ADDBREAK
, fileName
, lineNumber
, DVAL_EOM
], function (msg
) {
1495 _this
.uiMessage('debugger-info', 'Failed to add breakpoint: ' + err
);
1499 // Read final, effective breakpoints from the target
1500 _this
.sendBreakpointListRequest();
1504 Debugger
.prototype.disconnectDebugger = function () {
1505 if (this.targetStream
) {
1506 // We require a destroy() method from the actual target stream
1507 this.targetStream
.destroy();
1508 this.targetStream
= null;
1510 if (this.inputParser
) {
1511 this.inputParser
.close();
1512 this.inputParser
= null;
1514 if (this.outputPassThroughStream
) {
1515 // There is no close() or destroy() for a passthrough stream, so just
1516 // close the outputParser which will cancel timers etc.
1518 if (this.outputParser
) {
1519 this.outputParser
.close();
1520 this.outputParser
= null;
1523 this.attached
= false;
1524 this.handshook
= false;
1525 this.reqQueue
= null;
1536 Debugger
.prototype.connectDebugger = function () {
1539 this.disconnectDebugger(); // close previous target connection
1541 // CUSTOMTRANSPORT: to use a custom transport, change this.targetStream to
1542 // use your custom transport.
1544 console
.log('Connecting to ' + optTargetHost
+ ':' + optTargetPort
+ '...');
1545 this.targetStream
= new net
.Socket();
1546 this.targetStream
.connect(optTargetPort
, optTargetHost
, function () {
1547 console
.log('Debug transport connected');
1548 _this
.attached
= true;
1549 _this
.reqQueue
= [];
1550 _this
.uiMessage('debugger-info', 'Debug transport connected');
1553 this.inputParser
= new DebugProtocolParser(
1558 optDumpDebugPretty
? 'Recv: ' : null,
1560 null // console logging is done at a higher level to match request/response
1563 // Use a PassThrough stream to debug dump and get stats for output messages.
1564 // Simply write outgoing data to both the targetStream and this passthrough
1566 this.outputPassThroughStream
= stream
.PassThrough();
1567 this.outputParser
= new DebugProtocolParser(
1568 this.outputPassThroughStream
,
1572 optDumpDebugPretty
? 'Send: ' : null,
1574 null // console logging is done at a higher level to match request/response
1577 this.inputParser
.on('transport-close', function () {
1578 _this
.uiMessage('debugger-info', 'Debug transport closed');
1579 _this
.disconnectDebugger();
1580 _this
.emit('exec-status-update');
1581 _this
.emit('detached');
1584 this.inputParser
.on('transport-error', function (err
) {
1585 _this
.uiMessage('debugger-info', 'Debug transport error: ' + err
);
1586 _this
.disconnectDebugger();
1589 this.inputParser
.on('protocol-version', function (msg
) {
1590 var ver
= msg
.protocolVersion
;
1591 console
.log('Debug version identification:', msg
.versionIdentification
);
1592 _this
.protocolVersion
= ver
;
1593 _this
.uiMessage('debugger-info', 'Debug version identification: ' + msg
.versionIdentification
);
1595 _this
.uiMessage('debugger-info', 'Protocol version ' + ver
+ ' unsupported, dropping connection');
1596 _this
.targetStream
.destroy();
1598 _this
.uiMessage('debugger-info', 'Debug protocol version: ' + ver
);
1599 _this
.handshook
= true;
1600 _this
.execStatus
= {
1608 _this
.emit('exec-status-update');
1609 _this
.emit('attached'); // inform web UI
1611 // Fetch basic info right away
1612 _this
.sendBasicInfoRequest();
1616 this.inputParser
.on('debug-message', function (msg
) {
1617 _this
.processDebugMessage(msg
);
1620 this.inputParser
.on('stats-update', function () {
1621 _this
.stats
.rxBytes
= this.bytes
;
1622 _this
.stats
.rxDvalues
= this.dvalues
;
1623 _this
.stats
.rxMessages
= this.messages
;
1624 _this
.stats
.rxBytesPerSec
= this.bytesPerSec
;
1625 _this
.emit('debug-stats-update');
1628 this.outputParser
.on('stats-update', function () {
1629 _this
.stats
.txBytes
= this.bytes
;
1630 _this
.stats
.txDvalues
= this.dvalues
;
1631 _this
.stats
.txMessages
= this.messages
;
1632 _this
.stats
.txBytesPerSec
= this.bytesPerSec
;
1633 _this
.emit('debug-stats-update');
1637 Debugger
.prototype.processDebugMessage = function (msg
) {
1639 var prevState
, newState
;
1642 if (msg
[0] === DVAL_REQ
) {
1643 // No actual requests sent by the target right now (just notifys).
1644 console
.log('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg
));
1645 } else if (msg
[0] === DVAL_REP
) {
1646 if (this.reqQueue
.length
<= 0) {
1647 console
.log('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg
));
1648 this.targetStream
.destroy();
1650 req
= this.reqQueue
.shift();
1652 if (optLogMessages
) {
1653 console
.log('Reply for ' + prettyDebugCommand(req
.reqCmd
) + ': ' + prettyDebugMessage(msg
));
1656 if (req
.resolveCb
) {
1661 } else if (msg
[0] === DVAL_ERR
) {
1662 if (this.reqQueue
.length
<= 0) {
1663 console
.log('Unsolicited error message, dropping connection: ' + prettyDebugMessage(msg
));
1664 this.targetStream
.destroy();
1666 err
= new Error(String(msg
[2]) + ' (code ' + String(msg
[1]) + ')');
1667 err
.errorCode
= msg
[1] || 0;
1668 req
= this.reqQueue
.shift();
1670 if (optLogMessages
) {
1671 console
.log('Error for ' + prettyDebugCommand(req
.reqCmd
) + ': ' + prettyDebugMessage(msg
));
1679 } else if (msg
[0] === DVAL_NFY
) {
1680 if (optLogMessages
) {
1681 console
.log('Notify ' + prettyDebugCommand(msg
[1]) + ': ' + prettyDebugMessage(msg
));
1684 if (msg
[1] === CMD_STATUS
) {
1685 prevState
= this.execStatus
.state
;
1686 newState
= msg
[2] === 0 ? 'running' : 'paused';
1696 if (prevState
!== newState
&& newState
=== 'paused') {
1697 // update run state now that we're paused
1698 this.sendBreakpointListRequest();
1699 this.sendGetLocalsRequest();
1700 this.sendGetCallStackRequest();
1703 this.emit('exec-status-update');
1704 } else if (msg
[1] === CMD_PRINT
) {
1705 this.uiMessage('print', prettyUiStringUnquoted(msg
[2], UI_MESSAGE_CLIPLEN
));
1706 } else if (msg
[1] === CMD_ALERT
) {
1707 this.uiMessage('alert', prettyUiStringUnquoted(msg
[2], UI_MESSAGE_CLIPLEN
));
1708 } else if (msg
[1] === CMD_LOG
) {
1709 this.uiMessage({ type
: 'log', level
: msg
[2], message
: prettyUiStringUnquoted(msg
[3], UI_MESSAGE_CLIPLEN
) });
1710 } else if (msg
[1] === CMD_THROW
) {
1711 this.uiMessage({ type
: 'throw', fatal
: msg
[2], message
: (msg
[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg
[3], UI_MESSAGE_CLIPLEN
), fileName
: msg
[4], lineNumber
: msg
[5] });
1712 } else if (msg
[1] === CMD_DETACHING
) {
1713 this.uiMessage({ type
: 'detaching', reason
: msg
[2], message
: 'DETACH: ' + (msg
.length
>= 5 ? prettyUiStringUnquoted(msg
[3]) : 'detaching') });
1715 // Ignore unknown notify messages
1716 console
.log('Unknown notify, ignoring: ' + prettyDebugMessage(msg
));
1718 //this.targetStream.destroy();
1721 console
.log('Invalid initial dvalue, dropping connection: ' + prettyDebugMessage(msg
));
1722 this.targetStream
.destroy();
1726 Debugger
.prototype.run = function () {
1729 // Initial debugger connection
1731 this.connectDebugger();
1733 // Poll various state items when running
1736 var statusPending
= false;
1737 var bplistPending
= false;
1738 var localsPending
= false;
1739 var callStackPending
= false;
1741 setInterval(function () {
1742 if (_this
.execStatus
.state
!== 'running') {
1746 // Could also check for an empty request queue, but that's probably
1749 // Pending flags are used to avoid requesting the same thing twice
1750 // while a previous request is pending. The flag-based approach is
1751 // quite awkward. Rework to use promises.
1753 switch (sendRound
) {
1755 if (!statusPending
) {
1756 statusPending
= true;
1757 _this
.sendStatusRequest().finally(function () { statusPending
= false; });
1761 if (!bplistPending
) {
1762 bplistPending
= true;
1763 _this
.sendBreakpointListRequest().finally(function () { bplistPending
= false; });
1767 if (!localsPending
) {
1768 localsPending
= true;
1769 _this
.sendGetLocalsRequest().finally(function () { localsPending
= false; });
1773 if (!callStackPending
) {
1774 callStackPending
= true;
1775 _this
.sendGetCallStackRequest().finally(function () { callStackPending
= false; });
1779 sendRound
= (sendRound
+ 1) % 4;
1784 * Express setup and socket.io
1787 function DebugWebServer() {
1788 this.dbg
= null; // debugger singleton
1789 this.socket
= null; // current socket (or null)
1790 this.keepaliveTimer
= null;
1791 this.uiMessageLimiter
= null;
1792 this.cachedJson
= {}; // cache to avoid resending identical data
1793 this.sourceFileManager
= new SourceFileManager(optSourceSearchDirs
);
1794 this.sourceFileManager
.scan();
1797 DebugWebServer
.prototype.handleSourcePost = function (req
, res
) {
1798 var fileName
= req
.body
&& req
.body
.fileName
;
1801 console
.log('Source request: ' + fileName
);
1803 if (typeof fileName
!== 'string') {
1804 res
.status(500).send('invalid request');
1807 fileData
= this.sourceFileManager
.search(fileName
, optSourceSearchDirs
);
1808 if (typeof fileData
!== 'string') {
1809 res
.status(404).send('not found');
1812 res
.status(200).send(fileData
); // UTF-8
1815 DebugWebServer
.prototype.handleSourceListPost = function (req
, res
) {
1816 console
.log('Source list request');
1818 var files
= this.sourceFileManager
.getFiles();
1819 res
.header('Content-Type', 'application/json');
1820 res
.status(200).json(files
);
1823 DebugWebServer
.prototype.handleHeapDumpGet = function (req
, res
) {
1824 console
.log('Heap dump get');
1826 this.dbg
.sendDumpHeapRequest().then(function (val
) {
1827 res
.header('Content-Type', 'application/json');
1828 //res.status(200).json(val);
1829 res
.status(200).send(JSON
.stringify(val
, null, 4));
1830 }).catch(function (err
) {
1831 res
.status(500).send('Failed to get heap dump: ' + (err
.stack
|| err
));
1835 DebugWebServer
.prototype.run = function () {
1838 var express
= require('express');
1839 var bodyParser
= require('body-parser');
1840 var app
= express();
1841 var http
= require('http').Server(app
);
1842 var io
= require('socket.io')(http
);
1844 app
.use(bodyParser
.json());
1845 app
.post('/source', this.handleSourcePost
.bind(this));
1846 app
.post('/sourceList', this.handleSourceListPost
.bind(this));
1847 app
.get('/heapDump.json', this.handleHeapDumpGet
.bind(this));
1848 app
.use('/', express
.static(__dirname
+ '/static'));
1850 http
.listen(optHttpPort
, function () {
1851 console
.log('Listening on *:' + optHttpPort
);
1854 io
.on('connection', this.handleNewSocketIoConnection
.bind(this));
1856 this.dbg
.on('attached', function () {
1857 console
.log('Debugger attached');
1860 this.dbg
.on('detached', function () {
1861 console
.log('Debugger detached');
1864 this.dbg
.on('debug-stats-update', function () {
1865 _this
.debugStatsLimiter
.trigger();
1868 this.dbg
.on('ui-message-update', function () {
1869 // Explicit rate limiter because this is a source of a lot of traffic.
1870 _this
.uiMessageLimiter
.trigger();
1873 this.dbg
.on('basic-info-update', function () {
1874 _this
.emitBasicInfo();
1877 this.dbg
.on('breakpoints-update', function () {
1878 _this
.emitBreakpoints();
1881 this.dbg
.on('exec-status-update', function () {
1882 // Explicit rate limiter because this is a source of a lot of traffic.
1883 _this
.execStatusLimiter
.trigger();
1886 this.dbg
.on('locals-update', function () {
1890 this.dbg
.on('callstack-update', function () {
1891 _this
.emitCallStack();
1894 this.uiMessageLimiter
= new RateLimited(10, 1000, this.uiMessageLimiterCallback
.bind(this));
1895 this.execStatusLimiter
= new RateLimited(50, 500, this.execStatusLimiterCallback
.bind(this));
1896 this.debugStatsLimiter
= new RateLimited(1, 2000, this.debugStatsLimiterCallback
.bind(this));
1898 this.keepaliveTimer
= setInterval(this.emitKeepalive
.bind(this), 30000);
1901 DebugWebServer
.prototype.handleNewSocketIoConnection = function (socket
) {
1904 console
.log('Socket.io connected');
1906 console
.log('Closing previous socket.io socket');
1907 this.socket
.emit('replaced');
1909 this.socket
= socket
;
1911 this.emitKeepalive();
1913 socket
.on('disconnect', function () {
1914 console
.log('Socket.io disconnected');
1915 if (_this
.socket
=== socket
) {
1916 _this
.socket
= null;
1920 socket
.on('keepalive', function (msg
) {
1924 socket
.on('attach', function (msg
) {
1925 if (_this
.dbg
.targetStream
) {
1926 console
.log('Attach request when debugger already has a connection, ignoring');
1928 _this
.dbg
.connectDebugger();
1932 socket
.on('detach', function (msg
) {
1933 // Try to detach cleanly, timeout if no response
1935 _this
.dbg
.sendDetachRequest(),
1937 ]).finally(function () {
1938 _this
.dbg
.disconnectDebugger();
1942 socket
.on('stepinto', function (msg
) {
1943 _this
.dbg
.sendStepIntoRequest();
1946 socket
.on('stepover', function (msg
) {
1947 _this
.dbg
.sendStepOverRequest();
1950 socket
.on('stepout', function (msg
) {
1951 _this
.dbg
.sendStepOutRequest();
1954 socket
.on('pause', function (msg
) {
1955 _this
.dbg
.sendPauseRequest();
1958 socket
.on('resume', function (msg
) {
1959 _this
.dbg
.sendResumeRequest();
1962 socket
.on('eval', function (msg
) {
1963 // msg.input is a proper Unicode strings here, and needs to be
1964 // converted into a protocol string (U+0000...U+00FF).
1965 var input
= stringToDebugString(msg
.input
);
1966 _this
.dbg
.sendEvalRequest(input
, msg
.level
).then(function (v
) {
1967 socket
.emit('eval-result', { error
: v
.error
, result
: prettyUiDebugValue(v
.value
, EVAL_CLIPLEN
) });
1970 // An eval call quite possibly changes the local variables so always
1971 // re-read locals afterwards. We don't need to wait for Eval to
1972 // complete here; the requests will pipeline automatically and be
1973 // executed in order.
1975 // XXX: move this to the web UI so that the UI can control what
1976 // locals are listed (or perhaps show locals for all levels with
1977 // an expandable tree view).
1978 _this
.dbg
.sendGetLocalsRequest();
1981 socket
.on('getvar', function (msg
) {
1982 // msg.varname is a proper Unicode strings here, and needs to be
1983 // converted into a protocol string (U+0000...U+00FF).
1984 var varname
= stringToDebugString(msg
.varname
);
1985 _this
.dbg
.sendGetVarRequest(varname
, msg
.level
)
1986 .then(function (v
) {
1987 socket
.emit('getvar-result', { found
: v
.found
, result
: prettyUiDebugValue(v
.value
, GETVAR_CLIPLEN
) });
1991 socket
.on('putvar', function (msg
) {
1992 // msg.varname and msg.varvalue are proper Unicode strings here, they
1993 // need to be converted into protocol strings (U+0000...U+00FF).
1994 var varname
= stringToDebugString(msg
.varname
);
1995 var varvalue
= msg
.varvalue
;
1997 // varvalue is JSON parsed by the web UI for now, need special string
1999 if (typeof varvalue
=== 'string') {
2000 varvalue
= stringToDebugString(msg
.varvalue
);
2003 _this
.dbg
.sendPutVarRequest(varname
, varvalue
, msg
.level
)
2004 .then(function (v
) {
2005 console
.log('putvar done'); // XXX: signal success to UI?
2008 // A PutVar call quite possibly changes the local variables so always
2009 // re-read locals afterwards. We don't need to wait for PutVar to
2010 // complete here; the requests will pipeline automatically and be
2011 // executed in order.
2013 // XXX: make the client do this?
2014 _this
.dbg
.sendGetLocalsRequest();
2017 socket
.on('add-breakpoint', function (msg
) {
2018 _this
.dbg
.changeBreakpoint(msg
.fileName
, msg
.lineNumber
, 'add');
2021 socket
.on('delete-breakpoint', function (msg
) {
2022 _this
.dbg
.changeBreakpoint(msg
.fileName
, msg
.lineNumber
, 'delete');
2025 socket
.on('toggle-breakpoint', function (msg
) {
2026 _this
.dbg
.changeBreakpoint(msg
.fileName
, msg
.lineNumber
, 'toggle');
2029 socket
.on('delete-all-breakpoints', function (msg
) {
2030 _this
.dbg
.changeBreakpoint(null, null, 'deleteall');
2033 socket
.on('get-bytecode', function (msg
) {
2034 _this
.dbg
.sendGetBytecodeRequest().then(function (res
) {
2035 socket
.emit('bytecode', res
);
2039 // Resend all debugger state for new client
2040 this.cachedJson
= {}; // clear client state cache
2041 this.emitBasicInfo();
2043 this.emitExecStatus();
2044 this.emitUiMessages();
2045 this.emitBreakpoints();
2046 this.emitCallStack();
2050 // Check if 'msg' would encode to the same JSON which was previously sent
2051 // to the web client. The caller then avoid resending unnecessary stuff.
2052 DebugWebServer
.prototype.cachedJsonCheck = function (cacheKey
, msg
) {
2053 var newJson
= JSON
.stringify(msg
);
2054 if (this.cachedJson
[cacheKey
] === newJson
) {
2055 return true; // cached
2057 this.cachedJson
[cacheKey
] = newJson
;
2058 return false; // not cached, send (cache already updated)
2061 DebugWebServer
.prototype.uiMessageLimiterCallback = function () {
2062 this.emitUiMessages();
2065 DebugWebServer
.prototype.execStatusLimiterCallback = function () {
2066 this.emitExecStatus();
2069 DebugWebServer
.prototype.debugStatsLimiterCallback = function () {
2073 DebugWebServer
.prototype.emitKeepalive = function () {
2074 if (!this.socket
) { return; }
2076 this.socket
.emit('keepalive', { nodeVersion
: process
.version
});
2079 DebugWebServer
.prototype.emitBasicInfo = function () {
2080 if (!this.socket
) { return; }
2083 duk_version
: this.dbg
.dukVersion
,
2084 duk_git_describe
: this.dbg
.dukGitDescribe
,
2085 target_info
: this.dbg
.targetInfo
,
2086 endianness
: this.dbg
.endianness
2088 if (this.cachedJsonCheck('basic-info', newMsg
)) {
2091 this.socket
.emit('basic-info', newMsg
);
2094 DebugWebServer
.prototype.emitStats = function () {
2095 if (!this.socket
) { return; }
2097 this.socket
.emit('debug-stats', this.dbg
.stats
);
2100 DebugWebServer
.prototype.emitExecStatus = function () {
2101 if (!this.socket
) { return; }
2103 var newMsg
= this.dbg
.execStatus
;
2104 if (this.cachedJsonCheck('exec-status', newMsg
)) {
2107 this.socket
.emit('exec-status', newMsg
);
2110 DebugWebServer
.prototype.emitUiMessages = function () {
2111 if (!this.socket
) { return; }
2113 var newMsg
= this.dbg
.messageLines
;
2114 if (this.cachedJsonCheck('output-lines', newMsg
)) {
2117 this.socket
.emit('output-lines', newMsg
);
2120 DebugWebServer
.prototype.emitBreakpoints = function () {
2121 if (!this.socket
) { return; }
2123 var newMsg
= { breakpoints
: this.dbg
.breakpoints
};
2124 if (this.cachedJsonCheck('breakpoints', newMsg
)) {
2127 this.socket
.emit('breakpoints', newMsg
);
2130 DebugWebServer
.prototype.emitCallStack = function () {
2131 if (!this.socket
) { return; }
2133 var newMsg
= { callstack
: this.dbg
.callstack
};
2134 if (this.cachedJsonCheck('callstack', newMsg
)) {
2137 this.socket
.emit('callstack', newMsg
);
2140 DebugWebServer
.prototype.emitLocals = function () {
2141 if (!this.socket
) { return; }
2143 var newMsg
= { locals
: this.dbg
.locals
};
2144 if (this.cachedJsonCheck('locals', newMsg
)) {
2147 this.socket
.emit('locals', newMsg
);
2154 function DebugProxy(serverPort
) {
2155 this.serverPort
= serverPort
;
2158 this.targetStream
= null;
2159 this.inputParser
= null;
2161 // preformatted dvalues
2162 this.dval_eom
= formatDebugValue(DVAL_EOM
);
2163 this.dval_req
= formatDebugValue(DVAL_REQ
);
2164 this.dval_rep
= formatDebugValue(DVAL_REP
);
2165 this.dval_nfy
= formatDebugValue(DVAL_NFY
);
2166 this.dval_err
= formatDebugValue(DVAL_ERR
);
2169 DebugProxy
.prototype.determineCommandNumber = function (cmdName
, cmdNumber
) {
2171 if (typeof cmdName
=== 'string') {
2172 ret
= debugCommandNumbers
[cmdName
];
2173 } else if (typeof cmdName
=== 'number') {
2176 ret
= ret
|| cmdNumber
;
2177 if (typeof ret
!== 'number') {
2178 throw Error('cannot figure out command number for "' + cmdName
+ '" (' + cmdNumber
+ ')');
2183 DebugProxy
.prototype.commandNumberToString = function (id
) {
2184 return debugCommandNames
[id
] || String(id
);
2187 DebugProxy
.prototype.formatDvalues = function (args
) {
2191 return args
.map(function (v
) {
2192 return formatDebugValue(v
);
2196 DebugProxy
.prototype.writeJson = function (val
) {
2197 this.socket
.write(JSON
.stringify(val
) + '\n');
2200 DebugProxy
.prototype.writeJsonSafe = function (val
) {
2202 this.writeJson(val
);
2204 console
.log('Failed to write JSON in writeJsonSafe, ignoring: ' + e
);
2208 DebugProxy
.prototype.disconnectJsonClient = function () {
2210 this.socket
.destroy();
2215 DebugProxy
.prototype.disconnectTarget = function () {
2216 if (this.inputParser
) {
2217 this.inputParser
.close();
2218 this.inputParser
= null;
2220 if (this.targetStream
) {
2221 this.targetStream
.destroy();
2222 this.targetStream
= null;
2226 DebugProxy
.prototype.run = function () {
2229 console
.log('Waiting for client connections on port ' + this.serverPort
);
2230 this.server
= net
.createServer(function (socket
) {
2231 console
.log('JSON proxy client connected');
2233 _this
.disconnectJsonClient();
2234 _this
.disconnectTarget();
2236 // A byline-parser is simple and good enough for now (assume
2237 // compact JSON with no newlines).
2238 var socketByline
= byline(socket
);
2239 _this
.socket
= socket
;
2241 socketByline
.on('data', function (line
) {
2243 // console.log('Received json proxy input line: ' + line.toString('utf8'));
2244 var msg
= JSON
.parse(line
.toString('utf8'));
2246 var args_dvalues
= _this
.formatDvalues(msg
.args
);
2247 var last_dval
= _this
.dval_eom
;
2251 // "request" can be a string or "true"
2252 first_dval
= _this
.dval_req
;
2253 cmd
= _this
.determineCommandNumber(msg
.request
, msg
.command
);
2254 } else if (msg
.reply
) {
2255 first_dval
= _this
.dval_rep
;
2256 } else if (msg
.notify
) {
2257 // "notify" can be a string or "true"
2258 first_dval
= _this
.dval_nfy
;
2259 cmd
= _this
.determineCommandNumber(msg
.notify
, msg
.command
);
2260 } else if (msg
.error
) {
2261 first_dval
= _this
.dval_err
;
2263 throw new Error('Invalid input JSON message: ' + JSON
.stringify(msg
));
2266 _this
.targetStream
.write(first_dval
);
2268 _this
.targetStream
.write(formatDebugValue(cmd
));
2270 args_dvalues
.forEach(function (v
) {
2271 _this
.targetStream
.write(v
);
2273 _this
.targetStream
.write(last_dval
);
2277 _this
.writeJsonSafe({
2279 args
: [ 'Failed to handle input json message: ' + e
]
2282 _this
.disconnectJsonClient();
2283 _this
.disconnectTarget();
2287 _this
.connectToTarget();
2288 }).listen(this.serverPort
);
2291 DebugProxy
.prototype.connectToTarget = function () {
2294 console
.log('Connecting to ' + optTargetHost
+ ':' + optTargetPort
+ '...');
2295 this.targetStream
= new net
.Socket();
2296 this.targetStream
.connect(optTargetPort
, optTargetHost
, function () {
2297 console
.log('Debug transport connected');
2300 this.inputParser
= new DebugProtocolParser(
2305 optDumpDebugPretty
? 'Recv: ' : null,
2307 null // console logging is done at a higher level to match request/response
2310 // Don't add a 'value' key to numbers.
2311 this.inputParser
.readableNumberValue
= false;
2313 this.inputParser
.on('transport-close', function () {
2314 console
.log('Debug transport closed');
2316 _this
.writeJsonSafe({
2317 notify
: '_Disconnecting'
2320 _this
.disconnectJsonClient();
2321 _this
.disconnectTarget();
2324 this.inputParser
.on('transport-error', function (err
) {
2325 console
.log('Debug transport error', err
);
2327 _this
.writeJsonSafe({
2329 args
: [ String(err
) ]
2333 this.inputParser
.on('protocol-version', function (msg
) {
2334 var ver
= msg
.protocolVersion
;
2335 console
.log('Debug version identification:', msg
.versionIdentification
);
2338 notify
: '_TargetConnected',
2339 args
: [ msg
.versionIdentification
] // raw identification string
2343 console
.log('Protocol version ' + ver
+ ' unsupported, dropping connection');
2347 this.inputParser
.on('debug-message', function (msg
) {
2352 if (typeof msg
[0] !== 'object' || msg
[0] === null) {
2353 throw new Error('unexpected initial dvalue: ' + msg
[0]);
2354 } else if (msg
[0].type
=== 'eom') {
2355 throw new Error('unexpected initial dvalue: ' + msg
[0]);
2356 } else if (msg
[0].type
=== 'req') {
2357 if (typeof msg
[1] !== 'number') {
2358 throw new Error('unexpected request command number: ' + msg
[1]);
2361 request
: _this
.commandNumberToString(msg
[1]),
2363 args
: msg
.slice(2, msg
.length
- 1)
2366 } else if (msg
[0].type
=== 'rep') {
2369 args
: msg
.slice(1, msg
.length
- 1)
2372 } else if (msg
[0].type
=== 'err') {
2375 args
: msg
.slice(1, msg
.length
- 1)
2378 } else if (msg
[0].type
=== 'nfy') {
2379 if (typeof msg
[1] !== 'number') {
2380 throw new Error('unexpected notify command number: ' + msg
[1]);
2383 notify
: _this
.commandNumberToString(msg
[1]),
2385 args
: msg
.slice(2, msg
.length
- 1)
2389 throw new Error('unexpected initial dvalue: ' + msg
[0]);
2393 this.inputParser
.on('stats-update', function () {
2398 * Command line parsing and initialization
2402 console
.log('((o) Duktape debugger');
2406 var argv
= require('minimist')(process
.argv
.slice(2));
2407 //console.dir(argv);
2408 if (argv
['target-host']) {
2409 optTargetHost
= argv
['target-host'];
2411 if (argv
['target-port']) {
2412 optTargetPort
= argv
['target-port'];
2414 if (argv
['http-port']) {
2415 optHttpPort
= argv
['http-port'];
2417 if (argv
['json-proxy-port']) {
2418 optJsonProxyPort
= argv
['json-proxy-port'];
2420 if (argv
['json-proxy']) {
2421 optJsonProxy
= argv
['json-proxy'];
2423 if (argv
['source-dirs']) {
2424 optSourceSearchDirs
= argv
['source-dirs'].split(path
.delimiter
);
2426 if (argv
['dump-debug-read']) {
2427 optDumpDebugRead
= argv
['dump-debug-read'];
2429 if (argv
['dump-debug-write']) {
2430 optDumpDebugWrite
= argv
['dump-debug-write'];
2432 if (argv
['dump-debug-pretty']) {
2433 optDumpDebugPretty
= argv
['dump-debug-pretty'];
2435 if (argv
['log-messages']) {
2436 optLogMessages
= true;
2439 // Dump effective options. Also provides a list of option names.
2442 console
.log('Effective options:');
2443 console
.log(' --target-host: ' + optTargetHost
);
2444 console
.log(' --target-port: ' + optTargetPort
);
2445 console
.log(' --http-port: ' + optHttpPort
);
2446 console
.log(' --json-proxy-port: ' + optJsonProxyPort
);
2447 console
.log(' --json-proxy: ' + optJsonProxy
);
2448 console
.log(' --source-dirs: ' + optSourceSearchDirs
.join(' '));
2449 console
.log(' --dump-debug-read: ' + optDumpDebugRead
);
2450 console
.log(' --dump-debug-write: ' + optDumpDebugWrite
);
2451 console
.log(' --dump-debug-pretty: ' + optDumpDebugPretty
);
2452 console
.log(' --log-messages: ' + optLogMessages
);
2455 // Create debugger and web UI singletons, tie them together and
2459 console
.log('Starting in JSON proxy mode, JSON port: ' + optJsonProxyPort
);
2461 var prx
= new DebugProxy(optJsonProxyPort
);
2464 var dbg
= new Debugger();
2465 var web
= new DebugWebServer();