2163 lines
115 KiB
React
2163 lines
115 KiB
React
|
/*
|
||
|
*
|
||
|
* ===============
|
||
|
* Duktape license
|
||
|
* ===============
|
||
|
*
|
||
|
* (http://opensource.org/licenses/MIT)
|
||
|
*
|
||
|
* Copyright (c) 2013-2017 by Duktape authors (see AUTHORS.rst)
|
||
|
*
|
||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
* of this software and associated documentation files (the "Software"), to deal
|
||
|
* in the Software without restriction, including without limitation the rights
|
||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
* copies of the Software, and to permit persons to whom the Software is
|
||
|
* furnished to do so, subject to the following conditions:
|
||
|
*
|
||
|
* The above copyright notice and this permission notice shall be included in
|
||
|
* all copies or substantial portions of the Software.
|
||
|
*
|
||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
* THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
/* AUTHORS.rst */
|
||
|
/*
|
||
|
* ===============
|
||
|
* Duktape authors
|
||
|
* ===============
|
||
|
*
|
||
|
* Copyright
|
||
|
* =========
|
||
|
*
|
||
|
* Duktape copyrights are held by its authors. Each author has a copyright
|
||
|
* to their contribution, and agrees to irrevocably license the contribution
|
||
|
* under the Duktape ``LICENSE.txt``.
|
||
|
*
|
||
|
* Authors
|
||
|
* =======
|
||
|
*
|
||
|
* Please include an e-mail address, a link to your GitHub profile, or something
|
||
|
* similar to allow your contribution to be identified accurately.
|
||
|
*
|
||
|
* The following people have contributed code, website contents, or Wiki contents,
|
||
|
* and agreed to irrevocably license their contributions under the Duktape
|
||
|
* ``LICENSE.txt`` (in order of appearance):
|
||
|
*
|
||
|
* * Sami Vaarala <sami.vaarala@iki.fi>
|
||
|
* * Niki Dobrev
|
||
|
* * Andreas \u00d6man <andreas@lonelycoder.com>
|
||
|
* * L\u00e1szl\u00f3 Lang\u00f3 <llango.u-szeged@partner.samsung.com>
|
||
|
* * Legimet <legimet.calc@gmail.com>
|
||
|
* * Karl Skomski <karl@skomski.com>
|
||
|
* * Bruce Pascoe <fatcerberus1@gmail.com>
|
||
|
* * Ren\u00e9 Hollander <rene@rene8888.at>
|
||
|
* * Julien Hamaide (https://github.com/crazyjul)
|
||
|
* * Sebastian G\u00f6tte (https://github.com/jaseg)
|
||
|
* * Tomasz Magulski (https://github.com/magul)
|
||
|
* * \D. Bohdan (https://github.com/dbohdan)
|
||
|
* * Ond\u0159ej Jirman (https://github.com/megous)
|
||
|
* * Sa\u00fal Ibarra Corretg\u00e9 <saghul@gmail.com>
|
||
|
* * Jeremy HU <huxingyi@msn.com>
|
||
|
* * Ole Andr\u00e9 Vadla Ravn\u00e5s (https://github.com/oleavr)
|
||
|
* * Harold Brenes (https://github.com/harold-b)
|
||
|
* * Oliver Crow (https://github.com/ocrow)
|
||
|
* * Jakub Ch\u0142api\u0144ski (https://github.com/jchlapinski)
|
||
|
* * Brett Vickers (https://github.com/beevik)
|
||
|
* * Dominik Okwieka (https://github.com/okitec)
|
||
|
* * Remko Tron\u00e7on (https://el-tramo.be)
|
||
|
* * Romero Malaquias (rbsm@ic.ufal.br)
|
||
|
* * Michael Drake <michael.drake@codethink.co.uk>
|
||
|
* * Steven Don (https://github.com/shdon)
|
||
|
* * Simon Stone (https://github.com/sstone1)
|
||
|
* * \J. McC. (https://github.com/jmhmccr)
|
||
|
*
|
||
|
* Other contributions
|
||
|
* ===================
|
||
|
*
|
||
|
* The following people have contributed something other than code (e.g. reported
|
||
|
* bugs, provided ideas, etc; roughly in order of appearance):
|
||
|
*
|
||
|
* * Greg Burns
|
||
|
* * Anthony Rabine
|
||
|
* * Carlos Costa
|
||
|
* * Aur\u00e9lien Bouilland
|
||
|
* * Preet Desai (Pris Matic)
|
||
|
* * judofyr (http://www.reddit.com/user/judofyr)
|
||
|
* * Jason Woofenden
|
||
|
* * Micha\u0142 Przyby\u015b
|
||
|
* * Anthony Howe
|
||
|
* * Conrad Pankoff
|
||
|
* * Jim Schimpf
|
||
|
* * Rajaran Gaunker (https://github.com/zimbabao)
|
||
|
* * Andreas \u00d6man
|
||
|
* * Doug Sanden
|
||
|
* * Josh Engebretson (https://github.com/JoshEngebretson)
|
||
|
* * Remo Eichenberger (https://github.com/remoe)
|
||
|
* * Mamod Mehyar (https://github.com/mamod)
|
||
|
* * David Demelier (https://github.com/markand)
|
||
|
* * Tim Caswell (https://github.com/creationix)
|
||
|
* * Mitchell Blank Jr (https://github.com/mitchblank)
|
||
|
* * https://github.com/yushli
|
||
|
* * Seo Sanghyeon (https://github.com/sanxiyn)
|
||
|
* * Han ChoongWoo (https://github.com/tunz)
|
||
|
* * Joshua Peek (https://github.com/josh)
|
||
|
* * Bruce E. Pascoe (https://github.com/fatcerberus)
|
||
|
* * https://github.com/Kelledin
|
||
|
* * https://github.com/sstruchtrup
|
||
|
* * Michael Drake (https://github.com/tlsa)
|
||
|
* * https://github.com/chris-y
|
||
|
* * Laurent Zubiaur (https://github.com/lzubiaur)
|
||
|
* * Neil Kolban (https://github.com/nkolban)
|
||
|
*
|
||
|
* If you are accidentally missing from this list, send me an e-mail
|
||
|
* (``sami.vaarala@iki.fi``) and I'll fix the omission.
|
||
|
*
|
||
|
*
|
||
|
* =================================================================
|
||
|
* Modifications made by Bryan Roe, Copyright 2018 Intel Corporation
|
||
|
* =================================================================
|
||
|
*
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
|
||
|
var events = require('events');
|
||
|
var Promise = require('promise');
|
||
|
var dbgHTML = '77u/PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBYSFRNTCAxLjAgVHJhbnNpdGlvbmFsLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMS9EVEQveGh0bWwxLXRyYW5zaXRpb25hbC5kdGQiPg0KPGh0bWw+DQo8aGVhZD4NCjxtZXRhIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD11dGYtOCIgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiAvPg0KPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1FREdFIiAvPg0KPG1ldGEgbmFtZT0iZm9ybWF0LWRldGVjdGlvbiIgY29udGVudD0idGVsZXBob25lPW5vIiAvPg0KPHRpdGxlPldlYlJUQyBEZWJ1ZzwvdGl0bGU+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KYm9keSANCnsNCiAgICBtYXJnaW46IDA7DQoJcGFkZGluZzogMDsNCglib3JkZXI6IDA7DQoJY29sb3I6IGJsYWNrOw0KCWZvbnQtc2l6ZTogMTNweDsNCglmb250LWZhbWlseTogIlRyZWJ1Y2hldCBNUyIsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWY7DQoJYmFja2dyb3VuZC1jb2xvcjogI2QzZDlkNjsNCn0NCiNjb250YWluZXIgew0KCWJhY2tncm91bmQtY29sb3I6ICNmZmY7DQoJd2lkdGg6IDk2MHB4Ow0KCW1hcmdpbjogMCBhdXRvOw0KCWJvcmRlci10b3A6IDA7DQoJYm9yZGVyLXJpZ2h0OiAxcHggc29saWQgI2I3YjdiNzsNCglib3JkZXItYm90dG9tOiAwOw0KCWJvcmRlci1sZWZ0OiAxcHggc29saWQgI2I3YjdiNzsNCglwYWRkaW5nOiAwOw0KfQ0KI21hc3RoZWFkIHsNCgl3aWR0aDogYXV0bzsNCgltYXJnaW46IDA7DQoJcGFkZGluZzogMDsNCglvdmVyZmxvdzogYXV0bzsNCgl0ZXh0LWFsaWduOiByaWdodDsNCgliYWNrZ3JvdW5kLWNvbG9yOiAjMDM2Ow0KCXdpZHRoOiA5NjBweDsNCg0KICAgIGJhY2tncm91bmQ6IHJnYig0NSw4NiwxMzcpOyAvKiBPbGQgYnJvd3NlcnMgKi8NCiAgICBiYWNrZ3JvdW5kOiAtbW96LWxpbmVhci1ncmFkaWVudChsZWZ0LCAgcmdiYSg0NSw4NiwxMzcsMSkgMCUsIHJnYmEoMCw1MSwxMDIsMSkgMjklKTsgLyogRkYzLjYrICovDQogICAgYmFja2dyb3VuZDogLXdlYmtpdC1ncmFkaWVudChsaW5lYXIsIGxlZnQgdG9wLCByaWdodCB0b3AsIGNvbG9yLXN0b3AoMCUscmdiYSg0NSw4NiwxMzcsMSkpLCBjb2xvci1zdG9wKDI5JSxyZ2JhKDAsNTEsMTAyLDEpKSk7IC8qIENocm9tZSxTYWZhcmk0KyAqLw0KICAgIGJhY2tncm91bmQ6IC13ZWJraXQtbGluZWFyLWdyYWRpZW50KGxlZnQsICByZ2JhKDQ1LDg2LDEzNywxKSAwJSxyZ2JhKDAsNTEsMTAyLDEpIDI5JSk7IC8qIENocm9tZTEwKyxTYWZhcmk1LjErICovDQogICAgYmFja2dyb3VuZDogLW8tbGluZWFyLWdyYWRpZW50KGxlZnQsICByZ2JhKDQ1LDg2LDEzNywxKSAwJSxyZ2JhKDAsNTEsMTAyLDEpIDI5JSk7IC8qIE9wZXJhIDExLjEwKyAqLw0KICAgIGJhY2tncm91bmQ6IC1tcy1saW5lYXItZ3JhZGllbnQobGVmdCwgIHJnYmEoNDUsODYsMTM3LDEpIDAlLHJnYmEoMCw1MSwxMDIsMSkgMjklKTsgLyogSUUxMCsgKi8NCiAgICBiYWNrZ3JvdW5kOiBsaW5lYXItZ3JhZGllbnQodG8gcmlnaHQsICByZ2JhKDQ1LDg2LDEzNywxKSAwJSxyZ2JhKDAsNTEsMTAyLDEpIDI5JSk7IC8qIFczQyAqLw0KICAgIGZpbHRlcjogcHJvZ2lkOkRYSW1hZ2VUcmFuc2Zvcm0uTWljcm9zb2Z0LmdyYWRpZW50KCBzdGFydENvbG9yc3RyPScjMmQ1Njg5JywgZW5kQ29sb3JzdHI9JyMwMDMzNjYnLEdyYWRpZW50VHlwZT0xICk7IC8qIElFNi05ICovDQp9DQojY29sdW1uX2wgew0KCXBvc2l0aW9uOiByZWxhdGl2ZTsNCglmbG9hdDogbGVmdDsNCgl3aWR0aDogOTMwcHg7DQoJbWFyZ2luOiAwOw0KCXBhZGRpbmc6IDAgMTVweDsNCgliYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOw0KfQ0KI2Zvb3RlciB7DQoJY2xlYXI6IGJvdGg7DQoJb3ZlcmZsb3c6IGF1dG87DQoJd2lkdGg6IDk2MHB4Ow0KCXRleHQtYWxpZ246IGNlbnRlcjsNCgliYWNrZ3JvdW5kLWNvbG9yOiAjMTEzOTYyOw0KCXBhZGRpbmctdG9wOiA1cHg7DQoJcGFkZGluZy1ib3R0b206IDVweDsNCn0NCiNmb290ZXIgYSB7DQoJY29sb3I6ICNmZmY7DQoJdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmU7DQp9DQojZm9vdGVyIGE6aG92ZXIgew0KCWNvbG9yOiAjZmZmOw0KCXRleHQtZGVjb3JhdGlvbjogbm9uZTsNCn0NCmEgew0KCWNvbG9yOiAjMDM2Ow0KCXRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOw0KfQ0KLnN0eWxlMyB7DQoJdGV4dC1hbGlnbjogY2VudGVyOw0KCWNvbG9yOiB3aGl0ZTsNCgliYWNrZ3JvdW5kLWNvbG9yOiAjODA4MDgwOw0KCWZvbnQtd2VpZ2h0OiBib2xkOw0KfQ0KLnN0eWxlNiB7DQogICAgcGFkZGluZy10b3A6IDJweDsNCiAgICBwYWRkaW5nLWJvdHRvbTogMnB4Ow0KCWJhY2tncm91bmQtY29sb3I6ICNDMEMwQzA7DQp9DQoueHNlY3Rpb24gew0KICAgIGJhY2tncm91bmQtY29sb3I6ICNFMEUwRTA7DQogICAgcGFkZGluZzogNXB4IDIwcHggMjVweCAyMHB4Ow0KICAgIGJvcmRlci1yYWRpdXM6IDEwcHg7DQp9DQouZml4ZWRmb250IHsNCiAgICBmb250LWZhbWlseTpjb3VyaWVyLCAiY291cmllciBuZXciLCBtb25vc3BhY2U7DQp9DQouaW5mb2VudHJ5IHsNCiAgICBwYWRkaW5nLXRvcDoxcHg7DQp9DQouR0VORVJJQ19NT0RVTEV7DQogICAgY29sb3I6YmxhY2s7DQp9DQouVkFSUVVFUll7DQogICAgY29sb3I6YmxhY2s7DQp9DQouVkFSUkVTVUxUew0KICAgIGNvbG9yOmRhcmtncmVlbjsNCn0NCi5CUkVBS1BPSU5Uew0KICAgIGNvbG9yOmRhcmt2aW9sZXQNCn0NCi5CUkVBS1BPSU5UOmhvdmVyIA0Kew0KCWJhY2tncm91bmQtY29sb3I6ICNFRUU7DQp9DQouU1RBVEVDSEFOR0V7DQogICAgY29sb3I6ZGFya3Zpb2xldA0KfQ0KLlNPVVJDRUNPREVQQVJFTlQgDQp7DQogICAgd2lkdGg6MTAwMDBweA0KfQ0KLlNPVVJDRUNPREVQQVJFTlQ6aG92ZXIgDQp7DQoJYmFja2dyb3VuZC1jb2xvcjogI0VFRTsNCn0NCi5TT1VSQ0VDT0RFew0KICAgIGNvbG9yOmRhcmtncmVlbjsNCiAgICBwYWRkaW5nLWxlZnQ6MTBweDsNCiAgICBkaXNwbGF5OmlubGluZS1i
|
||
|
|
||
|
var optTargetHost = '127.0.0.1';
|
||
|
var optTargetPort = 9091;
|
||
|
var optHttpPort = 9092;
|
||
|
var optJsonProxyPort = 9093;
|
||
|
var optJsonProxy = false;
|
||
|
var optSourceSearchDirs = ['../tests/ecmascript'];
|
||
|
var optDumpDebugRead = null;
|
||
|
var optDumpDebugWrite = null;
|
||
|
var optDumpDebugPretty = null;
|
||
|
var optLogMessages = true;
|
||
|
|
||
|
// Marker objects for special protocol values
|
||
|
var DVAL_EOM = { type: 'eom' };
|
||
|
var DVAL_REQ = { type: 'req' };
|
||
|
var DVAL_REP = { type: 'rep' };
|
||
|
var DVAL_ERR = { type: 'err' };
|
||
|
var DVAL_NFY = { type: 'nfy' };
|
||
|
|
||
|
|
||
|
// Commands initiated by Duktape
|
||
|
var CMD_STATUS = 0x01;
|
||
|
var CMD_UNUSED_2 = 0x02; // Duktape 1.x: print notify
|
||
|
var CMD_UNUSED_3 = 0x03; // Duktape 1.x: alert notify
|
||
|
var CMD_UNUSED_4 = 0x04; // Duktape 1.x: log notify
|
||
|
var CMD_THROW = 0x05;
|
||
|
var CMD_DETACHING = 0x06;
|
||
|
var CMD_APPNOTIFY = 0x07;
|
||
|
|
||
|
// Commands initiated by the debug client (= us)
|
||
|
var CMD_BASICINFO = 0x10;
|
||
|
var CMD_TRIGGERSTATUS = 0x11;
|
||
|
var CMD_PAUSE = 0x12;
|
||
|
var CMD_RESUME = 0x13;
|
||
|
var CMD_STEPINTO = 0x14;
|
||
|
var CMD_STEPOVER = 0x15;
|
||
|
var CMD_STEPOUT = 0x16;
|
||
|
var CMD_LISTBREAK = 0x17;
|
||
|
var CMD_ADDBREAK = 0x18;
|
||
|
var CMD_DELBREAK = 0x19;
|
||
|
var CMD_GETVAR = 0x1a;
|
||
|
var CMD_PUTVAR = 0x1b;
|
||
|
var CMD_GETCALLSTACK = 0x1c;
|
||
|
var CMD_GETLOCALS = 0x1d;
|
||
|
var CMD_EVAL = 0x1e;
|
||
|
var CMD_DETACH = 0x1f;
|
||
|
var CMD_DUMPHEAP = 0x20;
|
||
|
var CMD_GETBYTECODE = 0x21;
|
||
|
|
||
|
// Constants
|
||
|
var UI_MESSAGE_CLIPLEN = 128;
|
||
|
var LOCALS_CLIPLEN = 64;
|
||
|
var EVAL_CLIPLEN = 4096;
|
||
|
var GETVAR_CLIPLEN = 4096;
|
||
|
var SUPPORTED_DEBUG_PROTOCOL_VERSION = 2;
|
||
|
|
||
|
|
||
|
var debugCommandNames = {};
|
||
|
debugCommandNames['0'] = 'Reserved_0';
|
||
|
debugCommandNames['1'] = 'Status';
|
||
|
debugCommandNames['2'] = 'Reserved_2';
|
||
|
debugCommandNames['3'] = 'Reserved_3';
|
||
|
debugCommandNames['4'] = 'Reserved_4';
|
||
|
debugCommandNames['5'] = 'Throw';
|
||
|
debugCommandNames['6'] = 'Detaching';
|
||
|
debugCommandNames['7'] = 'AppNotify';
|
||
|
debugCommandNames['8'] = 'Reserved_8';
|
||
|
debugCommandNames['9'] = 'Reserved_9';
|
||
|
debugCommandNames['10'] = 'Reserved_10';
|
||
|
debugCommandNames['11'] = 'Reserved_11';
|
||
|
debugCommandNames['12'] = 'Reserved_12';
|
||
|
debugCommandNames['13'] = 'Reserved_13';
|
||
|
debugCommandNames['14'] = 'Reserved_14';
|
||
|
debugCommandNames['15'] = 'Reserved_15';
|
||
|
debugCommandNames['16'] = 'BasicInfo';
|
||
|
debugCommandNames['17'] = 'TriggerStatus';
|
||
|
debugCommandNames['18'] = 'Pause';
|
||
|
debugCommandNames['19'] = 'Resume';
|
||
|
debugCommandNames['20'] = 'StepInto';
|
||
|
debugCommandNames['21'] = 'StepOver';
|
||
|
debugCommandNames['22'] = 'StepOut';
|
||
|
debugCommandNames['23'] = 'ListBreak';
|
||
|
debugCommandNames['24'] = 'AddBreak';
|
||
|
debugCommandNames['25'] = 'DelBreak';
|
||
|
debugCommandNames['26'] = 'GetVar';
|
||
|
debugCommandNames['27'] = 'PutVar';
|
||
|
debugCommandNames['28'] = 'GetCallStack';
|
||
|
debugCommandNames['29'] = 'GetLocals';
|
||
|
debugCommandNames['30'] = 'Eval';
|
||
|
debugCommandNames['31'] = 'Detach';
|
||
|
debugCommandNames['32'] = 'DumpHeap';
|
||
|
debugCommandNames['33'] = 'GetBytecode';
|
||
|
debugCommandNames['34'] = 'AppRequest';
|
||
|
debugCommandNames['35'] = 'GetHeapObjInfo';
|
||
|
debugCommandNames['36'] = 'GetObjPropDesc';
|
||
|
debugCommandNames['37'] = 'GetObjPropDescRange';
|
||
|
|
||
|
// Duktape heaphdr type constants, must match C headers
|
||
|
var DUK_HTYPE_STRING = 0;
|
||
|
var DUK_HTYPE_OBJECT = 1;
|
||
|
var DUK_HTYPE_BUFFER = 2;
|
||
|
|
||
|
/*
|
||
|
* Debugger output formatting
|
||
|
*/
|
||
|
|
||
|
function formatDebugValue(v) {
|
||
|
var buf, dec, len;
|
||
|
|
||
|
// See doc/debugger.rst for format description.
|
||
|
|
||
|
if (typeof v === 'object' && v !== null) {
|
||
|
// Note: typeof null === 'object', so null special case explicitly
|
||
|
if (v.type === 'eom') {
|
||
|
return new Buffer([0x00]);
|
||
|
} else if (v.type === 'req') {
|
||
|
return new Buffer([0x01]);
|
||
|
} else if (v.type === 'rep') {
|
||
|
return new Buffer([0x02]);
|
||
|
} else if (v.type === 'err') {
|
||
|
return new Buffer([0x03]);
|
||
|
} else if (v.type === 'nfy') {
|
||
|
return new Buffer([0x04]);
|
||
|
} else if (v.type === 'unused') {
|
||
|
return new Buffer([0x15]);
|
||
|
} else if (v.type === 'undefined') {
|
||
|
return new Buffer([0x16]);
|
||
|
} else if (v.type === 'number') {
|
||
|
dec = new Buffer(v.data, 'hex');
|
||
|
len = dec.length;
|
||
|
if (len !== 8) {
|
||
|
throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
|
||
|
}
|
||
|
buf = Buffer.alloc(1 + len);
|
||
|
buf[0] = 0x1a;
|
||
|
dec.copy(buf, 1);
|
||
|
return buf;
|
||
|
} else if (v.type === 'buffer') {
|
||
|
dec = new Buffer(v.data, 'hex');
|
||
|
len = dec.length;
|
||
|
if (len <= 0xffff) {
|
||
|
buf = Buffer.alloc(3 + len);
|
||
|
buf[0] = 0x14;
|
||
|
buf[1] = (len >> 8) & 0xff;
|
||
|
buf[2] = (len >> 0) & 0xff;
|
||
|
dec.copy(buf, 3);
|
||
|
return buf;
|
||
|
} else {
|
||
|
buf = Buffer.alloc(5 + len);
|
||
|
buf[0] = 0x13;
|
||
|
buf[1] = (len >> 24) & 0xff;
|
||
|
buf[2] = (len >> 16) & 0xff;
|
||
|
buf[3] = (len >> 8) & 0xff;
|
||
|
buf[4] = (len >> 0) & 0xff;
|
||
|
dec.copy(buf, 5);
|
||
|
return buf;
|
||
|
}
|
||
|
} else if (v.type === 'object') {
|
||
|
dec = new Buffer(v.pointer, 'hex');
|
||
|
len = dec.length;
|
||
|
buf = Buffer.alloc(3 + len);
|
||
|
buf[0] = 0x1b;
|
||
|
buf[1] = v.class;
|
||
|
buf[2] = len;
|
||
|
dec.copy(buf, 3);
|
||
|
return buf;
|
||
|
} else if (v.type === 'pointer') {
|
||
|
dec = new Buffer(v.pointer, 'hex');
|
||
|
len = dec.length;
|
||
|
buf = Buffer.alloc(2 + len);
|
||
|
buf[0] = 0x1c;
|
||
|
buf[1] = len;
|
||
|
dec.copy(buf, 2);
|
||
|
return buf;
|
||
|
} else if (v.type === 'lightfunc') {
|
||
|
dec = new Buffer(v.pointer, 'hex');
|
||
|
len = dec.length;
|
||
|
buf = Buffer.alloc(4 + len);
|
||
|
buf[0] = 0x1d;
|
||
|
buf[1] = (v.flags >> 8) & 0xff;
|
||
|
buf[2] = v.flags & 0xff;
|
||
|
buf[3] = len;
|
||
|
dec.copy(buf, 4);
|
||
|
return buf;
|
||
|
} else if (v.type === 'heapptr') {
|
||
|
dec = new Buffer(v.pointer, 'hex');
|
||
|
len = dec.length;
|
||
|
buf = Buffer.alloc(2 + len);
|
||
|
buf[0] = 0x1e;
|
||
|
buf[1] = len;
|
||
|
dec.copy(buf, 2);
|
||
|
return buf;
|
||
|
}
|
||
|
} else if (v === null) {
|
||
|
return new Buffer([0x17]);
|
||
|
} else if (typeof v === 'boolean') {
|
||
|
return new Buffer([v ? 0x18 : 0x19]);
|
||
|
} else if (typeof v === 'number') {
|
||
|
if (Math.floor(v) === v && /* whole */
|
||
|
(v !== 0 || 1 / v > 0) && /* not negative zero */
|
||
|
v >= -0x80000000 && v <= 0x7fffffff) {
|
||
|
// Represented signed 32-bit integers as plain integers.
|
||
|
// Debugger code expects this for all fields that are not
|
||
|
// duk_tval representations (e.g. command numbers and such).
|
||
|
if (v >= 0x00 && v <= 0x3f) {
|
||
|
return new Buffer([0x80 + v]);
|
||
|
} else if (v >= 0x0000 && v <= 0x3fff) {
|
||
|
return new Buffer([0xc0 + (v >> 8), v & 0xff]);
|
||
|
} else if (v >= -0x80000000 && v <= 0x7fffffff) {
|
||
|
return new Buffer([0x10,
|
||
|
(v >> 24) & 0xff,
|
||
|
(v >> 16) & 0xff,
|
||
|
(v >> 8) & 0xff,
|
||
|
(v >> 0) & 0xff]);
|
||
|
} else {
|
||
|
throw new Error('internal error when encoding integer to dvalue: ' + v);
|
||
|
}
|
||
|
} else {
|
||
|
// Represent non-integers as IEEE double dvalues
|
||
|
buf = Buffer.alloc(1 + 8);
|
||
|
buf[0] = 0x1a;
|
||
|
buf.writeDoubleBE(v, 1);
|
||
|
return buf;
|
||
|
}
|
||
|
} else if (typeof v === 'string') {
|
||
|
if (v.length < 0 || v.length > 0xffffffff) {
|
||
|
// Not possible in practice.
|
||
|
throw new TypeError('cannot convert to dvalue, invalid string length: ' + v.length);
|
||
|
}
|
||
|
if (v.length <= 0x1f) {
|
||
|
buf = Buffer.alloc(1 + v.length);
|
||
|
buf[0] = 0x60 + v.length;
|
||
|
writeDebugStringToBuffer(v, buf, 1);
|
||
|
return buf;
|
||
|
} else if (v.length <= 0xffff) {
|
||
|
buf = Buffer.alloc(3 + v.length);
|
||
|
buf[0] = 0x12;
|
||
|
buf[1] = (v.length >> 8) & 0xff;
|
||
|
buf[2] = (v.length >> 0) & 0xff;
|
||
|
writeDebugStringToBuffer(v, buf, 3);
|
||
|
return buf;
|
||
|
} else {
|
||
|
buf = Buffer.alloc(5 + v.length);
|
||
|
buf[0] = 0x11;
|
||
|
buf[1] = (v.length >> 24) & 0xff;
|
||
|
buf[2] = (v.length >> 16) & 0xff;
|
||
|
buf[3] = (v.length >> 8) & 0xff;
|
||
|
buf[4] = (v.length >> 0) & 0xff;
|
||
|
writeDebugStringToBuffer(v, buf, 5);
|
||
|
return buf;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Shouldn't come here.
|
||
|
throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
|
||
|
}
|
||
|
|
||
|
|
||
|
function DebugProtocolParser(inputStream,
|
||
|
protocolVersion,
|
||
|
rawDumpFileName,
|
||
|
textDumpFileName,
|
||
|
textDumpFilePrefix,
|
||
|
hexDumpConsolePrefix,
|
||
|
textDumpConsolePrefix)
|
||
|
{
|
||
|
var _this = this;
|
||
|
this._ObjectID = 'DebugProtocolParser';
|
||
|
this.inputStream = inputStream;
|
||
|
this.closed = false; // stream is closed/broken, don't parse anymore
|
||
|
this.bytes = 0;
|
||
|
this.dvalues = 0;
|
||
|
this.messages = 0;
|
||
|
this.requests = 0;
|
||
|
this.prevBytes = 0;
|
||
|
this.bytesPerSec = 0;
|
||
|
this.statsTimer = null;
|
||
|
this.readableNumberValue = true;
|
||
|
|
||
|
events.EventEmitter.call(this);
|
||
|
|
||
|
var buf = Buffer.alloc(0); // accumulate data
|
||
|
var msg = []; // accumulated message until EOM
|
||
|
var versionIdentification;
|
||
|
|
||
|
var statsInterval = 2000;
|
||
|
var statsIntervalSec = statsInterval / 1000;
|
||
|
this.statsTimer = setInterval(function () {
|
||
|
_this.bytesPerSec = (_this.bytes - _this.prevBytes) / statsIntervalSec;
|
||
|
_this.prevBytes = _this.bytes;
|
||
|
_this.emit('stats-update');
|
||
|
}, statsInterval);
|
||
|
|
||
|
function consume(n) {
|
||
|
var tmp = Buffer.alloc(buf.length - n);
|
||
|
buf.copy(tmp, 0, n);
|
||
|
buf = tmp;
|
||
|
}
|
||
|
|
||
|
inputStream.on('data', function (data) {
|
||
|
var i, n, x, v, gotValue, len, t, tmpbuf, verstr;
|
||
|
var prettyMsg;
|
||
|
|
||
|
if (_this.closed || !_this.inputStream)
|
||
|
{
|
||
|
console.error('Ignoring incoming data from closed input stream, len ' + data.length);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
_this.bytes += data.length;
|
||
|
if (rawDumpFileName) {
|
||
|
fs.appendFileSync(rawDumpFileName, data);
|
||
|
}
|
||
|
if (hexDumpConsolePrefix) {
|
||
|
console.info1(hexDumpConsolePrefix + data.toString('hex'));
|
||
|
}
|
||
|
|
||
|
buf = Buffer.concat([buf, data]);
|
||
|
|
||
|
// Protocol version handling. When dumping an output stream, the
|
||
|
// caller gives a non-null protocolVersion so we don't read one here.
|
||
|
|
||
|
if (protocolVersion == null) {
|
||
|
if (buf.length > 1024) {
|
||
|
_this.emit('transport-error', 'Parse error (version identification too long), dropping connection');
|
||
|
_this.close();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
for (i = 0, n = buf.length; i < n; i++)
|
||
|
{
|
||
|
if (buf[i] == 0x0a)
|
||
|
{
|
||
|
tmpbuf = Buffer.alloc(i);
|
||
|
buf.copy(tmpbuf, 0, 0, i);
|
||
|
consume(i + 1);
|
||
|
verstr = tmpbuf.toString();
|
||
|
t = verstr.split(' ');
|
||
|
protocolVersion = Number(t[0]);
|
||
|
versionIdentification = verstr;
|
||
|
|
||
|
_this.emit('protocol-version', {
|
||
|
protocolVersion: protocolVersion,
|
||
|
versionIdentification: versionIdentification
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (protocolVersion == null)
|
||
|
{
|
||
|
// Still waiting for version identification to complete.
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Parse complete dvalues (quite inefficient now) by trial parsing.
|
||
|
// Consume a value only when it's fully present in 'buf'.
|
||
|
// See doc/debugger.rst for format description.
|
||
|
|
||
|
while (buf.length > 0) {
|
||
|
x = buf[0];
|
||
|
v = undefined;
|
||
|
gotValue = false; // used to flag special values like undefined
|
||
|
|
||
|
if (x >= 0xc0) {
|
||
|
// 0xc0...0xff: integers 0-16383
|
||
|
if (buf.length >= 2) {
|
||
|
v = ((x - 0xc0) << 8) + buf[1];
|
||
|
consume(2);
|
||
|
}
|
||
|
} else if (x >= 0x80) {
|
||
|
// 0x80...0xbf: integers 0-63
|
||
|
v = x - 0x80;
|
||
|
consume(1);
|
||
|
} else if (x >= 0x60) {
|
||
|
// 0x60...0x7f: strings with length 0-31
|
||
|
len = x - 0x60;
|
||
|
if (buf.length >= 1 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 1, 1 + len);
|
||
|
v = bufferToDebugString(v);
|
||
|
consume(1 + len);
|
||
|
}
|
||
|
} else {
|
||
|
switch (x) {
|
||
|
case 0x00: v = DVAL_EOM; consume(1); break;
|
||
|
case 0x01: v = DVAL_REQ; consume(1); break;
|
||
|
case 0x02: v = DVAL_REP; consume(1); break;
|
||
|
case 0x03: v = DVAL_ERR; consume(1); break;
|
||
|
case 0x04: v = DVAL_NFY; consume(1); break;
|
||
|
case 0x10: // 4-byte signed integer
|
||
|
if (buf.length >= 5) {
|
||
|
v = buf.readInt32BE(1);
|
||
|
consume(5);
|
||
|
}
|
||
|
break;
|
||
|
case 0x11: // 4-byte string
|
||
|
if (buf.length >= 5) {
|
||
|
len = buf.readUInt32BE(1);
|
||
|
if (buf.length >= 5 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 5, 5 + len);
|
||
|
v = bufferToDebugString(v);
|
||
|
consume(5 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x12: // 2-byte string
|
||
|
if (buf.length >= 3) {
|
||
|
len = buf.readUInt16BE(1);
|
||
|
if (buf.length >= 3 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 3, 3 + len);
|
||
|
v = bufferToDebugString(v);
|
||
|
consume(3 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x13: // 4-byte buffer
|
||
|
if (buf.length >= 5) {
|
||
|
len = buf.readUInt32BE(1);
|
||
|
if (buf.length >= 5 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 5, 5 + len);
|
||
|
v = { type: 'buffer', data: v.toString('hex') };
|
||
|
consume(5 + len);
|
||
|
// Value could be a Node.js buffer directly, but
|
||
|
// we prefer all dvalues to be JSON compatible
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x14: // 2-byte buffer
|
||
|
if (buf.length >= 3) {
|
||
|
len = buf.readUInt16BE(1);
|
||
|
if (buf.length >= 3 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 3, 3 + len);
|
||
|
v = { type: 'buffer', data: v.toString('hex') };
|
||
|
consume(3 + len);
|
||
|
// Value could be a Node.js buffer directly, but
|
||
|
// we prefer all dvalues to be JSON compatible
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x15: // unused/none
|
||
|
v = { type: 'unused' };
|
||
|
consume(1);
|
||
|
break;
|
||
|
case 0x16: // undefined
|
||
|
v = { type: 'undefined' };
|
||
|
gotValue = true; // indicate 'v' is actually set
|
||
|
consume(1);
|
||
|
break;
|
||
|
case 0x17: // null
|
||
|
v = null;
|
||
|
gotValue = true; // indicate 'v' is actually set
|
||
|
consume(1);
|
||
|
break;
|
||
|
case 0x18: // true
|
||
|
v = true;
|
||
|
consume(1);
|
||
|
break;
|
||
|
case 0x19: // false
|
||
|
v = false;
|
||
|
consume(1);
|
||
|
break;
|
||
|
case 0x1a: // number (IEEE double), big endian
|
||
|
if (buf.length >= 9) {
|
||
|
v = Buffer.alloc(8);
|
||
|
buf.copy(v, 0, 1, 9);
|
||
|
v = { type: 'number', data: v.toString('hex') };
|
||
|
|
||
|
if (_this.readableNumberValue) {
|
||
|
// The value key should not be used programmatically,
|
||
|
// it is just there to make the dumps more readable.
|
||
|
v.value = buf.readDoubleBE(1);
|
||
|
}
|
||
|
consume(9);
|
||
|
}
|
||
|
break;
|
||
|
case 0x1b: // object
|
||
|
if (buf.length >= 3) {
|
||
|
len = buf[2];
|
||
|
if (buf.length >= 3 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 3, 3 + len);
|
||
|
v = { type: 'object', 'class': buf[1], pointer: v.toString('hex') };
|
||
|
consume(3 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x1c: // pointer
|
||
|
if (buf.length >= 2) {
|
||
|
len = buf[1];
|
||
|
if (buf.length >= 2 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 2, 2 + len);
|
||
|
v = { type: 'pointer', pointer: v.toString('hex') };
|
||
|
consume(2 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x1d: // lightfunc
|
||
|
if (buf.length >= 4) {
|
||
|
len = buf[3];
|
||
|
if (buf.length >= 4 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 4, 4 + len);
|
||
|
v = { type: 'lightfunc', flags: buf.readUInt16BE(1), pointer: v.toString('hex') };
|
||
|
consume(4 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 0x1e: // heapptr
|
||
|
if (buf.length >= 2) {
|
||
|
len = buf[1];
|
||
|
if (buf.length >= 2 + len) {
|
||
|
v = Buffer.alloc(len);
|
||
|
buf.copy(v, 0, 2, 2 + len);
|
||
|
v = { type: 'heapptr', pointer: v.toString('hex') };
|
||
|
consume(2 + len);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
_this.emit('transport-error', 'Parse error, dropping connection');
|
||
|
_this.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (typeof v === 'undefined' && !gotValue) {
|
||
|
break;
|
||
|
}
|
||
|
msg.push(v);
|
||
|
_this.dvalues++;
|
||
|
|
||
|
// Could emit a 'debug-value' event here, but that's not necessary
|
||
|
// because the receiver will just collect statistics which can also
|
||
|
// be done using the finished message.
|
||
|
|
||
|
if (v === DVAL_EOM)
|
||
|
{
|
||
|
_this.messages++;
|
||
|
|
||
|
if (textDumpFileName || textDumpConsolePrefix) {
|
||
|
prettyMsg = prettyDebugMessage(msg);
|
||
|
if (textDumpFileName) {
|
||
|
fs.appendFileSync(textDumpFileName, (textDumpFilePrefix || '') + prettyMsg + '\n');
|
||
|
}
|
||
|
if (textDumpConsolePrefix) {
|
||
|
console.info1(textDumpConsolePrefix + prettyMsg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_this.emit('debug-message', msg);
|
||
|
msg = []; // new object, old may be in circulation for a while
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Not all streams will emit this.
|
||
|
inputStream.on('error', function (err) {
|
||
|
_this.emit('transport-error', err);
|
||
|
_this.close();
|
||
|
});
|
||
|
|
||
|
// Not all streams will emit this.
|
||
|
inputStream.on('close', function () {
|
||
|
_this.close();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
DebugProtocolParser.prototype.close = function () {
|
||
|
// Although the underlying transport may not have a close() or destroy()
|
||
|
// method or even a 'close' event, this method is always available and
|
||
|
// will generate a 'transport-close'.
|
||
|
//
|
||
|
// The caller is responsible for closing the underlying stream if that
|
||
|
// is necessary.
|
||
|
|
||
|
if (this.closed) { return; }
|
||
|
|
||
|
this.closed = true;
|
||
|
if (this.statsTimer) {
|
||
|
clearInterval(this.statsTimer);
|
||
|
this.statsTimer = null;
|
||
|
}
|
||
|
this.emit('transport-close');
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Debugger implementation
|
||
|
*
|
||
|
* A debugger instance communicates with the debug target and maintains
|
||
|
* persistent debug state so that the current state can be resent to the
|
||
|
* socket.io client (web UI) if it reconnects. Whenever the debugger state
|
||
|
* is changed an event is generated. The socket.io handler will listen to
|
||
|
* state change events and push the necessary updates to the web UI, often
|
||
|
* in a rate limited fashion or using a client pull to ensure the web UI
|
||
|
* is not overloaded.
|
||
|
*
|
||
|
* The debugger instance assumes that if the debug protocol connection is
|
||
|
* re-established, it is always to the same target. There is no separate
|
||
|
* abstraction for a debugger session.
|
||
|
*/
|
||
|
|
||
|
function Debugger()
|
||
|
{
|
||
|
events.EventEmitter.call(this);
|
||
|
this._ObjectID = 'Debugger';
|
||
|
this.web = null; // web UI singleton
|
||
|
this.targetStream = null; // transport connection to target
|
||
|
this.outputPassThroughStream = null; // dummy passthrough for message dumping
|
||
|
this.inputParser = null; // parser for incoming debug messages
|
||
|
this.outputParser = null; // parser for outgoing debug messages (stats, dumping)
|
||
|
this.protocolVersion = null;
|
||
|
this.dukVersion = null;
|
||
|
this.dukGitDescribe = null;
|
||
|
this.targetInfo = null;
|
||
|
this.debugger_attached = false;
|
||
|
this.handshook = false;
|
||
|
this.reqQueue = null;
|
||
|
this.stats = { // stats for current debug connection
|
||
|
rxBytes: 0, rxDvalues: 0, rxMessages: 0, rxBytesPerSec: 0,
|
||
|
txBytes: 0, txDvalues: 0, txMessages: 0, txBytesPerSec: 0
|
||
|
};
|
||
|
this.execStatus = {
|
||
|
attached: false,
|
||
|
state: 'detached',
|
||
|
fileName: '',
|
||
|
funcName: '',
|
||
|
line: 0,
|
||
|
pc: 0
|
||
|
};
|
||
|
this.breakpoints = [];
|
||
|
this.callstack = [];
|
||
|
this.locals = [];
|
||
|
this.messageLines = [];
|
||
|
this.messageScrollBack = 100;
|
||
|
|
||
|
this.on('attached', function OnAttached()
|
||
|
{
|
||
|
this.websocket.write({ cmd: 'ATTACH' });
|
||
|
console.info1('Debug transport connected');
|
||
|
|
||
|
this.debugger_attached = true;
|
||
|
this.reqQueue = [];
|
||
|
this.uiMessage('debugger-info', 'Debug transport connected');
|
||
|
|
||
|
this.sendStatusRequest().then(function () { statusPending = false; console.info1('finally'); }, console.info1);
|
||
|
hostCooperate();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
Debugger.prototype.decodeBytecodeFromBuffer = function (buf, consts, funcs)
|
||
|
{
|
||
|
var i, j, n, m, ins, pc;
|
||
|
var res = [];
|
||
|
var op, str, args, comments;
|
||
|
|
||
|
// XXX: add constants inline to preformatted output (e.g. for strings,
|
||
|
// add a short escaped snippet as a comment on the line after the
|
||
|
// compact argument list).
|
||
|
|
||
|
for (i = 0, n = buf.length; i < n; i += 4) {
|
||
|
pc = i / 4;
|
||
|
|
||
|
// shift forces unsigned
|
||
|
if (this.endianness === 'little') {
|
||
|
ins = buf.readInt32LE(i) >>> 0;
|
||
|
} else {
|
||
|
ins = buf.readInt32BE(i) >>> 0;
|
||
|
}
|
||
|
|
||
|
op = dukOpcodes.opcodes[ins & 0xff];
|
||
|
|
||
|
args = [];
|
||
|
comments = [];
|
||
|
if (op.args) {
|
||
|
for (j = 0, m = op.args.length; j < m; j++) {
|
||
|
var A = (ins >>> 8) & 0xff;
|
||
|
var B = (ins >>> 16) & 0xff;
|
||
|
var C = (ins >>> 24) & 0xff;
|
||
|
var BC = (ins >>> 16) & 0xffff;
|
||
|
var ABC = (ins >>> 8) & 0xffffff;
|
||
|
var Bconst = op & 0x01;
|
||
|
var Cconst = op & 0x02;
|
||
|
|
||
|
switch (op.args[j]) {
|
||
|
case 'A_R': args.push('r' + A); break;
|
||
|
case 'A_RI': args.push('r' + A + '(indirect)'); break;
|
||
|
case 'A_C': args.push('c' + A); break;
|
||
|
case 'A_H': args.push('0x' + A.toString(16)); break;
|
||
|
case 'A_I': args.push(A.toString(10)); break;
|
||
|
case 'A_B': args.push(A ? 'true' : 'false'); break;
|
||
|
case 'B_RC': args.push((Bconst ? 'c' : 'r') + B); break;
|
||
|
case 'B_R': args.push('r' + B); break;
|
||
|
case 'B_RI': args.push('r' + B + '(indirect)'); break;
|
||
|
case 'B_C': args.push('c' + B); break;
|
||
|
case 'B_H': args.push('0x' + B.toString(16)); break;
|
||
|
case 'B_I': args.push(B.toString(10)); break;
|
||
|
case 'C_RC': args.push((Cconst ? 'c' : 'r') + C); break;
|
||
|
case 'C_R': args.push('r' + C); break;
|
||
|
case 'C_RI': args.push('r' + C + '(indirect)'); break;
|
||
|
case 'C_C': args.push('c' + C); break;
|
||
|
case 'C_H': args.push('0x' + C.toString(16)); break;
|
||
|
case 'C_I': args.push(C.toString(10)); break;
|
||
|
case 'BC_R': args.push('r' + BC); break;
|
||
|
case 'BC_C': args.push('c' + BC); break;
|
||
|
case 'BC_H': args.push('0x' + BC.toString(16)); break;
|
||
|
case 'BC_I': args.push(BC.toString(10)); break;
|
||
|
case 'ABC_H': args.push(ABC.toString(16)); break;
|
||
|
case 'ABC_I': args.push(ABC.toString(10)); break;
|
||
|
case 'BC_LDINT': args.push(BC - (1 << 15)); break;
|
||
|
case 'BC_LDINTX': args.push(BC - 0); break; // no bias in LDINTX
|
||
|
case 'ABC_JUMP': {
|
||
|
var pc_add = ABC - (1 << 23) + 1; // pc is preincremented before adding
|
||
|
var pc_dst = pc + pc_add;
|
||
|
args.push(pc_dst + ' (' + (pc_add >= 0 ? '+' : '') + pc_add + ')');
|
||
|
break;
|
||
|
}
|
||
|
default: args.push('?'); break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (op.flags) {
|
||
|
for (j = 0, m = op.flags.length; j < m; j++) {
|
||
|
if (ins & op.flags[j].mask) {
|
||
|
comments.push(op.flags[j].name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (args.length > 0) {
|
||
|
str = sprintf('%05d %08x %-10s %s', pc, ins, op.name, args.join(', '));
|
||
|
} else {
|
||
|
str = sprintf('%05d %08x %-10s', pc, ins, op.name);
|
||
|
}
|
||
|
if (comments.length > 0) {
|
||
|
str = sprintf('%-44s ; %s', str, comments.join(', '));
|
||
|
}
|
||
|
|
||
|
res.push({
|
||
|
str: str,
|
||
|
ins: ins
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return res;
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.uiMessage = function (type, val)
|
||
|
{
|
||
|
var msg;
|
||
|
if (typeof type === 'object') {
|
||
|
msg = type;
|
||
|
} else if (typeof type === 'string') {
|
||
|
msg = { type: type, message: val };
|
||
|
} else {
|
||
|
throw new TypeError('invalid ui message: ' + type);
|
||
|
}
|
||
|
this.messageLines.push(msg);
|
||
|
while (this.messageLines.length > this.messageScrollBack) {
|
||
|
this.messageLines.shift();
|
||
|
}
|
||
|
this.emit('ui-message-update'); // just trigger a sync, gets rate limited
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendRequest = function (msg)
|
||
|
{
|
||
|
var _this = this;
|
||
|
return new Promise(function (resolve, reject) {
|
||
|
var dvals = [];
|
||
|
var dval;
|
||
|
var data;
|
||
|
var i;
|
||
|
|
||
|
if (!_this.debugger_attached || !_this.handshook || !_this.reqQueue || !_this.targetStream)
|
||
|
{
|
||
|
console.error(_this.debugger_attached, _this.handshook, _this.reqQueue);
|
||
|
throw new Error('invalid state for sendRequest');
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < msg.length; i++) {
|
||
|
try {
|
||
|
dval = formatDebugValue(msg[i]);
|
||
|
} catch (e) {
|
||
|
console.error('Failed to format dvalue, dropping connection: ' + e);
|
||
|
console.error(e.stack || e);
|
||
|
_this.targetStream.destroy();
|
||
|
throw new Error('failed to format dvalue');
|
||
|
}
|
||
|
dvals.push(dval);
|
||
|
}
|
||
|
|
||
|
data = Buffer.concat(dvals);
|
||
|
|
||
|
//console.log('Writing: ' + data.toString('hex'));
|
||
|
|
||
|
_this.targetStream.write(data);
|
||
|
|
||
|
if (_this.outputPassThroughStream) { _this.outputPassThroughStream.write(data); } // stats and dumping
|
||
|
if (optLogMessages) {
|
||
|
console.info1('Request ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
|
||
|
}
|
||
|
|
||
|
if (!_this.reqQueue) {
|
||
|
throw new Error('no reqQueue');
|
||
|
}
|
||
|
_this.reqQueue.push({
|
||
|
reqMsg: msg,
|
||
|
reqCmd: msg[1],
|
||
|
resolveCb: resolve,
|
||
|
rejectCb: reject
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendBasicInfoRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_BASICINFO, DVAL_EOM]).then(function (msg) {
|
||
|
_this.dukVersion = msg[1];
|
||
|
_this.dukGitDescribe = msg[2];
|
||
|
_this.targetInfo = msg[3];
|
||
|
_this.endianness = { 1: 'little', 2: 'mixed', 3: 'big' }[msg[4]] || 'unknown';
|
||
|
_this.emit('basic-info-update');
|
||
|
return msg;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendGetVarRequest = function (varname, level) {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_GETVAR, (typeof level === 'number' ? level : -1), varname, DVAL_EOM]).then(function (msg) {
|
||
|
return { found: msg[1] === 1, value: msg[2] };
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendPutVarRequest = function (varname, varvalue, level) {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_PUTVAR, (typeof level === 'number' ? level : -1), varname, varvalue, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendInvalidCommandTestRequest = function () {
|
||
|
// Intentional invalid command
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, 0xdeadbeef, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendStatusRequest = function () {
|
||
|
// Send a status request to trigger a status notify, result is ignored:
|
||
|
// target sends a status notify instead of a meaningful reply
|
||
|
//console.log('this', this.sendRequest);
|
||
|
return (this.sendRequest([DVAL_REQ, CMD_TRIGGERSTATUS, DVAL_EOM]));
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendBreakpointListRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_LISTBREAK, DVAL_EOM]).then(function (msg) {
|
||
|
var i, n;
|
||
|
var breakpts = [];
|
||
|
|
||
|
for (i = 1, n = msg.length - 1; i < n; i += 2) {
|
||
|
breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] });
|
||
|
}
|
||
|
|
||
|
_this.breakpoints = breakpts;
|
||
|
_this.emit('breakpoints-update', breakpts);
|
||
|
return msg;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendGetLocalsRequest = function (level) {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_GETLOCALS, (typeof level === 'number' ? level : -1), DVAL_EOM]).then(function (msg) {
|
||
|
var i;
|
||
|
var locals = [];
|
||
|
|
||
|
for (i = 1; i <= msg.length - 2; i += 2)
|
||
|
{
|
||
|
// XXX: do pretty printing in debug client for now
|
||
|
locals.push({ key: msg[i], value: prettyUiDebugValue(msg[i + 1], LOCALS_CLIPLEN), type: (msg[i+1]?msg[i+1].type:'undefined') });
|
||
|
}
|
||
|
|
||
|
_this.locals = locals;
|
||
|
_this.emit('locals-update');
|
||
|
return locals;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendGetCallStackRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_GETCALLSTACK, DVAL_EOM]).then(function (msg) {
|
||
|
var i;
|
||
|
var stack = [];
|
||
|
|
||
|
for (i = 1; i + 3 <= msg.length - 1; i += 4)
|
||
|
{
|
||
|
stack.push({
|
||
|
fileName: msg[i],
|
||
|
funcName: msg[i + 1],
|
||
|
lineNumber: msg[i + 2],
|
||
|
pc: msg[i + 3]
|
||
|
});
|
||
|
}
|
||
|
|
||
|
_this.callstack = stack;
|
||
|
_this.emit('callstack-update');
|
||
|
return msg;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendStepIntoRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_STEPINTO, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendStepOverRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_STEPOVER, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendStepOutRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_STEPOUT, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendPauseRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_PAUSE, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendResumeRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_RESUME, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendEvalRequest = function (evalInput, level) {
|
||
|
var _this = this;
|
||
|
// Use explicit level if given. If no level is given, use null if the call
|
||
|
// stack is empty, -1 otherwise. This works well when we're paused and the
|
||
|
// callstack information is not liable to change before we do an Eval.
|
||
|
console.info1('levelType = ' + typeof (level));
|
||
|
if (typeof level !== 'number') {
|
||
|
level = this.callstack && this.callstack.length > 0 ? -1 : null;
|
||
|
}
|
||
|
console.info1('level=' + level);
|
||
|
return this.sendRequest([DVAL_REQ, CMD_EVAL, level, evalInput, DVAL_EOM]).then(function (msg) {
|
||
|
return { error: msg[1] === 1 /*error*/, value: msg[2] };
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendDetachRequest = function () {
|
||
|
var _this = this;
|
||
|
return this.sendRequest([DVAL_REQ, CMD_DETACH, DVAL_EOM]);
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendDumpHeapRequest = function () {
|
||
|
var _this = this;
|
||
|
|
||
|
return this.sendRequest([DVAL_REQ, CMD_DUMPHEAP, DVAL_EOM]).then(function (msg) {
|
||
|
var res = {};
|
||
|
var objs = [];
|
||
|
var i, j, n, m, o, prop;
|
||
|
|
||
|
res.type = 'heapDump';
|
||
|
res.heapObjects = objs;
|
||
|
|
||
|
for (i = 1, n = msg.length - 1; i < n; /*nop*/) {
|
||
|
o = {};
|
||
|
o.ptr = msg[i++];
|
||
|
o.type = msg[i++];
|
||
|
o.flags = msg[i++] >>> 0; /* unsigned */
|
||
|
o.refc = msg[i++];
|
||
|
|
||
|
if (o.type === DUK_HTYPE_STRING) {
|
||
|
o.blen = msg[i++];
|
||
|
o.clen = msg[i++];
|
||
|
o.hash = msg[i++] >>> 0; /* unsigned */
|
||
|
o.data = msg[i++];
|
||
|
} else if (o.type === DUK_HTYPE_BUFFER) {
|
||
|
o.len = msg[i++];
|
||
|
o.data = msg[i++];
|
||
|
} else if (o.type === DUK_HTYPE_OBJECT) {
|
||
|
o['class'] = msg[i++];
|
||
|
o.proto = msg[i++];
|
||
|
o.esize = msg[i++];
|
||
|
o.enext = msg[i++];
|
||
|
o.asize = msg[i++];
|
||
|
o.hsize = msg[i++];
|
||
|
o.props = [];
|
||
|
for (j = 0, m = o.enext; j < m; j++) {
|
||
|
prop = {};
|
||
|
prop.flags = msg[i++];
|
||
|
prop.key = msg[i++];
|
||
|
prop.accessor = (msg[i++] == 1);
|
||
|
if (prop.accessor) {
|
||
|
prop.getter = msg[i++];
|
||
|
prop.setter = msg[i++];
|
||
|
} else {
|
||
|
prop.value = msg[i++];
|
||
|
}
|
||
|
o.props.push(prop);
|
||
|
}
|
||
|
o.array = [];
|
||
|
for (j = 0, m = o.asize; j < m; j++) {
|
||
|
prop = {};
|
||
|
prop.value = msg[i++];
|
||
|
o.array.push(prop);
|
||
|
}
|
||
|
} else {
|
||
|
console.error('invalid htype: ' + o.type + ', disconnect');
|
||
|
_this.disconnectDebugger();
|
||
|
throw new Error('invalid htype');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
objs.push(o);
|
||
|
}
|
||
|
|
||
|
return res;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.sendGetBytecodeRequest = function () {
|
||
|
var _this = this;
|
||
|
|
||
|
return this.sendRequest([DVAL_REQ, CMD_GETBYTECODE, -1 /* level; could be other than -1 too */, DVAL_EOM]).then(function (msg) {
|
||
|
var idx = 1;
|
||
|
var nconst;
|
||
|
var nfunc;
|
||
|
var val;
|
||
|
var buf;
|
||
|
var i, n;
|
||
|
var consts = [];
|
||
|
var funcs = [];
|
||
|
var bcode;
|
||
|
var preformatted;
|
||
|
var ret;
|
||
|
var idxPreformattedInstructions;
|
||
|
|
||
|
//console.log(JSON.stringify(msg));
|
||
|
|
||
|
nconst = msg[idx++];
|
||
|
for (i = 0; i < nconst; i++) {
|
||
|
val = msg[idx++];
|
||
|
consts.push(val);
|
||
|
}
|
||
|
|
||
|
nfunc = msg[idx++];
|
||
|
for (i = 0; i < nfunc; i++) {
|
||
|
val = msg[idx++];
|
||
|
funcs.push(val);
|
||
|
}
|
||
|
val = msg[idx++];
|
||
|
|
||
|
// Right now bytecode is a string containing a direct dump of the
|
||
|
// bytecode in target endianness. Decode here so that the web UI
|
||
|
// doesn't need to.
|
||
|
|
||
|
buf = Buffer.alloc(val.length);
|
||
|
writeDebugStringToBuffer(val, buf, 0);
|
||
|
bcode = _this.decodeBytecodeFromBuffer(buf, consts, funcs);
|
||
|
|
||
|
preformatted = [];
|
||
|
consts.forEach(function (v, i) {
|
||
|
preformatted.push('; c' + i + ' ' + JSON.stringify(v));
|
||
|
});
|
||
|
preformatted.push('');
|
||
|
idxPreformattedInstructions = preformatted.length;
|
||
|
bcode.forEach(function (v) {
|
||
|
preformatted.push(v.str);
|
||
|
});
|
||
|
preformatted = preformatted.join('\n') + '\n';
|
||
|
|
||
|
ret = {
|
||
|
constants: consts,
|
||
|
functions: funcs,
|
||
|
bytecode: bcode,
|
||
|
preformatted: preformatted,
|
||
|
idxPreformattedInstructions: idxPreformattedInstructions
|
||
|
};
|
||
|
|
||
|
return ret;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.changeBreakpoint = function (fileName, lineNumber, mode) {
|
||
|
var _this = this;
|
||
|
|
||
|
return this.sendRequest([DVAL_REQ, CMD_LISTBREAK, DVAL_EOM]).then(function (msg) {
|
||
|
var i, n;
|
||
|
var breakpts = [];
|
||
|
var deleted = false;
|
||
|
|
||
|
// Up-to-date list of breakpoints on target
|
||
|
for (i = 1, n = msg.length - 1; i < n; i += 2) {
|
||
|
breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] });
|
||
|
}
|
||
|
|
||
|
// Delete matching breakpoints in reverse order so that indices
|
||
|
// remain valid. We do this for all operations so that duplicates
|
||
|
// are eliminated if present.
|
||
|
for (i = breakpts.length - 1; i >= 0; i--) {
|
||
|
var bp = breakpts[i];
|
||
|
if (mode === 'deleteall' || (bp.fileName === fileName && bp.lineNumber === lineNumber))
|
||
|
{
|
||
|
deleted = true;
|
||
|
_this.sendRequest([DVAL_REQ, CMD_DELBREAK, i, DVAL_EOM], function (msg)
|
||
|
{
|
||
|
// nop
|
||
|
}, function (err) {
|
||
|
// nop
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Technically we should wait for each delbreak reply but because
|
||
|
// target processes the requests in order, it doesn't matter.
|
||
|
if ((mode === 'add') || (mode === 'toggle' && !deleted)) {
|
||
|
_this.sendRequest([DVAL_REQ, CMD_ADDBREAK, fileName, lineNumber, DVAL_EOM], function (msg)
|
||
|
{
|
||
|
// nop
|
||
|
}, function (err) {
|
||
|
_this.uiMessage('debugger-info', 'Failed to add breakpoint: ' + err);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Read final, effective breakpoints from the target
|
||
|
_this.sendBreakpointListRequest();
|
||
|
hostCooperate();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.disconnectDebugger = function ()
|
||
|
{
|
||
|
console.info1('Debugger.disconnectDebugger();');
|
||
|
if (this.targetStream)
|
||
|
{
|
||
|
// We require a destroy() method from the actual target stream
|
||
|
this.targetStream.end();
|
||
|
this.targetStream = null;
|
||
|
}
|
||
|
if (this.inputParser) {
|
||
|
this.inputParser.close();
|
||
|
this.inputParser = null;
|
||
|
}
|
||
|
if (this.outputPassThroughStream) {
|
||
|
// There is no close() or destroy() for a passthrough stream, so just
|
||
|
// close the outputParser which will cancel timers etc.
|
||
|
}
|
||
|
if (this.outputParser) {
|
||
|
this.outputParser.close();
|
||
|
this.outputParser = null;
|
||
|
}
|
||
|
console.info1('disconnecting');
|
||
|
this.debugger_attached = false;
|
||
|
this.handshook = false;
|
||
|
this.reqQueue = null;
|
||
|
this.execStatus = {
|
||
|
attached: false,
|
||
|
state: 'detached',
|
||
|
fileName: '',
|
||
|
funcName: '',
|
||
|
line: 0,
|
||
|
pc: 0
|
||
|
};
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.connectDebugger = function connectDebugger()
|
||
|
{
|
||
|
var _this = this;
|
||
|
this.disconnectDebugger(); // close previous target connection
|
||
|
|
||
|
// CUSTOMTRANSPORT: to use a custom transport, change this.targetStream to
|
||
|
// use your custom transport.
|
||
|
|
||
|
console.info1('Connecting to ' + optTargetHost + ':' + optTargetPort + '...');
|
||
|
this.targetStream = require('net').createConnection(optTargetPort, optTargetHost);
|
||
|
this.targetStream.Debugger = this;
|
||
|
|
||
|
this.targetStream.on('connect', function OnDebugConnect()
|
||
|
{
|
||
|
console.info1('Debugger Transport Connection Attached...');
|
||
|
hostCooperate();
|
||
|
});
|
||
|
//this.targetStream.on('data', function (msg) { console.log('recv');});
|
||
|
|
||
|
try
|
||
|
{
|
||
|
this.inputParser = new DebugProtocolParser(
|
||
|
this.targetStream,
|
||
|
null,
|
||
|
optDumpDebugRead,
|
||
|
optDumpDebugPretty,
|
||
|
optDumpDebugPretty ? 'Recv: ' : null,
|
||
|
null,
|
||
|
null // console logging is done at a higher level to match request/response
|
||
|
);
|
||
|
}
|
||
|
catch(ee)
|
||
|
{
|
||
|
console.error(ee);
|
||
|
}
|
||
|
|
||
|
//// Use a PassThrough stream to debug dump and get stats for output messages.
|
||
|
//// Simply write outgoing data to both the targetStream and this passthrough
|
||
|
//// separately.
|
||
|
//this.outputPassThroughStream = stream.PassThrough();
|
||
|
//this.outputParser = new DebugProtocolParser(
|
||
|
// this.outputPassThroughStream,
|
||
|
// 1,
|
||
|
// optDumpDebugWrite,
|
||
|
// optDumpDebugPretty,
|
||
|
// optDumpDebugPretty ? 'Send: ' : null,
|
||
|
// null,
|
||
|
// null // console logging is done at a higher level to match request/response
|
||
|
//);
|
||
|
|
||
|
this.inputParser.on('transport-close', function ()
|
||
|
{
|
||
|
console.info1('TRANSPORT-CLOSE');
|
||
|
_this.uiMessage('debugger-info', 'Debug transport closed');
|
||
|
_this.disconnectDebugger();
|
||
|
_this.emit('exec-status-update');
|
||
|
_this.emit('detached');
|
||
|
});
|
||
|
|
||
|
this.inputParser.on('transport-error', function (err)
|
||
|
{
|
||
|
console.info1('TRANSPORT-ERROR', err);
|
||
|
_this.uiMessage('debugger-info', 'Debug transport error: ' + err);
|
||
|
_this.disconnectDebugger();
|
||
|
});
|
||
|
|
||
|
this.inputParser.on('protocol-version', function (msg) {
|
||
|
var ver = msg.protocolVersion;
|
||
|
console.info1('Debug version identification:', msg.versionIdentification);
|
||
|
_this.protocolVersion = ver;
|
||
|
_this.uiMessage('debugger-info', 'Debug version identification: ' + msg.versionIdentification);
|
||
|
if (ver !== SUPPORTED_DEBUG_PROTOCOL_VERSION) {
|
||
|
console.error('Unsupported debug protocol version (got ' + ver + ', support ' + SUPPORTED_DEBUG_PROTOCOL_VERSION + ')');
|
||
|
_this.uiMessage('debugger-info', 'Protocol version ' + ver + ' unsupported, dropping connection');
|
||
|
_this.targetStream.destroy();
|
||
|
} else {
|
||
|
_this.uiMessage('debugger-info', 'Debug protocol version: ' + ver);
|
||
|
_this.handshook = true;
|
||
|
_this.execStatus = {
|
||
|
attached: true,
|
||
|
state: 'attached',
|
||
|
fileName: '',
|
||
|
funcName: '',
|
||
|
line: 0,
|
||
|
pc: 0
|
||
|
};
|
||
|
_this.debugger_attached = true;
|
||
|
_this.emit('exec-status-update');
|
||
|
_this.emit('attached'); // inform web UI
|
||
|
|
||
|
// Fetch basic info right away
|
||
|
_this.sendBasicInfoRequest();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.inputParser.on('debug-message', function (msg)
|
||
|
{
|
||
|
_this.processDebugMessage(msg);
|
||
|
});
|
||
|
|
||
|
this.inputParser.on('stats-update', function () {
|
||
|
_this.stats.rxBytes = this.bytes;
|
||
|
_this.stats.rxDvalues = this.dvalues;
|
||
|
_this.stats.rxMessages = this.messages;
|
||
|
_this.stats.rxBytesPerSec = this.bytesPerSec;
|
||
|
_this.emit('debug-stats-update');
|
||
|
});
|
||
|
|
||
|
if (this.outputParser)
|
||
|
{
|
||
|
this.outputParser.on('stats-update', function () {
|
||
|
_this.stats.txBytes = this.bytes;
|
||
|
_this.stats.txDvalues = this.dvalues;
|
||
|
_this.stats.txMessages = this.messages;
|
||
|
_this.stats.txBytesPerSec = this.bytesPerSec;
|
||
|
_this.emit('debug-stats-update');
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.processDebugMessage = function (msg) {
|
||
|
var req;
|
||
|
var prevState, newState;
|
||
|
var err;
|
||
|
|
||
|
if (msg[0] === DVAL_REQ) {
|
||
|
// No actual requests sent by the target right now (just notifys).
|
||
|
console.error('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg));
|
||
|
} else if (msg[0] === DVAL_REP) {
|
||
|
if (this.reqQueue.length <= 0) {
|
||
|
console.error('Unsolicited reply message, dropping connection: [' + this.reqQueue.length + '] ' + prettyDebugMessage(msg));
|
||
|
this.targetStream.destroy();
|
||
|
}
|
||
|
req = this.reqQueue.shift();
|
||
|
|
||
|
if (optLogMessages) {
|
||
|
console.info1('Reply for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
|
||
|
}
|
||
|
|
||
|
if (req.resolveCb) {
|
||
|
req.resolveCb(msg);
|
||
|
} else {
|
||
|
// nop: no callback
|
||
|
}
|
||
|
} else if (msg[0] === DVAL_ERR) {
|
||
|
if (this.reqQueue.length <= 0) {
|
||
|
console.error('Unsolicited error message, dropping connection: ' + prettyDebugMessage(msg));
|
||
|
this.targetStream.destroy();
|
||
|
}
|
||
|
err = new Error(String(msg[2]) + ' (code ' + String(msg[1]) + ')');
|
||
|
err.errorCode = msg[1] || 0;
|
||
|
req = this.reqQueue.shift();
|
||
|
|
||
|
if (optLogMessages) {
|
||
|
console.info1('Error for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
|
||
|
}
|
||
|
|
||
|
console.info1(req.rejectCb, err);
|
||
|
|
||
|
if (req.rejectCb)
|
||
|
{
|
||
|
req.rejectCb(err);
|
||
|
} else
|
||
|
{
|
||
|
// nop: no callback
|
||
|
}
|
||
|
} else if (msg[0] === DVAL_NFY) {
|
||
|
if (optLogMessages) {
|
||
|
console.info1('Notify ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
|
||
|
}
|
||
|
|
||
|
if (msg[1] === CMD_STATUS)
|
||
|
{
|
||
|
prevState = this.execStatus.state;
|
||
|
newState = msg[2] === 0 ? 'running' : 'paused';
|
||
|
this.execStatus = {
|
||
|
attached: true,
|
||
|
state: newState,
|
||
|
fileName: (typeof(msg[3])=='string'?msg[3]:''),
|
||
|
funcName: (typeof (msg[4]) == 'string' ? msg[4] : ''),
|
||
|
line: msg[5],
|
||
|
pc: msg[6]
|
||
|
};
|
||
|
console.info1('MSG=>', msg);
|
||
|
for (var ii in msg)
|
||
|
{
|
||
|
console.info1('[' + ii + ']', msg[ii]);
|
||
|
}
|
||
|
if (prevState !== newState && newState === 'paused') {
|
||
|
// update run state now that we're paused
|
||
|
if(typeof(msg[3])=='string')
|
||
|
{
|
||
|
this.sendBreakpointListRequest();
|
||
|
this.sendGetLocalsRequest();
|
||
|
this.sendGetCallStackRequest();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.emit('exec-status-update');
|
||
|
} else if (msg[1] === CMD_THROW)
|
||
|
{
|
||
|
console.info1(msg);
|
||
|
this.emit('throw', { fatal: msg[2], message: (msg[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN), fileName: msg[4], lineNumber: msg[5] });
|
||
|
this.uiMessage({ type: 'throw', fatal: msg[2], message: (msg[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN), fileName: msg[4], lineNumber: msg[5] });
|
||
|
} else if (msg[1] === CMD_DETACHING) {
|
||
|
this.uiMessage({ type: 'detaching', reason: msg[2], message: 'DETACH: ' + (msg.length >= 5 ? prettyUiStringUnquoted(msg[3]) : 'detaching') });
|
||
|
} else if(msg[1] === CMD_APPNOTIFY)
|
||
|
{
|
||
|
switch(msg[2])
|
||
|
{
|
||
|
case 'MemoryAllocations':
|
||
|
console.info1('MemoryAllocation: ' + msg[3]);
|
||
|
this.emit('memory-allocation', { total: parseInt(msg[3]) });
|
||
|
break;
|
||
|
default:
|
||
|
console.info1('Unknown AppNotify: ' + msg[2]);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Ignore unknown notify messages
|
||
|
console.info1('Unknown notify, ignoring: ' + prettyDebugMessage(msg));
|
||
|
}
|
||
|
} else {
|
||
|
console.error('Invalid initial dvalue, dropping connection: ' + prettyDebugMessage(msg));
|
||
|
this.targetStream.destroy();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Debugger.prototype.run = function ()
|
||
|
{
|
||
|
var _this = this;
|
||
|
optTargetPort = transport;
|
||
|
|
||
|
this.WebServer = require('http').createServer();
|
||
|
this.WebServer.Debugger = this;
|
||
|
this.WebServer.listen({ port: webport });
|
||
|
updateWebPort(this.WebServer.address().port);
|
||
|
this.WebServer.on('request', function OnDebugWebsiteRequest(imsg, rsp)
|
||
|
{
|
||
|
console.info1('Received inbound request: ' + imsg.method + ' ' + imsg.url);
|
||
|
if (imsg.url == '/')
|
||
|
{
|
||
|
rsp.statusCode = 200;
|
||
|
rsp.statusText = 'OK';
|
||
|
rsp.setHeader('Content-Type', 'text/html');
|
||
|
|
||
|
if (require('fs').existsSync('debugger.html'))
|
||
|
{
|
||
|
rsp.fs = require('fs').createReadStream('debugger.html');
|
||
|
rsp.fs.pipe(rsp);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
rsp.end(Buffer.from(dbgHTML, 'base64'));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
rsp.statusCode = 404;
|
||
|
rsp.end();
|
||
|
}
|
||
|
});
|
||
|
this.WebServer.on('upgrade', function OnDebugWebSocket(req, soc, head)
|
||
|
{
|
||
|
console.info1('WebSocket request on: ', req.url);
|
||
|
this.Debugger.websocket = soc.upgradeWebSocket();
|
||
|
this.Debugger.websocket.Debugger = this.Debugger;
|
||
|
this.Debugger.websocket.on('end', function ()
|
||
|
{
|
||
|
this.Debugger.disconnectDebugger();
|
||
|
console.info1('web socket closed');
|
||
|
});
|
||
|
this.Debugger.websocket.on('data', function OnDebugCmd(buf)
|
||
|
{
|
||
|
var j = null;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
j = JSON.parse(buf);
|
||
|
}
|
||
|
catch(e)
|
||
|
{
|
||
|
console.error('Parse Error: ' + e);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
switch(j.cmd)
|
||
|
{
|
||
|
case 'ATTACH':
|
||
|
console.info1('Attaching Debugger');
|
||
|
this.Debugger.connectDebugger();
|
||
|
break;
|
||
|
case 'GC':
|
||
|
_debugGC(); // Debugger Heap
|
||
|
hostGC(); // Host Heap
|
||
|
console.info1('Force GC');
|
||
|
break;
|
||
|
case 'STEPOVER':
|
||
|
this.Debugger.sendStepOverRequest();
|
||
|
break;
|
||
|
case 'STEPINTO':
|
||
|
this.Debugger.sendStepIntoRequest();
|
||
|
break;
|
||
|
case 'STEPOUT':
|
||
|
this.Debugger.sendStepOutRequest();
|
||
|
break;
|
||
|
case 'CALLSTACK':
|
||
|
this.Debugger.sendGetCallStackRequest();
|
||
|
break;
|
||
|
case 'HEAP':
|
||
|
var p = this.Debugger.sendDumpHeapRequest();
|
||
|
p.Debugger = this.Debugger;
|
||
|
p.then(function onSendDumpHeap(obj)
|
||
|
{
|
||
|
console.info1('\n\n\n\n');
|
||
|
var heapObjects = obj.heapObjects;
|
||
|
for(var i in heapObjects)
|
||
|
{
|
||
|
console.info1(heapObjects[i].ptr, heapObjects[i].type);
|
||
|
}
|
||
|
//console.info1('this', this);
|
||
|
//process.stdout.write('THEN\n');
|
||
|
//process.stdout.write(JSON.stringify(this) + '\n');
|
||
|
//process.stdout.write(JSON.stringify(obj) + '\n');
|
||
|
}, function OnError(err) { console.info1('ERROR=>', err); });
|
||
|
break;
|
||
|
case 'PAUSE':
|
||
|
this.Debugger.sendPauseRequest();
|
||
|
break;
|
||
|
case 'RESUME':
|
||
|
this.Debugger.sendResumeRequest();
|
||
|
break;
|
||
|
case 'DETACH':
|
||
|
console.info1('Detach Debugger');
|
||
|
this.Debugger.disconnectDebugger();
|
||
|
break;
|
||
|
case 'BREAKPOINT':
|
||
|
console.info1(j);
|
||
|
if (j.file == '')
|
||
|
{
|
||
|
if ((j.file = this.Debugger.execStatus.fileName) == '')
|
||
|
{
|
||
|
j.file = _scriptPath;
|
||
|
}
|
||
|
console.info1('using filePath: ' + j.file);
|
||
|
}
|
||
|
|
||
|
console.info1('ChangeBreakpoint => ' + j.file + ':' + j.line + ' (' + j.mode + ')');
|
||
|
this.Debugger.changeBreakpoint(j.file, j.line, j.mode);
|
||
|
break;
|
||
|
case 'QUERY':
|
||
|
console.info1(j);
|
||
|
var p = this.Debugger.sendEvalRequest('JSON.stringify(' + j.var + ')', j.level);
|
||
|
p.var = j.var;
|
||
|
p.then(function (msg)
|
||
|
{
|
||
|
_this.websocket.write({ cmd: 'QUERY', var: this.promise.var, val: msg.value });
|
||
|
});
|
||
|
break;
|
||
|
case 'EVAL':
|
||
|
console.info1(j);
|
||
|
var p = this.Debugger.sendEvalRequest(j.eval, j.level);
|
||
|
p.eval = j.eval;
|
||
|
p.then(function (msg)
|
||
|
{
|
||
|
_this.websocket.write({ cmd: 'EVAL', eval: this.promise.eval, val: msg.value });
|
||
|
});
|
||
|
break;
|
||
|
break;
|
||
|
case 'LOCALS':
|
||
|
console.info1(j);
|
||
|
this.Debugger.sendGetLocalsRequest(j.level).then(function (locals)
|
||
|
{
|
||
|
var vals = [];
|
||
|
for (var v in locals)
|
||
|
{
|
||
|
console.info1('Pushing: [' + locals[v].key + '] = ' + locals[v].type);
|
||
|
vals.push({ key: locals[v].key, type: locals[v].type });
|
||
|
}
|
||
|
_this.websocket.write({ cmd: 'LOCALS', val: vals });
|
||
|
});
|
||
|
|
||
|
|
||
|
break
|
||
|
case 'SOURCE':
|
||
|
console.info1('SOURCE', j);
|
||
|
if (j.name != '')
|
||
|
{
|
||
|
if (j.name.toLowerCase().endsWith('.js') || j.name == '<<NATIVE>>')
|
||
|
{
|
||
|
console.info1(JSON.stringify({ cmd: 'SOURCE', name: j.name, source: _scriptTokens.join('\n') }));
|
||
|
_this.websocket.write({ cmd: 'SOURCE', name: j.name, source: _scriptTokens.join('\n') });
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var p = _this.sendEvalRequest("getJSModule('" + j.name + "');");
|
||
|
p.websocket = _this.websocket;
|
||
|
p.name = j.name;
|
||
|
|
||
|
p.then(function (result) {
|
||
|
this.promise.websocket.write({ cmd: 'SOURCE', name: this.promise.name, source: result.value });
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
hostCooperate();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('memory-allocation', function (mem)
|
||
|
{
|
||
|
var cmd = { cmd: 'MEMORY', total: mem.total };
|
||
|
this.websocket.write(cmd);
|
||
|
});
|
||
|
this.Debugger.on('detached', function ()
|
||
|
{
|
||
|
detachCleanup();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('debug-stats-update', function ()
|
||
|
{
|
||
|
// _this.debugStatsLimiter.trigger();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('ui-message-update', function ()
|
||
|
{
|
||
|
// Explicit rate limiter because this is a source of a lot of traffic.
|
||
|
//_this.uiMessageLimiter.trigger();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('basic-info-update', function ()
|
||
|
{
|
||
|
// _this.emitBasicInfo();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('breakpoints-update', function (bpts)
|
||
|
{
|
||
|
console.info1('breakpoints updated');
|
||
|
var cmd = { cmd: 'BREAKPOINT', list: bpts };
|
||
|
this.websocket.write(cmd);
|
||
|
//_this.emitBreakpoints();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('exec-status-update', function ()
|
||
|
{
|
||
|
if (!this.previousState || this.previousState != this.execStatus.state)
|
||
|
{
|
||
|
this.previousState = this.execStatus.state;
|
||
|
console.info1('new state', this.execStatus.state);
|
||
|
if (this.execStatus.state == 'attached') { console.info1('forcing cooperate'); hostCooperate(); }
|
||
|
if (this.execStatus.attached)
|
||
|
{
|
||
|
this.websocket.pausedNative = false;
|
||
|
switch(this.execStatus.state)
|
||
|
{
|
||
|
case 'paused':
|
||
|
console.info1('PAUSED', this.execStatus);
|
||
|
var src = _scriptPath == this.execStatus.fileName ? _scriptTokens[this.execStatus.line - 1] : null;
|
||
|
if (src == null)
|
||
|
{
|
||
|
if (this.execStatus.fileName != '')
|
||
|
{
|
||
|
var p = this.sendEvalRequest("getJSModule('" + this.execStatus.fileName + "');");
|
||
|
p.websocket = this.websocket;
|
||
|
p.execStatus = this.execStatus;
|
||
|
|
||
|
p.then(function (result) {
|
||
|
var tk = result.value.split('\n');
|
||
|
this.promise.websocket.write({ cmd: 'PAUSE', native: false, file: this.promise.execStatus.fileName, line: this.promise.execStatus.line, source: tk[this.promise.execStatus.line - 1] });
|
||
|
});
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
this.websocket.pausedNative = true;
|
||
|
this.websocket.write({ cmd: 'PAUSE', native: true, file: this.execStatus.fileName, line: this.execStatus.line, source: '(Native)' });
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
this.websocket.write({ cmd: 'PAUSE', native: false, file: this.execStatus.fileName, line: this.execStatus.line, source: src });
|
||
|
}
|
||
|
break;
|
||
|
case 'running':
|
||
|
this.websocket.write({ cmd: 'RUNNING', file: _scriptPath });
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else if (this.execStatus.state == 'detached')
|
||
|
{
|
||
|
this.websocket.write({ cmd: 'DETACH'});
|
||
|
}
|
||
|
}
|
||
|
// Explicit rate limiter because this is a source of a lot of traffic.
|
||
|
//_this.execStatusLimiter.trigger();
|
||
|
});
|
||
|
this.Debugger.on('throw', function OnThrown(err)
|
||
|
{
|
||
|
var source = err.fileName == _scriptPath ? (_scriptTokens[err.lineNumber - 1]) : null;
|
||
|
if (source == null) {
|
||
|
var p = this.sendEvalRequest("getJSModule('" + err.fileName + "');");
|
||
|
p.ws = this.websocket;
|
||
|
p.err = err;
|
||
|
|
||
|
p.then(function (result)
|
||
|
{
|
||
|
var tk = result.value.split('\n');
|
||
|
this.promise.ws.write({ cmd: 'THROW', line: this.promise.err.lineNumber, msg: this.promise.err.message, file: this.promise.err.fileName, source: tk[this.promise.err.lineNumber - 1] });
|
||
|
});
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
this.websocket.write({ cmd: 'THROW', line: err.lineNumber, msg: err.message, file: err.fileName, source: source });
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('locals-update', function ()
|
||
|
{
|
||
|
//_this.emitLocals();
|
||
|
});
|
||
|
|
||
|
this.Debugger.on('callstack-update', function ()
|
||
|
{
|
||
|
this.websocket.write({ cmd: 'CALLSTACK', callstack: this.callstack });
|
||
|
});
|
||
|
|
||
|
|
||
|
|
||
|
});
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
//// Initial debugger connection
|
||
|
|
||
|
//this.connectDebugger();
|
||
|
|
||
|
//// Poll various state items when running
|
||
|
|
||
|
//var sendRound = 0;
|
||
|
//var statusPending = false;
|
||
|
//var bplistPending = false;
|
||
|
//var localsPending = false;
|
||
|
//var callStackPending = false;
|
||
|
|
||
|
//setInterval(function () {
|
||
|
// if (_this.execStatus.state !== 'running') {
|
||
|
// return;
|
||
|
// }
|
||
|
|
||
|
// // Could also check for an empty request queue, but that's probably
|
||
|
// // too strict?
|
||
|
|
||
|
// // Pending flags are used to avoid requesting the same thing twice
|
||
|
// // while a previous request is pending. The flag-based approach is
|
||
|
// // quite awkward. Rework to use promises.
|
||
|
|
||
|
// switch (sendRound) {
|
||
|
// case 0:
|
||
|
// if (!statusPending) {
|
||
|
// statusPending = true;
|
||
|
// _this.sendStatusRequest().finally(function () { statusPending = false; });
|
||
|
// }
|
||
|
// break;
|
||
|
// case 1:
|
||
|
// if (!bplistPending) {
|
||
|
// bplistPending = true;
|
||
|
// _this.sendBreakpointListRequest().finally(function () { bplistPending = false; });
|
||
|
// }
|
||
|
// break;
|
||
|
// case 2:
|
||
|
// if (!localsPending) {
|
||
|
// localsPending = true;
|
||
|
// _this.sendGetLocalsRequest().finally(function () { localsPending = false; });
|
||
|
// }
|
||
|
// break;
|
||
|
// case 3:
|
||
|
// if (!callStackPending) {
|
||
|
// callStackPending = true;
|
||
|
// _this.sendGetCallStackRequest().finally(function () { callStackPending = false; });
|
||
|
// }
|
||
|
// break;
|
||
|
// }
|
||
|
// sendRound = (sendRound + 1) % 4;
|
||
|
//}, 500);
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
/*
|
||
|
* Miscellaneous helpers
|
||
|
*/
|
||
|
|
||
|
var nybbles = '0123456789abcdef';
|
||
|
|
||
|
/* Convert a buffer into a string using Unicode codepoints U+0000...U+00FF.
|
||
|
* This is the NodeJS 'binary' encoding, but since it's being deprecated,
|
||
|
* reimplement it here. We need to avoid parsing strings as e.g. UTF-8:
|
||
|
* although Duktape strings are usually UTF-8/CESU-8 that's not always the
|
||
|
* case, e.g. for internal strings. Buffer values are also represented as
|
||
|
* strings in the debug protocol, so we must deal accurately with arbitrary
|
||
|
* byte arrays.
|
||
|
*/
|
||
|
function bufferToDebugString(buf) {
|
||
|
var cp = [];
|
||
|
var i, n;
|
||
|
|
||
|
/*
|
||
|
// This fails with "RangeError: Maximum call stack size exceeded" for some
|
||
|
// reason, so use a much slower variant.
|
||
|
|
||
|
for (i = 0, n = buf.length; i < n; i++) {
|
||
|
cp[i] = buf[i];
|
||
|
}
|
||
|
|
||
|
return String.fromCharCode.apply(String, cp);
|
||
|
*/
|
||
|
|
||
|
for (i = 0, n = buf.length; i < n; i++) {
|
||
|
cp[i] = String.fromCharCode(buf[i]);
|
||
|
}
|
||
|
|
||
|
return cp.join('');
|
||
|
}
|
||
|
|
||
|
/* Write a string into a buffer interpreting codepoints U+0000...U+00FF
|
||
|
* as bytes. Drop higher bits.
|
||
|
*/
|
||
|
function writeDebugStringToBuffer(str, buf, off) {
|
||
|
var i, n;
|
||
|
|
||
|
for (i = 0, n = str.length; i < n; i++) {
|
||
|
buf[off + i] = str.charCodeAt(i) & 0xff; // truncate higher bits
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Encode an ordinary Unicode string into a dvalue compatible format, i.e.
|
||
|
* into a byte array represented as codepoints U+0000...U+00FF. Concretely,
|
||
|
* encode with UTF-8 and then represent the bytes with U+0000...U+00FF.
|
||
|
*/
|
||
|
function stringToDebugString(str) {
|
||
|
return utf8.encode(str);
|
||
|
}
|
||
|
|
||
|
/* Pretty print a dvalue. Useful for dumping etc. */
|
||
|
function prettyDebugValue(x) {
|
||
|
if (typeof x === 'object' && x !== null) {
|
||
|
if (x.type === 'eom') {
|
||
|
return 'EOM';
|
||
|
} else if (x.type === 'req') {
|
||
|
return 'REQ';
|
||
|
} else if (x.type === 'rep') {
|
||
|
return 'REP';
|
||
|
} else if (x.type === 'err') {
|
||
|
return 'ERR';
|
||
|
} else if (x.type === 'nfy') {
|
||
|
return 'NFY';
|
||
|
}
|
||
|
}
|
||
|
return JSON.stringify(x);
|
||
|
}
|
||
|
|
||
|
/* Pretty print a number for UI usage. Types and values should be easy to
|
||
|
* read and typing should be obvious. For numbers, support Infinity, NaN,
|
||
|
* and signed zeroes properly.
|
||
|
*/
|
||
|
function prettyUiNumber(x) {
|
||
|
if (x === 1 / 0) { return 'Infinity'; }
|
||
|
if (x === -1 / 0) { return '-Infinity'; }
|
||
|
if (isNaN(x)) { return 'NaN'; }
|
||
|
if (x === 0 && 1 / x > 0) { return '0'; }
|
||
|
if (x === 0 && 1 / x < 0) { return '-0'; }
|
||
|
return x.toString();
|
||
|
}
|
||
|
|
||
|
/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
|
||
|
* for UI usage. Try UTF-8 decoding to get a nice Unicode string (JSON
|
||
|
* encoded) but if that fails, ensure that bytes are encoded transparently.
|
||
|
* The result is a quoted string with a special quote marker for a "raw"
|
||
|
* string when UTF-8 decoding fails. Very long strings are optionally
|
||
|
* clipped.
|
||
|
*/
|
||
|
function prettyUiString(x, cliplen) {
|
||
|
var ret;
|
||
|
|
||
|
if (typeof x !== 'string') {
|
||
|
throw new Error('invalid input to prettyUiString: ' + typeof x);
|
||
|
}
|
||
|
try {
|
||
|
// Here utf8.decode() is better than decoding using NodeJS buffer
|
||
|
// operations because we want strict UTF-8 interpretation.
|
||
|
ret = JSON.stringify(utf8.decode(x));
|
||
|
} catch (e) {
|
||
|
// When we fall back to representing bytes, indicate that the string
|
||
|
// is "raw" with a 'r"' prefix (a somewhat arbitrary convention).
|
||
|
// U+0022 = ", U+0027 = '
|
||
|
ret = 'r"' + x.replace(/[\u0022\u0027\u0000-\u001f\u0080-\uffff]/g, function (match) {
|
||
|
var cp = match.charCodeAt(0);
|
||
|
return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f];
|
||
|
}) + '"';
|
||
|
}
|
||
|
|
||
|
if (cliplen && ret.length > cliplen) {
|
||
|
ret = ret.substring(0, cliplen) + '...'; // trailing '"' intentionally missing
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
|
||
|
* for UI usage without quotes.
|
||
|
*/
|
||
|
function prettyUiStringUnquoted(x, cliplen) {
|
||
|
var ret;
|
||
|
|
||
|
if (typeof x !== 'string') {
|
||
|
throw new Error('invalid input to prettyUiStringUnquoted: ' + typeof x);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
// Here utf8.decode() is better than decoding using NodeJS buffer
|
||
|
// operations because we want strict UTF-8 interpretation.
|
||
|
|
||
|
// XXX: unprintable characters etc? In some UI cases we'd want to
|
||
|
// e.g. escape newlines and in others not.
|
||
|
ret = utf8.decode(x);
|
||
|
} catch (e) {
|
||
|
// For the unquoted version we don't need to escape single or double
|
||
|
// quotes.
|
||
|
ret = x.replace(/[\u0000-\u001f\u0080-\uffff]/g, function (match) {
|
||
|
var cp = match.charCodeAt(0);
|
||
|
return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (cliplen && ret.length > cliplen) {
|
||
|
ret = ret.substring(0, cliplen) + '...';
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/* Pretty print a dvalue for UI usage. Everything comes out as a ready-to-use
|
||
|
* string.
|
||
|
*
|
||
|
* XXX: Currently the debug client formats all values for UI use. A better
|
||
|
* solution would be to pass values in typed form and let the UI format them,
|
||
|
* so that styling etc. could take typing into account.
|
||
|
*/
|
||
|
function prettyUiDebugValue(x, cliplen) {
|
||
|
if (typeof x === 'object' && x !== null) {
|
||
|
// Note: typeof null === 'object', so null special case explicitly
|
||
|
if (x.type === 'eom') {
|
||
|
return 'EOM';
|
||
|
} else if (x.type === 'req') {
|
||
|
return 'REQ';
|
||
|
} else if (x.type === 'rep') {
|
||
|
return 'REP';
|
||
|
} else if (x.type === 'err') {
|
||
|
return 'ERR';
|
||
|
} else if (x.type === 'nfy') {
|
||
|
return 'NFY';
|
||
|
} else if (x.type === 'unused') {
|
||
|
return 'unused';
|
||
|
} else if (x.type === 'undefined') {
|
||
|
return 'undefined';
|
||
|
} else if (x.type === 'buffer') {
|
||
|
return '|' + x.data + '|';
|
||
|
} else if (x.type === 'object') {
|
||
|
console.info1('OBJECT => ', JSON.stringify(x));
|
||
|
// return '[object ' + (dukClassNames[x.class] || ('class ' + x.class)) + ' ' + x.pointer + ']';
|
||
|
return '[object ' + ('class ' + x.class) + ' ' + x.pointer + ']';
|
||
|
} else if (x.type === 'pointer') {
|
||
|
return '<pointer ' + x.pointer + '>';
|
||
|
} else if (x.type === 'lightfunc') {
|
||
|
return '<lightfunc 0x' + x.flags.toString(16) + ' ' + x.pointer + '>';
|
||
|
} else if (x.type === 'number') {
|
||
|
// duk_tval number, any IEEE double
|
||
|
var tmp = new Buffer(x.data, 'hex'); // decode into hex
|
||
|
var val = tmp.readDoubleBE(0); // big endian ieee double
|
||
|
return prettyUiNumber(val);
|
||
|
}
|
||
|
} else if (x === null) {
|
||
|
return 'null';
|
||
|
} else if (typeof x === 'boolean') {
|
||
|
return x ? 'true' : 'false';
|
||
|
} else if (typeof x === 'string') {
|
||
|
return prettyUiString(x, cliplen);
|
||
|
} else if (typeof x === 'number') {
|
||
|
// Debug protocol integer
|
||
|
return prettyUiNumber(x);
|
||
|
}
|
||
|
|
||
|
// We shouldn't come here, but if we do, JSON is a reasonable default.
|
||
|
return JSON.stringify(x);
|
||
|
}
|
||
|
|
||
|
/* Pretty print a debugger message given as an array of parsed dvalues.
|
||
|
* Result should be a pure ASCII one-liner.
|
||
|
*/
|
||
|
function prettyDebugMessage(msg) {
|
||
|
return msg.map(prettyDebugValue).join(' ');
|
||
|
}
|
||
|
|
||
|
/* Pretty print a debugger command. */
|
||
|
function prettyDebugCommand(cmd)
|
||
|
{
|
||
|
return debugCommandNames[cmd] || String(cmd);
|
||
|
}
|
||
|
|
||
|
/* Decode and normalize source file contents: UTF-8, tabs to 8,
|
||
|
* CR LF to LF.
|
||
|
*/
|
||
|
function decodeAndNormalizeSource(data) {
|
||
|
var tmp;
|
||
|
var lines, line, repl;
|
||
|
var i, n;
|
||
|
var j, m;
|
||
|
|
||
|
try {
|
||
|
tmp = data.toString('utf8');
|
||
|
} catch (e) {
|
||
|
console.error('Failed to UTF-8 decode source file, ignoring: ' + e);
|
||
|
tmp = String(data);
|
||
|
}
|
||
|
|
||
|
lines = tmp.split(/\r?\n/);
|
||
|
for (i = 0, n = lines.length; i < n; i++) {
|
||
|
line = lines[i];
|
||
|
if (/\t/.test(line)) {
|
||
|
repl = '';
|
||
|
for (j = 0, m = line.length; j < m; j++) {
|
||
|
if (line.charAt(j) === '\t') {
|
||
|
repl += ' ';
|
||
|
while ((repl.length % 8) != 0) {
|
||
|
repl += ' ';
|
||
|
}
|
||
|
} else {
|
||
|
repl += line.charAt(j);
|
||
|
}
|
||
|
}
|
||
|
lines[i] = repl;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// XXX: normalize last newline (i.e. force a newline if contents don't
|
||
|
// end with a newline)?
|
||
|
|
||
|
return lines.join('\n');
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
module.exports = Debugger;
|