]> git.proxmox.com Git - ceph.git/blame - ceph/src/civetweb/src/third_party/duktape-1.8.0/debugger/duk_debug.js
buildsys: switch source download to quincy
[ceph.git] / ceph / src / civetweb / src / third_party / duktape-1.8.0 / debugger / duk_debug.js
CommitLineData
7c673cae
FG
1/*
2 * Minimal debug web console for Duktape command line tool
3 *
4 * See debugger/README.rst.
5 *
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.
11 *
12 * Promises are used to structure callback chains.
13 *
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
17 */
18
19var Promise = require('bluebird');
20var events = require('events');
21var stream = require('stream');
22var path = require('path');
23var fs = require('fs');
24var net = require('net');
25var byline = require('byline');
26var util = require('util');
27var readline = require('readline');
28var sprintf = require('sprintf').sprintf;
29var utf8 = require('utf8');
30var wrench = require('wrench'); // https://github.com/ryanmcgrath/wrench-js
31var yaml = require('yamljs');
32
33// Command line options (defaults here, overwritten if necessary)
34var optTargetHost = '127.0.0.1';
35var optTargetPort = 9091;
36var optHttpPort = 9092;
37var optJsonProxyPort = 9093;
38var optJsonProxy = false;
39var optSourceSearchDirs = [ '../tests/ecmascript' ];
40var optDumpDebugRead = null;
41var optDumpDebugWrite = null;
42var optDumpDebugPretty = null;
43var optLogMessages = false;
44
45// Constants
46var UI_MESSAGE_CLIPLEN = 128;
47var LOCALS_CLIPLEN = 64;
48var EVAL_CLIPLEN = 4096;
49var GETVAR_CLIPLEN = 4096;
50
51// Commands initiated by Duktape
52var CMD_STATUS = 0x01;
53var CMD_PRINT = 0x02;
54var CMD_ALERT = 0x03;
55var CMD_LOG = 0x04;
11fdf7f2
TL
56var CMD_THROW = 0x05;
57var CMD_DETACHING = 0x06;
7c673cae
FG
58
59// Commands initiated by the debug client (= us)
60var CMD_BASICINFO = 0x10;
61var CMD_TRIGGERSTATUS = 0x11;
62var CMD_PAUSE = 0x12;
63var CMD_RESUME = 0x13;
64var CMD_STEPINTO = 0x14;
65var CMD_STEPOVER = 0x15;
66var CMD_STEPOUT = 0x16;
67var CMD_LISTBREAK = 0x17;
68var CMD_ADDBREAK = 0x18;
69var CMD_DELBREAK = 0x19;
70var CMD_GETVAR = 0x1a;
71var CMD_PUTVAR = 0x1b;
72var CMD_GETCALLSTACK = 0x1c;
73var CMD_GETLOCALS = 0x1d;
74var CMD_EVAL = 0x1e;
75var CMD_DETACH = 0x1f;
76var CMD_DUMPHEAP = 0x20;
77var CMD_GETBYTECODE = 0x21;
78
79// Errors
80var ERR_UNKNOWN = 0x00;
81var ERR_UNSUPPORTED = 0x01;
82var ERR_TOOMANY = 0x02;
83var ERR_NOTFOUND = 0x03;
84
85// Marker objects for special protocol values
86var DVAL_EOM = { type: 'eom' };
87var DVAL_REQ = { type: 'req' };
88var DVAL_REP = { type: 'rep' };
89var DVAL_ERR = { type: 'err' };
90var DVAL_NFY = { type: 'nfy' };
91
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
11fdf7f2
TL
94// overlap. So merge the YAML metadata.
95var debugCommandMeta = yaml.load('duk_debugcommands.yaml');
96var debugCommandNames = []; // list of command names, merged client/target
97debugCommandMeta.target_commands.forEach(function (k, i) {
98 debugCommandNames[i] = k;
99});
100debugCommandMeta.client_commands.forEach(function (k, i) { // override
101 debugCommandNames[i] = k;
102});
103var debugCommandNumbers = {}; // map from (merged) command name to number
7c673cae
FG
104debugCommandNames.forEach(function (k, i) {
105 debugCommandNumbers[k] = i;
106});
107
108// Duktape heaphdr type constants, must match C headers
109var DUK_HTYPE_STRING = 1;
110var DUK_HTYPE_OBJECT = 2;
111var DUK_HTYPE_BUFFER = 3;
112
113// Duktape internal class numbers, must match C headers
11fdf7f2
TL
114var dukClassNameMeta = yaml.load('duk_classnames.yaml');
115var dukClassNames = dukClassNameMeta.class_names;
7c673cae
FG
116
117// Bytecode opcode/extraop metadata
11fdf7f2 118var dukOpcodes = yaml.load('duk_opcodes.yaml');
7c673cae
FG
119if (dukOpcodes.opcodes.length != 64) {
120 throw new Error('opcode metadata length incorrect');
121}
122if (dukOpcodes.extra.length != 256) {
123 throw new Error('extraop metadata length incorrect');
124}
125
126/*
127 * Miscellaneous helpers
128 */
129
130var nybbles = '0123456789abcdef';
131
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
138 * byte arrays.
139 */
140function bufferToDebugString(buf) {
141 var cp = [];
142 var i, n;
143
144/*
145 // This fails with "RangeError: Maximum call stack size exceeded" for some
146 // reason, so use a much slower variant.
147
148 for (i = 0, n = buf.length; i < n; i++) {
149 cp[i] = buf[i];
150 }
151
152 return String.fromCharCode.apply(String, cp);
153*/
154
155 for (i = 0, n = buf.length; i < n; i++) {
156 cp[i] = String.fromCharCode(buf[i]);
157 }
158
159 return cp.join('');
160}
161
162/* Write a string into a buffer interpreting codepoints U+0000...U+00FF
163 * as bytes. Drop higher bits.
164 */
165function writeDebugStringToBuffer(str, buf, off) {
166 var i, n;
167
168 for (i = 0, n = str.length; i < n; i++) {
11fdf7f2 169 buf[off + i] = str.charCodeAt(i) & 0xff; // truncate higher bits
7c673cae
FG
170 }
171}
172
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.
176 */
177function stringToDebugString(str) {
178 return utf8.encode(str);
179}
180
181/* Pretty print a dvalue. Useful for dumping etc. */
182function prettyDebugValue(x) {
183 if (typeof x === 'object' && x !== null) {
184 if (x.type === 'eom') {
185 return 'EOM';
186 } else if (x.type === 'req') {
187 return 'REQ';
188 } else if (x.type === 'rep') {
189 return 'REP';
190 } else if (x.type === 'err') {
191 return 'ERR';
192 } else if (x.type === 'nfy') {
193 return 'NFY';
194 }
195 }
196 return JSON.stringify(x);
197}
198
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.
202 */
203function prettyUiNumber(x) {
11fdf7f2
TL
204 if (x === 1 / 0) { return 'Infinity'; }
205 if (x === -1 / 0) { return '-Infinity'; }
7c673cae 206 if (Number.isNaN(x)) { return 'NaN'; }
11fdf7f2
TL
207 if (x === 0 && 1 / x > 0) { return '0'; }
208 if (x === 0 && 1 / x < 0) { return '-0'; }
7c673cae
FG
209 return x.toString();
210}
211
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
217 * clipped.
218 */
219function prettyUiString(x, cliplen) {
220 var ret;
221
222 if (typeof x !== 'string') {
223 throw new Error('invalid input to prettyUiString: ' + typeof x);
224 }
225 try {
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));
229 } catch (e) {
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];
236 }) + '"';
237 }
238
239 if (cliplen && ret.length > cliplen) {
240 ret = ret.substring(0, cliplen) + '...'; // trailing '"' intentionally missing
241 }
242 return ret;
243}
244
245/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
246 * for UI usage without quotes.
247 */
248function prettyUiStringUnquoted(x, cliplen) {
249 var ret;
250
251 if (typeof x !== 'string') {
252 throw new Error('invalid input to prettyUiStringUnquoted: ' + typeof x);
253 }
254
255 try {
256 // Here utf8.decode() is better than decoding using NodeJS buffer
257 // operations because we want strict UTF-8 interpretation.
258
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);
262 } catch (e) {
263 // For the unquoted version we don't need to escape single or double
264 // quotes.
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];
268 });
269 }
270
271 if (cliplen && ret.length > cliplen) {
272 ret = ret.substring(0, cliplen) + '...';
273 }
274 return ret;
275}
276
277/* Pretty print a dvalue for UI usage. Everything comes out as a ready-to-use
278 * string.
279 *
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.
283 */
284function 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') {
288 return 'EOM';
289 } else if (x.type === 'req') {
290 return 'REQ';
291 } else if (x.type === 'rep') {
292 return 'REP';
293 } else if (x.type === 'err') {
294 return 'ERR';
295 } else if (x.type === 'nfy') {
296 return 'NFY';
297 } else if (x.type === 'unused') {
298 return 'unused';
299 } else if (x.type === 'undefined') {
300 return '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);
314 }
315 } else if (x === null) {
316 return '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);
324 }
325
326 // We shouldn't come here, but if we do, JSON is a reasonable default.
327 return JSON.stringify(x);
328}
329
330/* Pretty print a debugger message given as an array of parsed dvalues.
331 * Result should be a pure ASCII one-liner.
332 */
333function prettyDebugMessage(msg) {
334 return msg.map(prettyDebugValue).join(' ');
335}
336
337/* Pretty print a debugger command. */
338function prettyDebugCommand(cmd) {
339 return debugCommandNames[cmd] || String(cmd);
340}
341
342/* Decode and normalize source file contents: UTF-8, tabs to 8,
343 * CR LF to LF.
344 */
345function decodeAndNormalizeSource(data) {
346 var tmp;
347 var lines, line, repl;
348 var i, n;
349 var j, m;
350
351 try {
352 tmp = data.toString('utf8');
353 } catch (e) {
354 console.log('Failed to UTF-8 decode source file, ignoring: ' + e);
355 tmp = String(data);
356 }
357
358 lines = tmp.split(/\r?\n/);
359 for (i = 0, n = lines.length; i < n; i++) {
360 line = lines[i];
361 if (/\t/.test(line)) {
362 repl = '';
363 for (j = 0, m = line.length; j < m; j++) {
364 if (line.charAt(j) === '\t') {
365 repl += ' ';
366 while ((repl.length % 8) != 0) {
367 repl += ' ';
368 }
369 } else {
370 repl += line.charAt(j);
371 }
372 }
373 lines[i] = repl;
374 }
375 }
376
377 // XXX: normalize last newline (i.e. force a newline if contents don't
378 // end with a newline)?
379
380 return lines.join('\n');
381}
382
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.
386 */
387function RateLimited(tokens, rate, cb) {
388 var _this = this;
389 this.maxTokens = tokens;
390 this.tokens = this.maxTokens;
391 this.rate = rate;
392 this.cb = cb;
393 this.delayedCb = false;
394
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.
398
399 this.tokenAdder = setInterval(function () {
400 if (_this.tokens < _this.maxTokens) {
401 _this.tokens++;
402 }
403 if (_this.delayedCb) {
404 _this.delayedCb = false;
405 _this.tokens--;
406 _this.cb();
407 }
408 }, this.rate);
409}
410RateLimited.prototype.trigger = function () {
411 if (this.tokens > 0) {
412 this.tokens--;
413 this.cb();
414 } else {
415 this.delayedCb = true;
416 }
417};
418
419/*
420 * Source file manager
421 *
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.
426 *
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.
432 */
433
434function SourceFileManager(directories) {
435 this.directories = directories;
11fdf7f2 436 this.extensions = { '.js': true, '.jsm': true };
7c673cae
FG
437 this.files;
438}
439
440SourceFileManager.prototype.scan = function () {
441 var _this = this;
442 var fileMap = {}; // absFn -> true
443 var files;
444
445 this.directories.forEach(function (dir) {
446 console.log('Scanning source files: ' + dir);
447 try {
448 wrench.readdirSyncRecursive(dir).forEach(function (fn) {
449 var absFn = path.normalize(path.join(dir, fn)); // './foo/bar.js' -> 'foo/bar.js'
450 var ent;
451
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.
457 fileMap[fn] = true;
458 }
459 });
460 } catch (e) {
461 console.log('Failed to scan ' + dir + ': ' + e);
462 }
463 });
464
465 files = Object.keys(fileMap);
466 files.sort();
467 this.files = files;
468
469 console.log('Found ' + files.length + ' source files in ' + this.directories.length + ' search directories');
470};
471
472SourceFileManager.prototype.getFiles = function () {
473 return this.files;
474};
475
476SourceFileManager.prototype.search = function (fileName) {
477 var _this = this;
478
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.
485
486 function tryLookup() {
487 var i, fn, data;
488
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
494 }
495 }
496 return null;
497 }
498
499 return tryLookup(fileName);
500};
501
502/*
503 * Debug protocol parser
504 *
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.
509 *
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.
514 *
515 * http://www.sitepoint.com/nodejs-events-and-eventemitter/
516 */
517
518function DebugProtocolParser(inputStream,
519 protocolVersion,
520 rawDumpFileName,
521 textDumpFileName,
522 textDumpFilePrefix,
523 hexDumpConsolePrefix,
524 textDumpConsolePrefix) {
525 var _this = this;
526 this.inputStream = inputStream;
527 this.closed = false; // stream is closed/broken, don't parse anymore
528 this.bytes = 0;
529 this.dvalues = 0;
530 this.messages = 0;
531 this.requests = 0;
532 this.prevBytes = 0;
533 this.bytesPerSec = 0;
534 this.statsTimer = null;
535 this.readableNumberValue = true;
536
537 events.EventEmitter.call(this);
538
539 var buf = new Buffer(0); // accumulate data
540 var msg = []; // accumulated message until EOM
541 var versionIdentification;
542
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');
549 }, statsInterval);
550
551 function consume(n) {
552 var tmp = new Buffer(buf.length - n);
553 buf.copy(tmp, 0, n);
554 buf = tmp;
555 }
556
557 inputStream.on('data', function (data) {
558 var i, n, x, v, gotValue, len, t, tmpbuf, verstr;
559 var prettyMsg;
560
561 if (_this.closed || !_this.inputStream) {
562 console.log('Ignoring incoming data from closed input stream, len ' + data.length);
563 return;
564 }
565
566 _this.bytes += data.length;
567 if (rawDumpFileName) {
568 fs.appendFileSync(rawDumpFileName, data);
569 }
570 if (hexDumpConsolePrefix) {
571 console.log(hexDumpConsolePrefix + data.toString('hex'));
572 }
573
574 buf = Buffer.concat([ buf, data ]);
575
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');
581 _this.close();
582 return;
583 }
584
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);
589 consume(i + 1);
590 verstr = tmpbuf.toString('utf-8');
591 t = verstr.split(' ');
592 protocolVersion = Number(t[0]);
593 versionIdentification = verstr;
594
595 _this.emit('protocol-version', {
596 protocolVersion: protocolVersion,
597 versionIdentification: versionIdentification
598 });
599 break;
600 }
601 }
602
603 if (protocolVersion == null) {
604 // Still waiting for version identification to complete.
605 return;
606 }
607 }
608
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.
612
613 while (buf.length > 0) {
614 x = buf[0];
615 v = undefined;
616 gotValue = false; // used to flag special values like undefined
617
618 if (x >= 0xc0) {
619 // 0xc0...0xff: integers 0-16383
620 if (buf.length >= 2) {
621 v = ((x - 0xc0) << 8) + buf[1];
622 consume(2);
623 }
624 } else if (x >= 0x80) {
625 // 0x80...0xbf: integers 0-63
626 v = x - 0x80;
627 consume(1);
628 } else if (x >= 0x60) {
629 // 0x60...0x7f: strings with length 0-31
630 len = x - 0x60;
631 if (buf.length >= 1 + len) {
632 v = new Buffer(len);
633 buf.copy(v, 0, 1, 1 + len);
634 v = bufferToDebugString(v);
635 consume(1 + len);
636 }
637 } else {
638 switch (x) {
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);
647 consume(5);
648 }
649 break;
650 case 0x11: // 4-byte string
651 if (buf.length >= 5) {
652 len = buf.readUInt32BE(1);
653 if (buf.length >= 5 + len) {
654 v = new Buffer(len);
655 buf.copy(v, 0, 5, 5 + len);
656 v = bufferToDebugString(v);
657 consume(5 + len);
658 }
659 }
660 break;
661 case 0x12: // 2-byte string
662 if (buf.length >= 3) {
663 len = buf.readUInt16BE(1);
664 if (buf.length >= 3 + len) {
665 v = new Buffer(len);
666 buf.copy(v, 0, 3, 3 + len);
667 v = bufferToDebugString(v);
668 consume(3 + len);
669 }
670 }
671 break;
672 case 0x13: // 4-byte buffer
673 if (buf.length >= 5) {
674 len = buf.readUInt32BE(1);
675 if (buf.length >= 5 + len) {
676 v = new Buffer(len);
677 buf.copy(v, 0, 5, 5 + len);
678 v = { type: 'buffer', data: v.toString('hex') };
679 consume(5 + len);
680 // Value could be a Node.js buffer directly, but
681 // we prefer all dvalues to be JSON compatible
682 }
683 }
684 break;
685 case 0x14: // 2-byte buffer
686 if (buf.length >= 3) {
687 len = buf.readUInt16BE(1);
688 if (buf.length >= 3 + len) {
689 v = new Buffer(len);
690 buf.copy(v, 0, 3, 3 + len);
691 v = { type: 'buffer', data: v.toString('hex') };
692 consume(3 + len);
693 // Value could be a Node.js buffer directly, but
694 // we prefer all dvalues to be JSON compatible
695 }
696 }
697 break;
698 case 0x15: // unused/none
699 v = { type: 'unused' };
700 consume(1);
701 break;
702 case 0x16: // undefined
703 v = { type: 'undefined' };
704 gotValue = true; // indicate 'v' is actually set
705 consume(1);
706 break;
707 case 0x17: // null
708 v = null;
709 gotValue = true; // indicate 'v' is actually set
710 consume(1);
711 break;
712 case 0x18: // true
713 v = true;
714 consume(1);
715 break;
716 case 0x19: // false
717 v = false;
718 consume(1);
719 break;
720 case 0x1a: // number (IEEE double), big endian
721 if (buf.length >= 9) {
722 v = new Buffer(8);
723 buf.copy(v, 0, 1, 9);
11fdf7f2 724 v = { type: 'number', data: v.toString('hex') };
7c673cae
FG
725
726 if (_this.readableNumberValue) {
11fdf7f2 727 // The value key should not be used programmatically,
7c673cae 728 // it is just there to make the dumps more readable.
11fdf7f2 729 v.value = buf.readDoubleBE(1);
7c673cae
FG
730 }
731 consume(9);
732 }
733 break;
734 case 0x1b: // object
735 if (buf.length >= 3) {
736 len = buf[2];
737 if (buf.length >= 3 + len) {
738 v = new Buffer(len);
739 buf.copy(v, 0, 3, 3 + len);
740 v = { type: 'object', 'class': buf[1], pointer: v.toString('hex') };
741 consume(3 + len);
742 }
743 }
744 break;
745 case 0x1c: // pointer
746 if (buf.length >= 2) {
747 len = buf[1];
748 if (buf.length >= 2 + len) {
749 v = new Buffer(len);
750 buf.copy(v, 0, 2, 2 + len);
751 v = { type: 'pointer', pointer: v.toString('hex') };
752 consume(2 + len);
753 }
754 }
755 break;
756 case 0x1d: // lightfunc
757 if (buf.length >= 4) {
758 len = buf[3];
759 if (buf.length >= 4 + len) {
760 v = new Buffer(len);
761 buf.copy(v, 0, 4, 4 + len);
762 v = { type: 'lightfunc', flags: buf.readUInt16BE(1), pointer: v.toString('hex') };
763 consume(4 + len);
764 }
765 }
766 break;
767 case 0x1e: // heapptr
768 if (buf.length >= 2) {
769 len = buf[1];
770 if (buf.length >= 2 + len) {
771 v = new Buffer(len);
772 buf.copy(v, 0, 2, 2 + len);
773 v = { type: 'heapptr', pointer: v.toString('hex') };
774 consume(2 + len);
775 }
776 }
777 break;
778 default:
779 _this.emit('transport-error', 'Parse error, dropping connection');
780 _this.close();
781 }
782 }
783
784 if (typeof v === 'undefined' && !gotValue) {
785 break;
786 }
787 msg.push(v);
788 _this.dvalues++;
789
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.
793
794 if (v === DVAL_EOM) {
795 _this.messages++;
796
797 if (textDumpFileName || textDumpConsolePrefix) {
798 prettyMsg = prettyDebugMessage(msg);
799 if (textDumpFileName) {
800 fs.appendFileSync(textDumpFileName, (textDumpFilePrefix || '') + prettyMsg + '\n');
801 }
802 if (textDumpConsolePrefix) {
803 console.log(textDumpConsolePrefix + prettyMsg);
804 }
805 }
806
807 _this.emit('debug-message', msg);
808 msg = []; // new object, old may be in circulation for a while
809 }
810 }
811 });
812
813 // Not all streams will emit this.
814 inputStream.on('error', function (err) {
815 _this.emit('transport-error', err);
816 _this.close();
817 });
818
819 // Not all streams will emit this.
820 inputStream.on('close', function () {
821 _this.close();
822 });
823}
824DebugProtocolParser.prototype = Object.create(events.EventEmitter.prototype);
825
826DebugProtocolParser.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'.
830 //
831 // The caller is responsible for closing the underlying stream if that
832 // is necessary.
833
834 if (this.closed) { return; }
835
836 this.closed = true;
837 if (this.statsTimer) {
838 clearInterval(this.statsTimer);
839 this.statsTimer = null;
840 }
841 this.emit('transport-close');
842};
843
844/*
845 * Debugger output formatting
846 */
847
848function formatDebugValue(v) {
849 var buf, dec, len;
850
851 // See doc/debugger.rst for format description.
852
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');
871 len = dec.length;
872 if (len !== 8) {
873 throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
874 }
875 buf = new Buffer(1 + len);
876 buf[0] = 0x1a;
877 dec.copy(buf, 1);
878 return buf;
879 } else if (v.type === 'buffer') {
880 dec = new Buffer(v.data, 'hex');
881 len = dec.length;
882 if (len <= 0xffff) {
883 buf = new Buffer(3 + len);
884 buf[0] = 0x14;
885 buf[1] = (len >> 8) & 0xff;
886 buf[2] = (len >> 0) & 0xff;
887 dec.copy(buf, 3);
888 return buf;
889 } else {
890 buf = new Buffer(5 + len);
891 buf[0] = 0x13;
892 buf[1] = (len >> 24) & 0xff;
893 buf[2] = (len >> 16) & 0xff;
894 buf[3] = (len >> 8) & 0xff;
895 buf[4] = (len >> 0) & 0xff;
896 dec.copy(buf, 5);
897 return buf;
898 }
899 } else if (v.type === 'object') {
900 dec = new Buffer(v.pointer, 'hex');
901 len = dec.length;
902 buf = new Buffer(3 + len);
903 buf[0] = 0x1b;
904 buf[1] = v.class;
905 buf[2] = len;
906 dec.copy(buf, 3);
907 return buf;
908 } else if (v.type === 'pointer') {
909 dec = new Buffer(v.pointer, 'hex');
910 len = dec.length;
911 buf = new Buffer(2 + len);
912 buf[0] = 0x1c;
913 buf[1] = len;
914 dec.copy(buf, 2);
915 return buf;
916 } else if (v.type === 'lightfunc') {
917 dec = new Buffer(v.pointer, 'hex');
918 len = dec.length;
919 buf = new Buffer(4 + len);
920 buf[0] = 0x1d;
921 buf[1] = (v.flags >> 8) & 0xff;
922 buf[2] = v.flags & 0xff;
923 buf[3] = len;
924 dec.copy(buf, 4);
925 return buf;
926 } else if (v.type === 'heapptr') {
927 dec = new Buffer(v.pointer, 'hex');
928 len = dec.length;
929 buf = new Buffer(2 + len);
930 buf[0] = 0x1e;
931 buf[1] = len;
932 dec.copy(buf, 2);
933 return buf;
934 }
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,
952 (v >> 24) & 0xff,
953 (v >> 16) & 0xff,
954 (v >> 8) & 0xff,
955 (v >> 0) & 0xff ]);
956 } else {
957 throw new Error('internal error when encoding integer to dvalue: ' + v);
958 }
959 } else {
960 // Represent non-integers as IEEE double dvalues
961 buf = new Buffer(1 + 8);
962 buf[0] = 0x1a;
963 buf.writeDoubleBE(v, 1);
964 return buf;
965 }
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);
970 }
971 if (v.length <= 0x1f) {
972 buf = new Buffer(1 + v.length);
973 buf[0] = 0x60 + v.length;
974 writeDebugStringToBuffer(v, buf, 1);
975 return buf;
976 } else if (v.length <= 0xffff) {
977 buf = new Buffer(3 + v.length);
978 buf[0] = 0x12;
979 buf[1] = (v.length >> 8) & 0xff;
980 buf[2] = (v.length >> 0) & 0xff;
981 writeDebugStringToBuffer(v, buf, 3);
982 return buf;
983 } else {
984 buf = new Buffer(5 + v.length);
985 buf[0] = 0x11;
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);
991 return buf;
992 }
993 }
994
995 // Shouldn't come here.
996 throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
997}
998
999/*
1000 * Debugger implementation
1001 *
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.
1009 *
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.
1013 */
1014
1015function Debugger() {
1016 events.EventEmitter.call(this);
1017
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
1033 };
1034 this.execStatus = {
1035 attached: false,
1036 state: 'detached',
1037 fileName: '',
1038 funcName: '',
1039 line: 0,
1040 pc: 0
1041 };
1042 this.breakpoints = [];
1043 this.callstack = [];
1044 this.locals = [];
1045 this.messageLines = [];
1046 this.messageScrollBack = 100;
1047}
1048Debugger.prototype = events.EventEmitter.prototype;
1049
1050Debugger.prototype.decodeBytecodeFromBuffer = function (buf, consts, funcs) {
1051 var i, j, n, m, ins, pc;
1052 var res = [];
1053 var op, str, args, comments;
1054
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).
1058
1059 for (i = 0, n = buf.length; i < n; i += 4) {
1060 pc = i / 4;
1061
1062 // shift forces unsigned
1063 if (this.endianness === 'little') {
1064 ins = buf.readInt32LE(i) >>> 0;
1065 } else {
1066 ins = buf.readInt32BE(i) >>> 0;
1067 }
1068
1069 op = dukOpcodes.opcodes[ins & 0x3f];
1070 if (op.extra) {
1071 op = dukOpcodes.extra[(ins >> 6) & 0xff];
1072 }
1073
1074 args = [];
1075 comments = [];
1076 if (op.args) {
1077 for (j = 0, m = op.args.length; j < m; j++) {
11fdf7f2 1078 switch (op.args[j]) {
7c673cae
FG
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
1105 case 'ABC_JUMP': {
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 + ')');
1109 break;
1110 }
1111 default: args.push('?'); break;
1112 }
1113 }
1114 }
1115 if (op.flags) {
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);
1119 }
1120 }
1121 }
1122
1123 if (args.length > 0) {
1124 str = sprintf('%05d %08x %-10s %s', pc, ins, op.name, args.join(', '));
1125 } else {
1126 str = sprintf('%05d %08x %-10s', pc, ins, op.name);
1127 }
1128 if (comments.length > 0) {
1129 str = sprintf('%-44s ; %s', str, comments.join(', '));
1130 }
1131
1132 res.push({
1133 str: str,
1134 ins: ins
1135 });
1136 }
1137
1138 return res;
1139};
1140
1141Debugger.prototype.uiMessage = function (type, val) {
1142 var msg;
1143 if (typeof type === 'object') {
1144 msg = type;
1145 } else if (typeof type === 'string') {
1146 msg = { type: type, message: val };
1147 } else {
1148 throw new TypeError('invalid ui message: ' + type);
1149 }
1150 this.messageLines.push(msg);
1151 while (this.messageLines.length > this.messageScrollBack) {
1152 this.messageLines.shift();
1153 }
1154 this.emit('ui-message-update'); // just trigger a sync, gets rate limited
1155};
1156
1157Debugger.prototype.sendRequest = function (msg) {
1158 var _this = this;
1159 return new Promise(function (resolve, reject) {
1160 var dvals = [];
1161 var dval;
1162 var data;
1163 var i;
1164
1165 if (!_this.attached || !_this.handshook || !_this.reqQueue || !_this.targetStream) {
1166 throw new Error('invalid state for sendRequest');
1167 }
1168
1169 for (i = 0; i < msg.length; i++) {
1170 try {
1171 dval = formatDebugValue(msg[i]);
1172 } catch (e) {
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');
1177 }
1178 dvals.push(dval);
1179 }
1180
1181 data = Buffer.concat(dvals);
1182
1183 _this.targetStream.write(data);
1184 _this.outputPassThroughStream.write(data); // stats and dumping
1185
1186 if (optLogMessages) {
1187 console.log('Request ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
1188 }
1189
1190 if (!_this.reqQueue) {
1191 throw new Error('no reqQueue');
1192 }
1193
1194 _this.reqQueue.push({
1195 reqMsg: msg,
1196 reqCmd: msg[1],
1197 resolveCb: resolve,
1198 rejectCb: reject
1199 });
1200 });
1201};
1202
1203Debugger.prototype.sendBasicInfoRequest = function () {
1204 var _this = this;
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');
1211 return msg;
1212 });
1213};
1214
11fdf7f2 1215Debugger.prototype.sendGetVarRequest = function (varname, level) {
7c673cae 1216 var _this = this;
11fdf7f2 1217 return this.sendRequest([ DVAL_REQ, CMD_GETVAR, varname, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) {
7c673cae
FG
1218 return { found: msg[1] === 1, value: msg[2] };
1219 });
1220};
1221
11fdf7f2 1222Debugger.prototype.sendPutVarRequest = function (varname, varvalue, level) {
7c673cae 1223 var _this = this;
11fdf7f2 1224 return this.sendRequest([ DVAL_REQ, CMD_PUTVAR, varname, varvalue, (typeof level === 'number' ? level : -1), DVAL_EOM ]);
7c673cae
FG
1225};
1226
1227Debugger.prototype.sendInvalidCommandTestRequest = function () {
1228 // Intentional invalid command
1229 var _this = this;
1230 return this.sendRequest([ DVAL_REQ, 0xdeadbeef, DVAL_EOM ]);
11fdf7f2 1231};
7c673cae
FG
1232
1233Debugger.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
1236 var _this = this;
1237 return this.sendRequest([ DVAL_REQ, CMD_TRIGGERSTATUS, DVAL_EOM ]);
11fdf7f2 1238};
7c673cae
FG
1239
1240Debugger.prototype.sendBreakpointListRequest = function () {
1241 var _this = this;
1242 return this.sendRequest([ DVAL_REQ, CMD_LISTBREAK, DVAL_EOM ]).then(function (msg) {
1243 var i, n;
1244 var breakpts = [];
1245
1246 for (i = 1, n = msg.length - 1; i < n; i += 2) {
1247 breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] });
1248 }
1249
1250 _this.breakpoints = breakpts;
1251 _this.emit('breakpoints-update');
1252 return msg;
1253 });
1254};
1255
11fdf7f2 1256Debugger.prototype.sendGetLocalsRequest = function (level) {
7c673cae 1257 var _this = this;
11fdf7f2 1258 return this.sendRequest([ DVAL_REQ, CMD_GETLOCALS, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) {
7c673cae
FG
1259 var i;
1260 var locals = [];
1261
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) });
1265 }
1266
1267 _this.locals = locals;
1268 _this.emit('locals-update');
1269 return msg;
1270 });
1271};
1272
1273Debugger.prototype.sendGetCallStackRequest = function () {
1274 var _this = this;
1275 return this.sendRequest([ DVAL_REQ, CMD_GETCALLSTACK, DVAL_EOM ]).then(function (msg) {
1276 var i;
1277 var stack = [];
1278
1279 for (i = 1; i + 3 <= msg.length - 1; i += 4) {
1280 stack.push({
1281 fileName: msg[i],
1282 funcName: msg[i + 1],
1283 lineNumber: msg[i + 2],
1284 pc: msg[i + 3]
1285 });
1286 }
1287
1288 _this.callstack = stack;
1289 _this.emit('callstack-update');
1290 return msg;
1291 });
1292};
1293
1294Debugger.prototype.sendStepIntoRequest = function () {
1295 var _this = this;
1296 return this.sendRequest([ DVAL_REQ, CMD_STEPINTO, DVAL_EOM ]);
1297};
1298
1299Debugger.prototype.sendStepOverRequest = function () {
1300 var _this = this;
1301 return this.sendRequest([ DVAL_REQ, CMD_STEPOVER, DVAL_EOM ]);
1302};
1303
1304Debugger.prototype.sendStepOutRequest = function () {
1305 var _this = this;
1306 return this.sendRequest([ DVAL_REQ, CMD_STEPOUT, DVAL_EOM ]);
1307};
1308
1309Debugger.prototype.sendPauseRequest = function () {
1310 var _this = this;
1311 return this.sendRequest([ DVAL_REQ, CMD_PAUSE, DVAL_EOM ]);
1312};
1313
1314Debugger.prototype.sendResumeRequest = function () {
1315 var _this = this;
1316 return this.sendRequest([ DVAL_REQ, CMD_RESUME, DVAL_EOM ]);
1317};
1318
11fdf7f2 1319Debugger.prototype.sendEvalRequest = function (evalInput, level) {
7c673cae 1320 var _this = this;
11fdf7f2 1321 return this.sendRequest([ DVAL_REQ, CMD_EVAL, evalInput, (typeof level === 'number' ? level : -1), DVAL_EOM ]).then(function (msg) {
7c673cae
FG
1322 return { error: msg[1] === 1 /*error*/, value: msg[2] };
1323 });
1324};
1325
1326Debugger.prototype.sendDetachRequest = function () {
1327 var _this = this;
1328 return this.sendRequest([ DVAL_REQ, CMD_DETACH, DVAL_EOM ]);
1329};
1330
1331Debugger.prototype.sendDumpHeapRequest = function () {
1332 var _this = this;
1333
1334 return this.sendRequest([ DVAL_REQ, CMD_DUMPHEAP, DVAL_EOM ]).then(function (msg) {
1335 var res = {};
1336 var objs = [];
1337 var i, j, n, m, o, prop;
1338
1339 res.type = 'heapDump';
1340 res.heapObjects = objs;
1341
1342 for (i = 1, n = msg.length - 1; i < n; /*nop*/) {
1343 o = {};
1344 o.ptr = msg[i++];
1345 o.type = msg[i++];
1346 o.flags = msg[i++] >>> 0; /* unsigned */
1347 o.refc = msg[i++];
1348
1349 if (o.type === DUK_HTYPE_STRING) {
1350 o.blen = msg[i++];
1351 o.clen = msg[i++];
1352 o.hash = msg[i++] >>> 0; /* unsigned */
1353 o.data = msg[i++];
1354 } else if (o.type === DUK_HTYPE_BUFFER) {
1355 o.len = msg[i++];
1356 o.data = msg[i++];
1357 } else if (o.type === DUK_HTYPE_OBJECT) {
1358 o['class'] = msg[i++];
1359 o.proto = msg[i++];
1360 o.esize = msg[i++];
1361 o.enext = msg[i++];
1362 o.asize = msg[i++];
1363 o.hsize = msg[i++];
1364 o.props = [];
1365 for (j = 0, m = o.enext; j < m; j++) {
1366 prop = {};
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++];
1373 } else {
1374 prop.value = msg[i++];
1375 }
1376 o.props.push(prop);
1377 }
1378 o.array = [];
1379 for (j = 0, m = o.asize; j < m; j++) {
1380 prop = {};
1381 prop.value = msg[i++];
1382 o.array.push(prop);
1383 }
1384 } else {
1385 console.log('invalid htype: ' + o.type + ', disconnect');
1386 _this.disconnectDebugger();
1387 throw new Error('invalid htype');
1388 return;
1389 }
1390
1391 objs.push(o);
1392 }
1393
1394 return res;
1395 });
1396};
1397
1398Debugger.prototype.sendGetBytecodeRequest = function () {
1399 var _this = this;
1400
1401 return this.sendRequest([ DVAL_REQ, CMD_GETBYTECODE, DVAL_EOM ]).then(function (msg) {
1402 var idx = 1;
1403 var nconst;
1404 var nfunc;
1405 var val;
1406 var buf;
1407 var i, n;
1408 var consts = [];
1409 var funcs = [];
1410 var bcode;
1411 var preformatted;
1412 var ret;
11fdf7f2 1413 var idxPreformattedInstructions;
7c673cae
FG
1414
1415 //console.log(JSON.stringify(msg));
1416
1417 nconst = msg[idx++];
1418 for (i = 0; i < nconst; i++) {
1419 val = msg[idx++];
1420 consts.push(val);
1421 }
1422
1423 nfunc = msg[idx++];
1424 for (i = 0; i < nfunc; i++) {
1425 val = msg[idx++];
1426 funcs.push(val);
1427 }
1428 val = msg[idx++];
1429
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
1432 // doesn't need to.
1433
1434 buf = new Buffer(val.length);
1435 writeDebugStringToBuffer(val, buf, 0);
1436 bcode = _this.decodeBytecodeFromBuffer(buf, consts, funcs);
1437
1438 preformatted = [];
1439 consts.forEach(function (v, i) {
1440 preformatted.push('; c' + i + ' ' + JSON.stringify(v));
1441 });
1442 preformatted.push('');
11fdf7f2 1443 idxPreformattedInstructions = preformatted.length;
7c673cae
FG
1444 bcode.forEach(function (v) {
1445 preformatted.push(v.str);
1446 });
1447 preformatted = preformatted.join('\n') + '\n';
1448
1449 ret = {
1450 constants: consts,
1451 functions: funcs,
1452 bytecode: bcode,
11fdf7f2
TL
1453 preformatted: preformatted,
1454 idxPreformattedInstructions: idxPreformattedInstructions
7c673cae
FG
1455 };
1456
1457 return ret;
1458 });
1459};
1460
1461Debugger.prototype.changeBreakpoint = function (fileName, lineNumber, mode) {
1462 var _this = this;
1463
1464 return this.sendRequest([ DVAL_REQ, CMD_LISTBREAK, DVAL_EOM ]).then(function (msg) {
1465 var i, n;
1466 var breakpts = [];
1467 var deleted = false;
1468
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] });
1472 }
1473
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)) {
1480 deleted = true;
1481 _this.sendRequest([ DVAL_REQ, CMD_DELBREAK, i, DVAL_EOM ], function (msg) {
1482 // nop
1483 }, function (err) {
1484 // nop
1485 });
1486 }
1487 }
1488
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) {
1493 // nop
1494 }, function (err) {
1495 _this.uiMessage('debugger-info', 'Failed to add breakpoint: ' + err);
1496 });
1497 }
1498
1499 // Read final, effective breakpoints from the target
1500 _this.sendBreakpointListRequest();
1501 });
1502};
1503
1504Debugger.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;
1509 }
1510 if (this.inputParser) {
1511 this.inputParser.close();
1512 this.inputParser = null;
1513 }
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.
1517 }
1518 if (this.outputParser) {
1519 this.outputParser.close();
1520 this.outputParser = null;
1521 }
1522
1523 this.attached = false;
1524 this.handshook = false;
1525 this.reqQueue = null;
1526 this.execStatus = {
1527 attached: false,
1528 state: 'detached',
1529 fileName: '',
1530 funcName: '',
1531 line: 0,
1532 pc: 0
1533 };
1534};
1535
1536Debugger.prototype.connectDebugger = function () {
1537 var _this = this;
1538
1539 this.disconnectDebugger(); // close previous target connection
1540
1541 // CUSTOMTRANSPORT: to use a custom transport, change this.targetStream to
1542 // use your custom transport.
1543
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');
1551 });
1552
1553 this.inputParser = new DebugProtocolParser(
1554 this.targetStream,
1555 null,
1556 optDumpDebugRead,
1557 optDumpDebugPretty,
1558 optDumpDebugPretty ? 'Recv: ' : null,
1559 null,
1560 null // console logging is done at a higher level to match request/response
1561 );
1562
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
1565 // separately.
1566 this.outputPassThroughStream = stream.PassThrough();
1567 this.outputParser = new DebugProtocolParser(
1568 this.outputPassThroughStream,
1569 1,
1570 optDumpDebugWrite,
1571 optDumpDebugPretty,
1572 optDumpDebugPretty ? 'Send: ' : null,
1573 null,
1574 null // console logging is done at a higher level to match request/response
1575 );
1576
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');
1582 });
1583
1584 this.inputParser.on('transport-error', function (err) {
1585 _this.uiMessage('debugger-info', 'Debug transport error: ' + err);
1586 _this.disconnectDebugger();
1587 });
1588
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);
1594 if (ver !== 1) {
1595 _this.uiMessage('debugger-info', 'Protocol version ' + ver + ' unsupported, dropping connection');
1596 _this.targetStream.destroy();
1597 } else {
1598 _this.uiMessage('debugger-info', 'Debug protocol version: ' + ver);
1599 _this.handshook = true;
1600 _this.execStatus = {
1601 attached: true,
1602 state: 'attached',
1603 fileName: '',
1604 funcName: '',
1605 line: 0,
1606 pc: 0
1607 };
1608 _this.emit('exec-status-update');
1609 _this.emit('attached'); // inform web UI
1610
1611 // Fetch basic info right away
1612 _this.sendBasicInfoRequest();
1613 }
1614 });
1615
1616 this.inputParser.on('debug-message', function (msg) {
1617 _this.processDebugMessage(msg);
1618 });
1619
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');
1626 });
1627
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');
1634 });
1635};
1636
1637Debugger.prototype.processDebugMessage = function (msg) {
1638 var req;
1639 var prevState, newState;
1640 var err;
1641
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();
1649 }
1650 req = this.reqQueue.shift();
1651
1652 if (optLogMessages) {
1653 console.log('Reply for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
1654 }
1655
1656 if (req.resolveCb) {
1657 req.resolveCb(msg);
1658 } else {
1659 // nop: no callback
1660 }
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();
1665 }
1666 err = new Error(String(msg[2]) + ' (code ' + String(msg[1]) + ')');
1667 err.errorCode = msg[1] || 0;
1668 req = this.reqQueue.shift();
1669
1670 if (optLogMessages) {
1671 console.log('Error for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
1672 }
1673
1674 if (req.rejectCb) {
1675 req.rejectCb(err);
1676 } else {
1677 // nop: no callback
1678 }
1679 } else if (msg[0] === DVAL_NFY) {
1680 if (optLogMessages) {
1681 console.log('Notify ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
1682 }
1683
1684 if (msg[1] === CMD_STATUS) {
1685 prevState = this.execStatus.state;
1686 newState = msg[2] === 0 ? 'running' : 'paused';
1687 this.execStatus = {
1688 attached: true,
1689 state: newState,
1690 fileName: msg[3],
1691 funcName: msg[4],
1692 line: msg[5],
1693 pc: msg[6]
1694 };
1695
1696 if (prevState !== newState && newState === 'paused') {
1697 // update run state now that we're paused
1698 this.sendBreakpointListRequest();
1699 this.sendGetLocalsRequest();
1700 this.sendGetCallStackRequest();
1701 }
1702
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) });
11fdf7f2
TL
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') });
7c673cae 1714 } else {
11fdf7f2
TL
1715 // Ignore unknown notify messages
1716 console.log('Unknown notify, ignoring: ' + prettyDebugMessage(msg));
1717
1718 //this.targetStream.destroy();
7c673cae
FG
1719 }
1720 } else {
1721 console.log('Invalid initial dvalue, dropping connection: ' + prettyDebugMessage(msg));
1722 this.targetStream.destroy();
1723 }
1724};
1725
1726Debugger.prototype.run = function () {
1727 var _this = this;
1728
1729 // Initial debugger connection
1730
1731 this.connectDebugger();
1732
1733 // Poll various state items when running
1734
1735 var sendRound = 0;
1736 var statusPending = false;
1737 var bplistPending = false;
1738 var localsPending = false;
1739 var callStackPending = false;
1740
1741 setInterval(function () {
1742 if (_this.execStatus.state !== 'running') {
1743 return;
1744 }
1745
1746 // Could also check for an empty request queue, but that's probably
1747 // too strict?
1748
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.
1752
11fdf7f2 1753 switch (sendRound) {
7c673cae
FG
1754 case 0:
1755 if (!statusPending) {
1756 statusPending = true;
1757 _this.sendStatusRequest().finally(function () { statusPending = false; });
1758 }
1759 break;
1760 case 1:
1761 if (!bplistPending) {
1762 bplistPending = true;
1763 _this.sendBreakpointListRequest().finally(function () { bplistPending = false; });
1764 }
1765 break;
1766 case 2:
1767 if (!localsPending) {
1768 localsPending = true;
1769 _this.sendGetLocalsRequest().finally(function () { localsPending = false; });
1770 }
1771 break;
1772 case 3:
1773 if (!callStackPending) {
1774 callStackPending = true;
1775 _this.sendGetCallStackRequest().finally(function () { callStackPending = false; });
1776 }
1777 break;
1778 }
1779 sendRound = (sendRound + 1) % 4;
1780 }, 500);
1781};
1782
1783/*
1784 * Express setup and socket.io
1785 */
1786
1787function 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();
1795}
1796
1797DebugWebServer.prototype.handleSourcePost = function (req, res) {
1798 var fileName = req.body && req.body.fileName;
1799 var fileData;
1800
1801 console.log('Source request: ' + fileName);
1802
1803 if (typeof fileName !== 'string') {
1804 res.status(500).send('invalid request');
1805 return;
1806 }
1807 fileData = this.sourceFileManager.search(fileName, optSourceSearchDirs);
1808 if (typeof fileData !== 'string') {
1809 res.status(404).send('not found');
1810 return;
1811 }
1812 res.status(200).send(fileData); // UTF-8
1813};
1814
1815DebugWebServer.prototype.handleSourceListPost = function (req, res) {
1816 console.log('Source list request');
1817
1818 var files = this.sourceFileManager.getFiles();
1819 res.header('Content-Type', 'application/json');
1820 res.status(200).json(files);
1821};
1822
1823DebugWebServer.prototype.handleHeapDumpGet = function (req, res) {
1824 console.log('Heap dump get');
1825
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));
1832 });
1833};
1834
1835DebugWebServer.prototype.run = function () {
1836 var _this = this;
1837
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);
1843
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'));
1849
1850 http.listen(optHttpPort, function () {
1851 console.log('Listening on *:' + optHttpPort);
1852 });
1853
1854 io.on('connection', this.handleNewSocketIoConnection.bind(this));
1855
1856 this.dbg.on('attached', function () {
1857 console.log('Debugger attached');
1858 });
1859
1860 this.dbg.on('detached', function () {
1861 console.log('Debugger detached');
1862 });
1863
1864 this.dbg.on('debug-stats-update', function () {
1865 _this.debugStatsLimiter.trigger();
1866 });
1867
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();
1871 });
1872
1873 this.dbg.on('basic-info-update', function () {
1874 _this.emitBasicInfo();
1875 });
1876
1877 this.dbg.on('breakpoints-update', function () {
1878 _this.emitBreakpoints();
1879 });
1880
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();
1884 });
1885
1886 this.dbg.on('locals-update', function () {
1887 _this.emitLocals();
1888 });
1889
1890 this.dbg.on('callstack-update', function () {
1891 _this.emitCallStack();
1892 });
1893
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));
1897
1898 this.keepaliveTimer = setInterval(this.emitKeepalive.bind(this), 30000);
1899};
1900
1901DebugWebServer.prototype.handleNewSocketIoConnection = function (socket) {
1902 var _this = this;
1903
1904 console.log('Socket.io connected');
1905 if (this.socket) {
1906 console.log('Closing previous socket.io socket');
1907 this.socket.emit('replaced');
1908 }
1909 this.socket = socket;
1910
1911 this.emitKeepalive();
1912
1913 socket.on('disconnect', function () {
1914 console.log('Socket.io disconnected');
1915 if (_this.socket === socket) {
1916 _this.socket = null;
1917 }
1918 });
1919
1920 socket.on('keepalive', function (msg) {
1921 // nop
1922 });
1923
1924 socket.on('attach', function (msg) {
1925 if (_this.dbg.targetStream) {
1926 console.log('Attach request when debugger already has a connection, ignoring');
1927 } else {
1928 _this.dbg.connectDebugger();
1929 }
1930 });
1931
1932 socket.on('detach', function (msg) {
1933 // Try to detach cleanly, timeout if no response
1934 Promise.any([
1935 _this.dbg.sendDetachRequest(),
1936 Promise.delay(3000)
1937 ]).finally(function () {
1938 _this.dbg.disconnectDebugger();
1939 });
1940 });
1941
1942 socket.on('stepinto', function (msg) {
1943 _this.dbg.sendStepIntoRequest();
1944 });
1945
1946 socket.on('stepover', function (msg) {
1947 _this.dbg.sendStepOverRequest();
1948 });
1949
1950 socket.on('stepout', function (msg) {
1951 _this.dbg.sendStepOutRequest();
1952 });
1953
1954 socket.on('pause', function (msg) {
1955 _this.dbg.sendPauseRequest();
1956 });
1957
1958 socket.on('resume', function (msg) {
1959 _this.dbg.sendResumeRequest();
1960 });
1961
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);
11fdf7f2 1966 _this.dbg.sendEvalRequest(input, msg.level).then(function (v) {
7c673cae
FG
1967 socket.emit('eval-result', { error: v.error, result: prettyUiDebugValue(v.value, EVAL_CLIPLEN) });
1968 });
1969
1970 // An eval call quite possibly changes the local variables so always
11fdf7f2 1971 // re-read locals afterwards. We don't need to wait for Eval to
7c673cae
FG
1972 // complete here; the requests will pipeline automatically and be
1973 // executed in order.
11fdf7f2
TL
1974
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).
7c673cae
FG
1978 _this.dbg.sendGetLocalsRequest();
1979 });
1980
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);
11fdf7f2 1985 _this.dbg.sendGetVarRequest(varname, msg.level)
7c673cae
FG
1986 .then(function (v) {
1987 socket.emit('getvar-result', { found: v.found, result: prettyUiDebugValue(v.value, GETVAR_CLIPLEN) });
1988 });
1989 });
1990
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;
1996
1997 // varvalue is JSON parsed by the web UI for now, need special string
1998 // encoding here.
1999 if (typeof varvalue === 'string') {
2000 varvalue = stringToDebugString(msg.varvalue);
2001 }
2002
11fdf7f2 2003 _this.dbg.sendPutVarRequest(varname, varvalue, msg.level)
7c673cae
FG
2004 .then(function (v) {
2005 console.log('putvar done'); // XXX: signal success to UI?
2006 });
2007
2008 // A PutVar call quite possibly changes the local variables so always
11fdf7f2 2009 // re-read locals afterwards. We don't need to wait for PutVar to
7c673cae
FG
2010 // complete here; the requests will pipeline automatically and be
2011 // executed in order.
11fdf7f2
TL
2012
2013 // XXX: make the client do this?
7c673cae
FG
2014 _this.dbg.sendGetLocalsRequest();
2015 });
2016
2017 socket.on('add-breakpoint', function (msg) {
2018 _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'add');
2019 });
2020
2021 socket.on('delete-breakpoint', function (msg) {
2022 _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'delete');
2023 });
2024
2025 socket.on('toggle-breakpoint', function (msg) {
2026 _this.dbg.changeBreakpoint(msg.fileName, msg.lineNumber, 'toggle');
2027 });
2028
2029 socket.on('delete-all-breakpoints', function (msg) {
2030 _this.dbg.changeBreakpoint(null, null, 'deleteall');
2031 });
2032
2033 socket.on('get-bytecode', function (msg) {
2034 _this.dbg.sendGetBytecodeRequest().then(function (res) {
2035 socket.emit('bytecode', res);
2036 });
2037 });
2038
2039 // Resend all debugger state for new client
2040 this.cachedJson = {}; // clear client state cache
2041 this.emitBasicInfo();
2042 this.emitStats();
2043 this.emitExecStatus();
2044 this.emitUiMessages();
2045 this.emitBreakpoints();
2046 this.emitCallStack();
2047 this.emitLocals();
2048};
2049
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.
2052DebugWebServer.prototype.cachedJsonCheck = function (cacheKey, msg) {
2053 var newJson = JSON.stringify(msg);
2054 if (this.cachedJson[cacheKey] === newJson) {
2055 return true; // cached
2056 }
2057 this.cachedJson[cacheKey] = newJson;
2058 return false; // not cached, send (cache already updated)
2059};
2060
2061DebugWebServer.prototype.uiMessageLimiterCallback = function () {
2062 this.emitUiMessages();
2063};
2064
2065DebugWebServer.prototype.execStatusLimiterCallback = function () {
2066 this.emitExecStatus();
2067};
2068
2069DebugWebServer.prototype.debugStatsLimiterCallback = function () {
2070 this.emitStats();
2071};
2072
2073DebugWebServer.prototype.emitKeepalive = function () {
2074 if (!this.socket) { return; }
2075
2076 this.socket.emit('keepalive', { nodeVersion: process.version });
2077};
2078
2079DebugWebServer.prototype.emitBasicInfo = function () {
2080 if (!this.socket) { return; }
2081
2082 var newMsg = {
2083 duk_version: this.dbg.dukVersion,
2084 duk_git_describe: this.dbg.dukGitDescribe,
2085 target_info: this.dbg.targetInfo,
2086 endianness: this.dbg.endianness
2087 };
2088 if (this.cachedJsonCheck('basic-info', newMsg)) {
2089 return;
2090 }
2091 this.socket.emit('basic-info', newMsg);
2092};
2093
2094DebugWebServer.prototype.emitStats = function () {
2095 if (!this.socket) { return; }
2096
2097 this.socket.emit('debug-stats', this.dbg.stats);
2098};
2099
2100DebugWebServer.prototype.emitExecStatus = function () {
2101 if (!this.socket) { return; }
2102
2103 var newMsg = this.dbg.execStatus;
2104 if (this.cachedJsonCheck('exec-status', newMsg)) {
2105 return;
2106 }
2107 this.socket.emit('exec-status', newMsg);
2108};
2109
2110DebugWebServer.prototype.emitUiMessages = function () {
2111 if (!this.socket) { return; }
2112
2113 var newMsg = this.dbg.messageLines;
2114 if (this.cachedJsonCheck('output-lines', newMsg)) {
2115 return;
2116 }
2117 this.socket.emit('output-lines', newMsg);
2118};
2119
2120DebugWebServer.prototype.emitBreakpoints = function () {
2121 if (!this.socket) { return; }
2122
2123 var newMsg = { breakpoints: this.dbg.breakpoints };
2124 if (this.cachedJsonCheck('breakpoints', newMsg)) {
2125 return;
2126 }
2127 this.socket.emit('breakpoints', newMsg);
2128};
2129
2130DebugWebServer.prototype.emitCallStack = function () {
2131 if (!this.socket) { return; }
2132
2133 var newMsg = { callstack: this.dbg.callstack };
2134 if (this.cachedJsonCheck('callstack', newMsg)) {
2135 return;
2136 }
2137 this.socket.emit('callstack', newMsg);
2138};
2139
2140DebugWebServer.prototype.emitLocals = function () {
2141 if (!this.socket) { return; }
2142
2143 var newMsg = { locals: this.dbg.locals };
2144 if (this.cachedJsonCheck('locals', newMsg)) {
2145 return;
2146 }
2147 this.socket.emit('locals', newMsg);
2148};
2149
2150/*
2151 * JSON debug proxy
2152 */
2153
2154function DebugProxy(serverPort) {
2155 this.serverPort = serverPort;
2156 this.server = null;
2157 this.socket = null;
2158 this.targetStream = null;
2159 this.inputParser = null;
2160
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);
2167}
2168
11fdf7f2 2169DebugProxy.prototype.determineCommandNumber = function (cmdName, cmdNumber) {
7c673cae 2170 var ret;
11fdf7f2
TL
2171 if (typeof cmdName === 'string') {
2172 ret = debugCommandNumbers[cmdName];
2173 } else if (typeof cmdName === 'number') {
2174 ret = cmdName;
7c673cae
FG
2175 }
2176 ret = ret || cmdNumber;
2177 if (typeof ret !== 'number') {
11fdf7f2 2178 throw Error('cannot figure out command number for "' + cmdName + '" (' + cmdNumber + ')');
7c673cae
FG
2179 }
2180 return ret;
2181};
2182
2183DebugProxy.prototype.commandNumberToString = function (id) {
2184 return debugCommandNames[id] || String(id);
2185};
2186
2187DebugProxy.prototype.formatDvalues = function (args) {
2188 if (!args) {
2189 return [];
2190 }
2191 return args.map(function (v) {
2192 return formatDebugValue(v);
2193 });
2194};
2195
2196DebugProxy.prototype.writeJson = function (val) {
2197 this.socket.write(JSON.stringify(val) + '\n');
2198};
2199
2200DebugProxy.prototype.writeJsonSafe = function (val) {
2201 try {
2202 this.writeJson(val);
2203 } catch (e) {
2204 console.log('Failed to write JSON in writeJsonSafe, ignoring: ' + e);
2205 }
2206};
2207
2208DebugProxy.prototype.disconnectJsonClient = function () {
2209 if (this.socket) {
2210 this.socket.destroy();
2211 this.socket = null;
2212 }
2213};
2214
2215DebugProxy.prototype.disconnectTarget = function () {
2216 if (this.inputParser) {
2217 this.inputParser.close();
2218 this.inputParser = null;
2219 }
2220 if (this.targetStream) {
2221 this.targetStream.destroy();
2222 this.targetStream = null;
2223 }
2224};
2225
2226DebugProxy.prototype.run = function () {
2227 var _this = this;
2228
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');
2232
2233 _this.disconnectJsonClient();
2234 _this.disconnectTarget();
2235
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;
2240
2241 socketByline.on('data', function (line) {
2242 try {
2243 // console.log('Received json proxy input line: ' + line.toString('utf8'));
2244 var msg = JSON.parse(line.toString('utf8'));
2245 var first_dval;
2246 var args_dvalues = _this.formatDvalues(msg.args);
2247 var last_dval = _this.dval_eom;
2248 var cmd;
2249
2250 if (msg.request) {
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;
2262 } else {
2263 throw new Error('Invalid input JSON message: ' + JSON.stringify(msg));
2264 }
2265
2266 _this.targetStream.write(first_dval);
2267 if (cmd) {
2268 _this.targetStream.write(formatDebugValue(cmd));
2269 }
2270 args_dvalues.forEach(function (v) {
2271 _this.targetStream.write(v);
2272 });
2273 _this.targetStream.write(last_dval);
2274 } catch (e) {
2275 console.log(e);
2276
2277 _this.writeJsonSafe({
2278 notify: '_Error',
2279 args: [ 'Failed to handle input json message: ' + e ]
2280 });
2281
2282 _this.disconnectJsonClient();
2283 _this.disconnectTarget();
2284 }
2285 });
2286
2287 _this.connectToTarget();
2288 }).listen(this.serverPort);
2289};
2290
2291DebugProxy.prototype.connectToTarget = function () {
2292 var _this = this;
2293
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');
2298 });
2299
2300 this.inputParser = new DebugProtocolParser(
2301 this.targetStream,
2302 null,
2303 optDumpDebugRead,
2304 optDumpDebugPretty,
2305 optDumpDebugPretty ? 'Recv: ' : null,
2306 null,
2307 null // console logging is done at a higher level to match request/response
2308 );
2309
11fdf7f2 2310 // Don't add a 'value' key to numbers.
7c673cae
FG
2311 this.inputParser.readableNumberValue = false;
2312
2313 this.inputParser.on('transport-close', function () {
2314 console.log('Debug transport closed');
2315
2316 _this.writeJsonSafe({
2317 notify: '_Disconnecting'
2318 });
2319
2320 _this.disconnectJsonClient();
2321 _this.disconnectTarget();
2322 });
2323
2324 this.inputParser.on('transport-error', function (err) {
2325 console.log('Debug transport error', err);
2326
2327 _this.writeJsonSafe({
2328 notify: '_Error',
2329 args: [ String(err) ]
2330 });
2331 });
2332
2333 this.inputParser.on('protocol-version', function (msg) {
2334 var ver = msg.protocolVersion;
2335 console.log('Debug version identification:', msg.versionIdentification);
2336
2337 _this.writeJson({
11fdf7f2 2338 notify: '_TargetConnected',
7c673cae
FG
2339 args: [ msg.versionIdentification ] // raw identification string
2340 });
2341
2342 if (ver !== 1) {
2343 console.log('Protocol version ' + ver + ' unsupported, dropping connection');
2344 }
2345 });
2346
2347 this.inputParser.on('debug-message', function (msg) {
2348 var t;
2349
2350 //console.log(msg);
2351
2352 if (typeof msg[0] !== 'object' || msg[0] === null) {
2353 throw new Error('unexpected initial dvalue: ' + msg[0]);
11fdf7f2 2354 } else if (msg[0].type === 'eom') {
7c673cae 2355 throw new Error('unexpected initial dvalue: ' + msg[0]);
11fdf7f2 2356 } else if (msg[0].type === 'req') {
7c673cae
FG
2357 if (typeof msg[1] !== 'number') {
2358 throw new Error('unexpected request command number: ' + msg[1]);
2359 }
2360 t = {
2361 request: _this.commandNumberToString(msg[1]),
2362 command: msg[1],
2363 args: msg.slice(2, msg.length - 1)
11fdf7f2 2364 };
7c673cae
FG
2365 _this.writeJson(t);
2366 } else if (msg[0].type === 'rep') {
2367 t = {
2368 reply: true,
2369 args: msg.slice(1, msg.length - 1)
11fdf7f2 2370 };
7c673cae
FG
2371 _this.writeJson(t);
2372 } else if (msg[0].type === 'err') {
2373 t = {
2374 error: true,
2375 args: msg.slice(1, msg.length - 1)
11fdf7f2 2376 };
7c673cae
FG
2377 _this.writeJson(t);
2378 } else if (msg[0].type === 'nfy') {
2379 if (typeof msg[1] !== 'number') {
2380 throw new Error('unexpected notify command number: ' + msg[1]);
2381 }
2382 t = {
2383 notify: _this.commandNumberToString(msg[1]),
2384 command: msg[1],
2385 args: msg.slice(2, msg.length - 1)
11fdf7f2 2386 };
7c673cae
FG
2387 _this.writeJson(t);
2388 } else {
2389 throw new Error('unexpected initial dvalue: ' + msg[0]);
2390 }
2391 });
2392
2393 this.inputParser.on('stats-update', function () {
2394 });
2395};
2396
2397/*
2398 * Command line parsing and initialization
2399 */
2400
2401function main() {
2402 console.log('((o) Duktape debugger');
2403
2404 // Parse arguments.
2405
2406 var argv = require('minimist')(process.argv.slice(2));
2407 //console.dir(argv);
2408 if (argv['target-host']) {
2409 optTargetHost = argv['target-host'];
2410 }
2411 if (argv['target-port']) {
2412 optTargetPort = argv['target-port'];
2413 }
2414 if (argv['http-port']) {
2415 optHttpPort = argv['http-port'];
2416 }
2417 if (argv['json-proxy-port']) {
2418 optJsonProxyPort = argv['json-proxy-port'];
2419 }
2420 if (argv['json-proxy']) {
2421 optJsonProxy = argv['json-proxy'];
2422 }
2423 if (argv['source-dirs']) {
2424 optSourceSearchDirs = argv['source-dirs'].split(path.delimiter);
2425 }
2426 if (argv['dump-debug-read']) {
2427 optDumpDebugRead = argv['dump-debug-read'];
2428 }
2429 if (argv['dump-debug-write']) {
2430 optDumpDebugWrite = argv['dump-debug-write'];
2431 }
2432 if (argv['dump-debug-pretty']) {
2433 optDumpDebugPretty = argv['dump-debug-pretty'];
2434 }
2435 if (argv['log-messages']) {
2436 optLogMessages = true;
2437 }
2438
2439 // Dump effective options. Also provides a list of option names.
2440
2441 console.log('');
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);
2453 console.log('');
2454
2455 // Create debugger and web UI singletons, tie them together and
2456 // start them.
2457
2458 if (optJsonProxy) {
2459 console.log('Starting in JSON proxy mode, JSON port: ' + optJsonProxyPort);
2460
2461 var prx = new DebugProxy(optJsonProxyPort);
2462 prx.run();
2463 } else {
2464 var dbg = new Debugger();
2465 var web = new DebugWebServer();
2466 dbg.web = web;
2467 web.dbg = dbg;
2468 dbg.run();
2469 web.run();
2470 }
2471}
2472
2473main();