2 * JSON debug proxy written in DukLuv
4 * This single file JSON debug proxy implementation is an alternative to the
5 * Node.js-based proxy in duk_debug.js. DukLuv is a much smaller dependency
6 * than Node.js so embedding DukLuv in a debug client is easier.
11 // XXX: Code assumes uv.write() will write fully. This is not necessarily
12 // true; should add support for partial writes (or at least failing when
13 // a partial write occurs).
15 var log
= new Duktape
.Logger('Proxy'); // default logger
16 //log.l = 0; // enable debug and trace logging
22 var serverHost
= '0.0.0.0';
23 var serverPort
= 9093;
24 var targetHost
= '127.0.0.1';
25 var targetPort
= 9091;
26 var singleConnection
= false;
27 var readableNumberValue
= false;
28 var lenientJsonParse
= false;
30 var metadataFile
= null;
32 var TORTURE
= false; // for manual testing of binary/json parsing robustness
35 * Detect missing 'var' declarations
38 // Prevent new bindings on global object. This detects missing 'var'
39 // declarations, e.g. "x = 123;" in a function without declaring it.
40 var global
= new Function('return this;')();
41 log
.debug('Preventing extensions on global object');
42 log
.debug('Global is extensible:', Object
.isExtensible(global
));
43 Object
.preventExtensions(global
);
44 log
.debug('Global is extensible:', Object
.isExtensible(global
));
50 function plainBufferCopy(typedarray
) {
51 // This is still pretty awkward in Duktape 1.4.x.
52 // Argument may be a "slice" and we want a copy of the slice
53 // (not the full underlying buffer).
55 var u8
= new Uint8Array(typedarray
.length
);
56 u8
.set(typedarray
); // make a copy, ensuring there's no slice offset
57 return Duktape
.Buffer(u8
); // get underlying plain buffer
60 function isObject(x
) {
61 // Note that typeof null === 'object'.
62 return (typeof x
=== 'object' && x
!== null);
65 function readFully(filename
, cb
) {
66 uv
.fs_open(metadataFile
, 'r', 0, function (handle
, err
) {
68 var data
= new Uint8Array(256);
74 function readCb(buf
, err
) {
78 log
.debug('Read callback:', buf
.length
, err
);
83 if (buf
.length
== 0) {
85 res
= new Uint8Array(dataOff
);
86 res
.set(data
.subarray(0, dataOff
));
87 res
= Duktape
.Buffer(res
); // plain buffer
88 log
.debug('Read', res
.length
, 'bytes from', filename
);
91 while (data
.length
- dataOff
< buf
.length
) {
92 log
.debug('Resize file read buffer:', data
.length
, '->', data
.length
* 2);
93 newData
= new Uint8Array(data
.length
* 2);
97 data
.set(new Uint8Array(buf
), dataOff
);
98 dataOff
+= buf
.length
;
99 fileOff
+= buf
.length
;
100 uv
.fs_read(handle
, 4096, fileOff
, readCb
);
102 uv
.fs_read(handle
, 4096, fileOff
, readCb
);
109 * Accepts an incoming JSON proxy client and connects to a debug target,
110 * tying the two connections together. Supports both a single connection
111 * and a persistent mode.
114 function JsonProxyServer(host
, port
) {
115 this.name
= 'JsonProxyServer';
116 this.handle
= uv
.new_tcp();
117 uv
.tcp_bind(this.handle
, host
, port
);
118 uv
.listen(this.handle
, 128, this.onConnection
.bind(this));
121 JsonProxyServer
.prototype.onConnection
= function onConnection(err
) {
123 log
.error('JSON proxy onConnection error:', err
);
126 log
.info('JSON proxy client connected'); // XXX: it'd be nice to log remote peer host:port
128 var jsonSock
= new JsonConnHandler(this);
129 var targSock
= new TargetConnHandler(this);
130 jsonSock
.targetHandler
= targSock
;
131 targSock
.jsonHandler
= jsonSock
;
132 uv
.accept(this.handle
, jsonSock
.handle
);
134 log
.info('Connecting to debug target at', targetHost
+ ':' + targetPort
);
135 jsonSock
.writeJson({ notify
: '_TargetConnecting', args
: [ targetHost
, targetPort
] });
136 uv
.tcp_connect(targSock
.handle
, targetHost
, targetPort
, targSock
.onConnect
.bind(targSock
));
138 if (singleConnection
) {
139 log
.info('Single connection mode, stop listening for more connections');
140 uv
.shutdown(this.handle
);
141 uv
.read_stop(this.handle
); // unnecessary but just in case
142 uv
.close(this.handle
);
147 JsonProxyServer
.prototype.onProxyClientDisconnected
= function onProxyClientDisconnected() {
148 // When this is invoked the proxy connection and the target connection
149 // have both been closed.
150 if (singleConnection
) {
151 log
.info('Proxy connection finished (single connection mode: we should be exiting now)');
153 log
.info('Proxy connection finished (persistent mode: wait for more connections)');
158 * JSON connection handler
161 function JsonConnHandler(server
) {
164 this.name
= 'JsonConnHandler';
165 this.server
= server
;
166 this.handle
= uv
.new_tcp();
167 this.incoming
= new Uint8Array(4096);
168 this.incomingOffset
= 0;
169 this.targetHandler
= null;
171 this.commandNumberLookup
= {};
172 if (metadata
&& metadata
.target_commands
) {
173 for (i
= 0, n
= metadata
.target_commands
.length
; i
< n
; i
++) {
174 this.commandNumberLookup
[metadata
.target_commands
[i
]] = i
;
179 JsonConnHandler
.prototype.finish
= function finish(msg
) {
183 log
.info('JsonConnHandler already disconnected, ignore finish()');
186 log
.info('JsonConnHandler finished:', msg
);
188 args
= msg
? [ msg
] : void 0;
189 this.writeJson({ notify
: '_Disconnecting', args
: args
});
191 log
.info('Failed to write _Disconnecting notify, ignoring:', e
);
193 uv
.shutdown(this.handle
);
194 uv
.read_stop(this.handle
);
195 uv
.close(this.handle
);
198 this.targetHandler
.finish(msg
); // disconnect target too (if not already disconnected)
200 this.server
.onProxyClientDisconnected();
203 JsonConnHandler
.prototype.onRead
= function onRead(err
, data
) {
209 log
.trace('Received data from JSON socket, err:', err
, 'data length:', data
? data
.length
: 'null');
212 errmsg
= 'Error reading data from JSON debug client: ' + err
;
217 // Feed the data one byte at a time when torture testing.
218 if (TORTURE
&& data
.length
> 1) {
219 for (var i
= 0; i
< data
.length
; i
++) {
220 tmpBuf
= Duktape
.Buffer(1);
222 this.onRead(null, tmpBuf
);
227 // Receive data into 'incoming', resizing as necessary.
228 while (data
.length
> this.incoming
.length
- this.incomingOffset
) {
229 newIncoming
= new Uint8Array(this.incoming
.length
* 1.3 + 16);
230 newIncoming
.set(this.incoming
);
231 this.incoming
= newIncoming
;
232 log
.debug('Resize incoming JSON buffer to ' + this.incoming
.length
);
234 this.incoming
.set(new Uint8Array(data
), this.incomingOffset
);
235 this.incomingOffset
+= data
.length
;
237 // Trial parse JSON message(s).
239 msg
= this.trialParseJsonMessage();
244 this.dispatchJsonMessage(msg
);
246 errmsg
= 'JSON message dispatch failed: ' + e
;
247 this.writeJson({ notify
: '_Error', args
: [ errmsg
] });
248 if (lenientJsonParse
) {
249 log
.warn('JSON message dispatch failed (lenient mode, ignoring):', e
);
251 log
.warn('JSON message dispatch failed (dropping connection):', e
);
257 this.finish('JSON proxy client disconnected');
261 JsonConnHandler
.prototype.writeJson
= function writeJson(msg
) {
262 log
.info('PROXY --> CLIENT:', JSON
.stringify(msg
));
264 uv
.write(this.handle
, JSON
.stringify(msg
) + '\n');
268 JsonConnHandler
.prototype.handleDebugMessage
= function handleDebugMessage(dvalues
) {
273 if (dvalues
.length
<= 0) {
274 throw new Error('invalid dvalues list: length <= 0');
276 var x
= dvalues
[idx
++];
278 throw new Error('invalid initial dvalue: ' + Duktape
.enc('jx', dvalues
));
280 if (x
.type
=== 'req') {
281 cmd
= dvalues
[idx
++];
282 if (typeof cmd
!== 'number') {
283 throw new Error('invalid command: ' + Duktape
.enc('jx', cmd
));
285 msg
.request
= this.determineCommandName(cmd
) || true;
287 } else if (x
.type
=== 'rep') {
289 } else if (x
.type
=== 'err') {
291 } else if (x
.type
=== 'nfy') {
292 cmd
= dvalues
[idx
++];
293 if (typeof cmd
!== 'number') {
294 throw new Error('invalid command: ' + Duktape
.enc('jx', cmd
));
296 msg
.notify
= this.determineCommandName(cmd
) || true;
299 throw new Error('invalid initial dvalue: ' + Duktape
.enc('jx', dvalues
));
302 for (; idx
< dvalues
.length
- 1; idx
++) {
306 msg
.args
.push(dvalues
[idx
]);
309 if (!isObject(dvalues
[idx
]) || dvalues
[idx
].type
!== 'eom') {
310 throw new Error('invalid final dvalue: ' + Duktape
.enc('jx', dvalues
));
316 JsonConnHandler
.prototype.determineCommandName
= function determineCommandName(cmd
) {
317 if (!(metadata
&& metadata
.client_commands
)) {
320 return metadata
.client_commands
[cmd
];
323 JsonConnHandler
.prototype.trialParseJsonMessage
= function trialParseJsonMessage() {
324 var buf
= this.incoming
;
325 var avail
= this.incomingOffset
;
327 var msg
, str
, errmsg
;
329 for (i
= 0; i
< avail
; i
++) {
330 if (buf
[i
] == 0x0a) {
331 str
= String(plainBufferCopy(buf
.subarray(0, i
)));
334 msg
= Duktape
.dec('jx', str
);
336 msg
= JSON
.parse(str
);
339 // In lenient mode if JSON parse fails just send back an _Error
340 // and ignore the line (useful for initial development).
342 // In non-lenient mode drop the connection here; if the failed line
343 // was a request the client is expecting a reply/error message back
344 // (otherwise it may go out of sync) but we can't send a synthetic
345 // one (as we can't parse the request).
346 errmsg
= 'JSON parse failed for: ' + JSON
.stringify(str
) + ': ' + e
;
347 this.writeJson({ notify
: '_Error', args
: [ errmsg
] });
348 if (lenientJsonParse
) {
349 log
.warn('JSON parse failed (lenient mode, ignoring):', e
);
351 log
.warn('JSON parse failed (dropping connection):', e
);
356 this.incoming
.set(this.incoming
.subarray(i
+ 1));
357 this.incomingOffset
-= i
+ 1;
363 JsonConnHandler
.prototype.dispatchJsonMessage
= function dispatchJsonMessage(msg
) {
368 log
.info('PROXY <-- CLIENT:', JSON
.stringify(msg
));
370 // Parse message type, determine initial marker for binary message.
372 cmd
= this.determineCommandNumber(msg
.request
, msg
.command
);
373 dvalues
.push(new Uint8Array([ 0x01 ]));
374 dvalues
.push(this.encodeJsonDvalue(cmd
));
375 } else if (msg
.reply
) {
376 dvalues
.push(new Uint8Array([ 0x02 ]));
377 } else if (msg
.notify
) {
378 cmd
= this.determineCommandNumber(msg
.notify
, msg
.command
);
379 dvalues
.push(new Uint8Array([ 0x04 ]));
380 dvalues
.push(this.encodeJsonDvalue(cmd
));
381 } else if (msg
.error
) {
382 dvalues
.push(new Uint8Array([ 0x03 ]));
384 throw new Error('invalid input JSON message: ' + JSON
.stringify(msg
));
387 // Encode arguments into dvalues.
388 for (i
= 0, n
= (msg
.args
? msg
.args
.length
: 0); i
< n
; i
++) {
389 dvalues
.push(this.encodeJsonDvalue(msg
.args
[i
]));
392 // Add an EOM, and write out the dvalues to the debug target.
393 dvalues
.push(new Uint8Array([ 0x00 ]));
394 for (i
= 0, n
= dvalues
.length
; i
< n
; i
++) {
395 this.targetHandler
.writeBinary(dvalues
[i
]);
399 JsonConnHandler
.prototype.determineCommandNumber
= function determineCommandNumber(name
, val
) {
402 if (typeof name
=== 'string') {
403 res
= this.commandNumberLookup
[name
];
405 log
.info('Unknown command name: ' + name
+ ', command number: ' + val
);
407 } else if (typeof name
=== 'number') {
409 } else if (name
!== true) {
410 throw new Error('invalid command name (must be string, number, or "true"): ' + name
);
412 if (typeof res
=== 'undefined' && typeof val
=== 'undefined') {
413 throw new Error('cannot determine command number from name: ' + name
);
415 if (typeof val
!== 'number' && typeof val
!== 'undefined') {
416 throw new Error('invalid command number: ' + val
);
422 JsonConnHandler
.prototype.writeDebugStringToBuffer
= function writeDebugStringToBuffer(v
, buf
, off
) {
425 for (i
= 0, n
= v
.length
; i
< n
; i
++) {
426 buf
[off
+ i
] = v
.charCodeAt(i
) & 0xff; // truncate higher bits
430 JsonConnHandler
.prototype.encodeJsonDvalue
= function encodeJsonDvalue(v
) {
431 var buf
, dec
, len
, dv
;
434 if (v
.type
=== 'eom') {
435 return new Uint8Array([ 0x00 ]);
436 } else if (v
.type
=== 'req') {
437 return new Uint8Array([ 0x01 ]);
438 } else if (v
.type
=== 'rep') {
439 return new Uint8Array([ 0x02 ]);
440 } else if (v
.type
=== 'err') {
441 return new Uint8Array([ 0x03 ]);
442 } else if (v
.type
=== 'nfy') {
443 return new Uint8Array([ 0x04 ]);
444 } else if (v
.type
=== 'unused') {
445 return new Uint8Array([ 0x15 ]);
446 } else if (v
.type
=== 'undefined') {
447 return new Uint8Array([ 0x16 ]);
448 } else if (v
.type
=== 'number') {
449 dec
= Duktape
.dec('hex', v
.data
);
452 throw new TypeError('value cannot be converted to dvalue: ' + JSON
.stringify(v
));
454 buf
= new Uint8Array(1 + len
);
456 buf
.set(new Uint8Array(dec
), 1);
458 } else if (v
.type
=== 'buffer') {
459 dec
= Duktape
.dec('hex', v
.data
);
462 buf
= new Uint8Array(3 + len
);
464 buf
[1] = (len
>> 8) & 0xff;
465 buf
[2] = (len
>> 0) & 0xff;
466 buf
.set(new Uint8Arrau(dec
), 3);
469 buf
= new Uint8Array(5 + len
);
471 buf
[1] = (len
>> 24) & 0xff;
472 buf
[2] = (len
>> 16) & 0xff;
473 buf
[3] = (len
>> 8) & 0xff;
474 buf
[4] = (len
>> 0) & 0xff;
475 buf
.set(new Uint8Array(dec
), 5);
478 } else if (v
.type
=== 'object') {
479 dec
= Duktape
.dec('hex', v
.pointer
);
481 buf
= new Uint8Array(3 + len
);
485 buf
.set(new Uint8Array(dec
), 3);
487 } else if (v
.type
=== 'pointer') {
488 dec
= Duktape
.dec('hex', v
.pointer
);
490 buf
= new Uint8Array(2 + len
);
493 buf
.set(new Uint8Array(dec
), 2);
495 } else if (v
.type
=== 'lightfunc') {
496 dec
= Duktape
.dec('hex', v
.pointer
);
498 buf
= new Uint8Array(4 + len
);
500 buf
[1] = (v
.flags
>> 8) & 0xff;
501 buf
[2] = v
.flags
& 0xff;
503 buf
.set(new Uint8Array(dec
), 4);
505 } else if (v
.type
=== 'heapptr') {
506 dec
= Duktape
.dec('hex', v
.pointer
);
508 buf
= new Uint8Array(2 + len
);
511 buf
.set(new Uint8Array(dec
), 2);
514 } else if (v
=== null) {
515 return new Uint8Array([ 0x17 ]);
516 } else if (typeof v
=== 'boolean') {
517 return new Uint8Array([ v
? 0x18 : 0x19 ]);
518 } else if (typeof v
=== 'number') {
519 if (Math
.floor(v
) === v
&& /* whole */
520 (v
!== 0 || 1 / v
> 0) && /* not negative zero */
521 v
>= -0x80000000 && v
<= 0x7fffffff) {
522 // Represented signed 32-bit integers as plain integers.
523 // Debugger code expects this for all fields that are not
524 // duk_tval representations (e.g. command numbers and such).
525 if (v
>= 0x00 && v
<= 0x3f) {
526 return new Uint8Array([ 0x80 + v
]);
527 } else if (v
>= 0x0000 && v
<= 0x3fff) {
528 return new Uint8Array([ 0xc0 + (v
>> 8), v
& 0xff ]);
529 } else if (v
>= -0x80000000 && v
<= 0x7fffffff) {
530 return new Uint8Array([ 0x10,
536 throw new Error('internal error when encoding integer to dvalue: ' + v
);
539 // Represent non-integers as IEEE double dvalues.
540 buf
= new Uint8Array(1 + 8);
542 new DataView(buf
).setFloat64(1, v
, false);
545 } else if (typeof v
=== 'string') {
546 if (v
.length
< 0 || v
.length
> 0xffffffff) {
547 // Not possible in practice.
548 throw new TypeError('cannot convert to dvalue, invalid string length: ' + v
.length
);
550 if (v
.length
<= 0x1f) {
551 buf
= new Uint8Array(1 + v
.length
);
552 buf
[0] = 0x60 + v
.length
;
553 this.writeDebugStringToBuffer(v
, buf
, 1);
555 } else if (v
.length
<= 0xffff) {
556 buf
= new Uint8Array(3 + v
.length
);
558 buf
[1] = (v
.length
>> 8) & 0xff;
559 buf
[2] = (v
.length
>> 0) & 0xff;
560 this.writeDebugStringToBuffer(v
, buf
, 3);
563 buf
= new Uint8Array(5 + v
.length
);
565 buf
[1] = (v
.length
>> 24) & 0xff;
566 buf
[2] = (v
.length
>> 16) & 0xff;
567 buf
[3] = (v
.length
>> 8) & 0xff;
568 buf
[4] = (v
.length
>> 0) & 0xff;
569 this.writeDebugStringToBuffer(v
, buf
, 5);
574 throw new TypeError('value cannot be converted to dvalue: ' + JSON
.stringify(v
));
578 * Target binary connection handler
581 function TargetConnHandler(server
) {
582 this.name
= 'TargetConnHandler';
583 this.server
= server
;
584 this.handle
= uv
.new_tcp();
585 this.jsonHandler
= null;
586 this.incoming
= new Uint8Array(4096);
587 this.incomingOffset
= 0;
591 TargetConnHandler
.prototype.finish
= function finish(msg
) {
593 log
.info('TargetConnHandler already disconnected, ignore finish()');
596 log
.info('TargetConnHandler finished:', msg
);
598 this.jsonHandler
.writeJson({ notify
: '_TargetDisconnected' });
600 // XXX: write a notify to target?
602 uv
.shutdown(this.handle
);
603 uv
.read_stop(this.handle
);
604 uv
.close(this.handle
);
607 this.jsonHandler
.finish(msg
); // disconnect JSON client too (if not already disconnected)
610 TargetConnHandler
.prototype.onConnect
= function onConnect(err
) {
614 errmsg
= 'Failed to connect to target: ' + err
;
616 this.jsonHandler
.writeJson({ notify
: '_Error', args
: [ String(err
) ] });
621 // Once we're connected to the target, start read both binary and JSON
622 // input. We don't want to read JSON input before this so that we can
623 // always translate incoming messages to dvalues and write them out
624 // without queueing. Any pending JSON messages will be queued by the
627 log
.info('Connected to debug target at', targetHost
+ ':' + targetPort
);
628 uv
.read_start(this.jsonHandler
.handle
, this.jsonHandler
.onRead
.bind(this.jsonHandler
));
629 uv
.read_start(this.handle
, this.onRead
.bind(this));
632 TargetConnHandler
.prototype.writeBinary
= function writeBinary(buf
) {
633 var plain
= plainBufferCopy(buf
);
634 log
.info('PROXY --> TARGET:', Duktape
.enc('jx', plain
));
636 uv
.write(this.handle
, plain
);
640 TargetConnHandler
.prototype.onRead
= function onRead(err
, data
) {
646 log
.trace('Received data from target socket, err:', err
, 'data length:', data
? data
.length
: 'null');
649 errmsg
= 'Error reading data from debug target: ' + err
;
655 // Feed the data one byte at a time when torture testing.
656 if (TORTURE
&& data
.length
> 1) {
657 for (var i
= 0; i
< data
.length
; i
++) {
658 tmpBuf
= Duktape
.Buffer(1);
660 this.onRead(null, tmpBuf
);
665 // Receive data into 'incoming', resizing as necessary.
666 while (data
.length
> this.incoming
.length
- this.incomingOffset
) {
667 newIncoming
= new Uint8Array(this.incoming
.length
* 1.3 + 16);
668 newIncoming
.set(this.incoming
);
669 this.incoming
= newIncoming
;
670 log
.debug('Resize incoming binary buffer to ' + this.incoming
.length
);
672 this.incoming
.set(new Uint8Array(data
), this.incomingOffset
);
673 this.incomingOffset
+= data
.length
;
675 // Trial parse handshake unless done.
676 if (!this.handshake
) {
677 this.trialParseHandshake();
680 // Trial parse dvalue(s) and debug messages.
681 if (this.handshake
) {
683 res
= this.trialParseDvalue();
687 log
.trace('Got dvalue:', Duktape
.enc('jx', res
.dvalue
));
688 this.dvalues
.push(res
.dvalue
);
689 if (isObject(res
.dvalue
) && res
.dvalue
.type
=== 'eom') {
691 this.jsonHandler
.handleDebugMessage(this.dvalues
);
694 errmsg
= 'JSON message handling failed: ' + e
;
695 this.jsonHandler
.writeJson({ notify
: '_Error', args
: [ errmsg
] });
696 if (lenientJsonParse
) {
697 log
.warn('JSON message handling failed (lenient mode, ignoring):', e
);
699 log
.warn('JSON message handling failed (dropping connection):', e
);
707 log
.info('Target disconnected');
708 this.finish('Target disconnected');
712 TargetConnHandler
.prototype.trialParseHandshake
= function trialParseHandshake() {
713 var buf
= this.incoming
;
714 var avail
= this.incomingOffset
;
720 for (i
= 0; i
< avail
; i
++) {
721 if (buf
[i
] == 0x0a) {
722 msg
= String(plainBufferCopy(buf
.subarray(0, i
)));
723 this.incoming
.set(this.incoming
.subarray(i
+ 1));
724 this.incomingOffset
-= i
+ 1;
726 // Generic handshake format: only relies on initial version field.
727 m
= /^(\d+) (.*)$/.exec(msg
) || {};
728 protocolVersion
= +m
[1];
731 protocolVersion
: protocolVersion
,
735 // More detailed v1 handshake line.
736 if (protocolVersion
=== 1) {
737 m
= /^(\d+) (\d+) (.*?) (.*?) (.*)$/.exec(msg
) || {};
738 this.handshake
.dukVersion
= m
[1];
739 this.handshake
.dukGitDescribe
= m
[2];
740 this.handshake
.targetString
= m
[3];
743 this.jsonHandler
.writeJson({ notify
: '_TargetConnected', args
: [ msg
] });
745 log
.info('Target handshake: ' + JSON
.stringify(this.handshake
));
751 TargetConnHandler
.prototype.bufferToDebugString
= function bufferToDebugString(buf
) {
752 return String
.fromCharCode
.apply(null, buf
);
755 TargetConnHandler
.prototype.trialParseDvalue
= function trialParseDvalue() {
757 var buf
= this.incoming
;
758 var avail
= this.incomingOffset
;
760 var gotValue
= false; // explicit flag for e.g. v === undefined
761 var dv
= new DataView(buf
);
766 function consume(n
) {
767 log
.info('PROXY <-- TARGET:', Duktape
.enc('jx', _this
.incoming
.subarray(0, n
)));
768 _this
.incoming
.set(_this
.incoming
.subarray(n
));
769 _this
.incomingOffset
-= n
;
775 } else if (x
>= 0xc0) {
776 // 0xc0...0xff: integers 0-16383
778 v
= ((x
- 0xc0) << 8) + buf
[1];
781 } else if (x
>= 0x80) {
782 // 0x80...0xbf: integers 0-63
785 } else if (x
>= 0x60) {
786 // 0x60...0x7f: strings with length 0-31
788 if (avail
>= 1 + len
) {
789 v
= new Uint8Array(len
);
790 v
.set(buf
.subarray(1, 1 + len
));
791 v
= this.bufferToDebugString(v
);
796 case 0x00: consume(1); v
= { type
: 'eom' }; break;
797 case 0x01: consume(1); v
= { type
: 'req' }; break;
798 case 0x02: consume(1); v
= { type
: 'rep' }; break;
799 case 0x03: consume(1); v
= { type
: 'err' }; break;
800 case 0x04: consume(1); v
= { type
: 'nfy' }; break;
801 case 0x10: // 4-byte signed integer
803 v
= dv
.getInt32(1, false);
807 case 0x11: // 4-byte string
809 len
= dv
.getUint32(1, false);
810 if (avail
>= 5 + len
) {
811 v
= new Uint8Array(len
);
812 v
.set(buf
.subarray(5, 5 + len
));
813 v
= this.bufferToDebugString(v
);
818 case 0x12: // 2-byte string
820 len
= dv
.getUint16(1, false);
821 if (avail
>= 3 + len
) {
822 v
= new Uint8Array(len
);
823 v
.set(buf
.subarray(3, 3 + len
));
824 v
= this.bufferToDebugString(v
);
829 case 0x13: // 4-byte buffer
831 len
= dv
.getUint32(1, false);
832 if (avail
>= 5 + len
) {
833 v
= new Uint8Array(len
);
834 v
.set(buf
.subarray(5, 5 + len
));
835 v
= { type
: 'buffer', data
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
840 case 0x14: // 2-byte buffer
842 len
= dv
.getUint16(1, false);
843 if (avail
>= 3 + len
) {
844 v
= new Uint8Array(len
);
845 v
.set(buf
.subarray(3, 3 + len
));
846 v
= { type
: 'buffer', data
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
851 case 0x15: // unused/none
852 v
= { type
: 'unused' };
855 case 0x16: // undefined
856 v
= { type
: 'undefined' };
857 gotValue
= true; // indicate 'v' is actually set
862 gotValue
= true; // indicate 'v' is actually set
873 case 0x1a: // number (IEEE double), big endian
875 tmp
= new Uint8Array(8);
876 tmp
.set(buf
.subarray(1, 9));
877 v
= { type
: 'number', data
: Duktape
.enc('hex', Duktape
.Buffer(tmp
)) };
878 if (readableNumberValue
) {
879 // The value key should not be used programmatically,
880 // it is just there to make the dumps more readable.
881 v
.value
= new DataView(tmp
.buffer
).getFloat64(0, false);
889 if (avail
>= 3 + len
) {
890 v
= new Uint8Array(len
);
891 v
.set(buf
.subarray(3, 3 + len
));
892 v
= { type
: 'object', 'class': buf
[1], pointer
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
897 case 0x1c: // pointer
900 if (avail
>= 2 + len
) {
901 v
= new Uint8Array(len
);
902 v
.set(buf
.subarray(2, 2 + len
));
903 v
= { type
: 'pointer', pointer
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
908 case 0x1d: // lightfunc
911 if (avail
>= 4 + len
) {
912 v
= new Uint8Array(len
);
913 v
.set(buf
.subarray(4, 4 + len
));
914 v
= { type
: 'lightfunc', flags
: dv
.getUint16(1, false), pointer
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
919 case 0x1e: // heapptr
922 if (avail
>= 2 + len
) {
923 v
= new Uint8Array(len
);
924 v
.set(buf
.subarray(2, 2 + len
));
925 v
= { type
: 'heapptr', pointer
: Duktape
.enc('hex', Duktape
.Buffer(v
)) };
931 throw new Error('failed parse initial byte: ' + buf
[0]);
935 if (typeof v
!== 'undefined' || gotValue
) {
936 return { dvalue
: v
};
945 var argv
= typeof uv
.argv
=== 'function' ? uv
.argv() : [];
947 for (i
= 2; i
< argv
.length
; i
++) { // skip dukluv and script name
948 if (argv
[i
] == '--help') {
949 print('Usage: dukluv ' + argv
[1] + ' [option]+');
951 print(' --server-host HOST JSON proxy server listen address');
952 print(' --server-port PORT JSON proxy server listen port');
953 print(' --target-host HOST Debug target address');
954 print(' --target-port PORT Debug target port');
955 print(' --metadata FILE Proxy metadata file (usually named duk_debug_meta.json)');
956 print(' --log-level LEVEL Set log level, default is 2; 0=trace, 1=debug, 2=info, 3=warn, etc');
957 print(' --single Run a single proxy connection and exit (default: persist for multiple connections)');
958 print(' --readable-numbers Add a non-programmatic "value" key for IEEE doubles help readability');
959 print(' --lenient Ignore (with warning) invalid JSON without dropping connection');
960 print(' --jx-parse Parse JSON proxy input with JX, useful when testing manually');
962 return; // don't register any sockets/timers etc to exit
963 } else if (argv
[i
] == '--single') {
964 singleConnection
= true;
966 } else if (argv
[i
] == '--readable-numbers') {
967 readableNumberValue
= true;
969 } else if (argv
[i
] == '--lenient') {
970 lenientJsonParse
= true;
972 } else if (argv
[i
] == '--jx-parse') {
976 if (i
>= argv
.length
- 1) {
977 throw new Error('missing option value for ' + argv
[i
]);
979 if (argv
[i
] == '--server-host') {
980 serverHost
= argv
[i
+ 1];
982 } else if (argv
[i
] == '--server-port') {
983 serverPort
= Math
.floor(+argv
[i
+ 1]);
985 } else if (argv
[i
] == '--target-host') {
986 targetHost
= argv
[i
+ 1];
988 } else if (argv
[i
] == '--target-port') {
989 targetPort
= Math
.floor(+argv
[i
+ 1]);
991 } else if (argv
[i
] == '--metadata') {
992 metadataFile
= argv
[i
+ 1];
994 } else if (argv
[i
] == '--log-level') {
995 log
.l
= Math
.floor(+argv
[i
+ 1]);
998 throw new Error('invalid option ' + argv
[i
]);
1002 function runServer() {
1003 var serverSocket
= new JsonProxyServer(serverHost
, serverPort
);
1004 var connMode
= singleConnection
? 'single connection mode' : 'persistent connection mode';
1005 log
.info('Listening for incoming JSON debug connection on ' + serverHost
+ ':' + serverPort
+
1006 ', target is ' + targetHost
+ ':' + targetPort
+ ', ' + connMode
);
1010 log
.info('Read proxy metadata from', metadataFile
);
1011 readFully(metadataFile
, function (data
, err
) {
1013 log
.error('Failed to load metadata:', err
);
1017 metadata
= JSON
.parse(String(data
));
1019 log
.error('Failed to parse JSON metadata from ' + metadataFile
+ ': ' + e
);