2 * Duktape debugger web client
4 * Talks to the NodeJS server using socket.io.
6 * http://unixpapa.com/js/key.html
9 // Update interval for custom source highlighting.
10 var SOURCE_UPDATE_INTERVAL
= 350;
13 var activeFileName
= null; // file that we want to be loaded in source view
14 var activeLine
= null; // scroll to line once file has been loaded
15 var activeHighlight
= null; // line that we want to highlight (if any)
16 var loadedFileName
= null; // currently loaded (shown) file
17 var loadedLineCount
= 0; // currently loaded file line count
18 var loadedFileExecuting
= false; // true if currFileName (loosely) matches loadedFileName
19 var loadedLinePending
= null; // if set, scroll loaded file to requested line
20 var highlightLine
= null; // highlight line
21 var sourceEditedLines
= []; // line numbers which have been modified
22 // (added classes etc, tracked for removing)
23 var sourceUpdateInterval
= null; // timer for updating source view
24 var sourceFetchXhr
= null; // current AJAX request for fetching a source file (if any)
25 var forceButtonUpdate
= false; // hack to reset button states
26 var bytecodeDialogOpen
= false; // bytecode dialog active
27 var bytecodeIdxHighlight
= null; // index of currently highlighted line (or null)
28 var bytecodeIdxInstr
= 0; // index to first line of bytecode instructions
31 var prevState
= null; // previous execution state ('paused', 'running', etc)
32 var prevAttached
= null; // previous debugger attached state (true, false, null)
33 var currFileName
= null; // current filename being executed
34 var currFuncName
= null; // current function name being executed
35 var currLine
= 0; // current line being executed
36 var currPc
= 0; // current bytecode PC being executed
37 var currState
= 0; // current execution state ('paused', 'running', 'detached', etc)
38 var currAttached
= false; // current debugger attached state (true or false)
39 var currLocals
= []; // current local variables
40 var currCallstack
= []; // current callstack (from top to bottom)
41 var currBreakpoints
= []; // current breakpoints
42 var startedRunning
= 0; // timestamp when last started running (if running)
43 // (used to grey out the source file if running for long enough)
49 function formatBytes(x
) {
51 return String(x
) + ' bytes';
52 } else if (x
< 1024 * 1024) {
53 return (x
/ 1024).toPrecision(3) + ' kB';
55 return (x
/ (1024 * 1024)).toPrecision(3) + ' MB';
60 * Source view periodic update handling
63 function doSourceUpdate() {
66 // Remove previously added custom classes
67 sourceEditedLines
.forEach(function (linenum
) {
68 elem
= $('#source-code div')[linenum
- 1];
70 elem
.classList
.remove('breakpoint');
71 elem
.classList
.remove('execution');
72 elem
.classList
.remove('highlight');
75 sourceEditedLines
.length
= 0;
77 // If we're executing the file shown, highlight current line
78 if (loadedFileExecuting
) {
79 elem
= $('#source-code div')[currLine
- 1];
81 sourceEditedLines
.push(currLine
);
82 elem
.classList
.add('execution');
87 currBreakpoints
.forEach(function (bp
) {
88 if (bp
.fileName
=== loadedFileName
) {
89 elem
= $('#source-code div')[bp
.lineNumber
- 1];
91 sourceEditedLines
.push(bp
.lineNumber
);
92 elem
.classList
.add('breakpoint');
97 if (highlightLine
!== null) {
98 elem
= $('#source-code div')[highlightLine
- 1];
100 sourceEditedLines
.push(highlightLine
);
101 elem
.classList
.add('highlight');
105 // Bytecode dialog highlight
106 if (loadedFileExecuting
&& bytecodeDialogOpen
&& bytecodeIdxHighlight
!== bytecodeIdxInstr
+ currPc
) {
107 if (typeof bytecodeIdxHighlight
=== 'number') {
108 $('#bytecode-preformatted div')[bytecodeIdxHighlight
].classList
.remove('highlight');
110 bytecodeIdxHighlight
= bytecodeIdxInstr
+ currPc
;
111 $('#bytecode-preformatted div')[bytecodeIdxHighlight
].classList
.add('highlight');
114 // If no-one requested us to scroll to a specific line, finish.
115 if (loadedLinePending
== null) {
119 var reqLine
= loadedLinePending
;
120 loadedLinePending
= null;
122 // Scroll to requested line. This is not very clean, so a better solution
124 // https://developer.mozilla.org/en-US/docs/Web/API/Element.scrollIntoView
125 // http://erraticdev.blogspot.fi/2011/02/jquery-scroll-into-view-plugin-with.html
126 // http://flesler.blogspot.fi/2007/10/jqueryscrollto.html
127 var tmpLine
= Math
.max(reqLine
- 5, 0);
128 elem
= $('#source-code div')[tmpLine
];
130 elem
.scrollIntoView();
134 // Source is updated periodically. Other code can also call doSourceUpdate()
135 // directly if an immediate update is needed.
136 sourceUpdateInterval
= setInterval(doSourceUpdate
, SOURCE_UPDATE_INTERVAL
);
139 * UI update handling when exec-status update arrives
142 function doUiUpdate() {
143 var now
= Date
.now();
145 // Note: loadedFileName can be either from target or from server, but they
146 // must match exactly. We could do a loose match here, but exact matches
147 // are needed for proper breakpoint handling anyway.
148 loadedFileExecuting
= (loadedFileName
=== currFileName
);
150 // If we just started running, store a timestamp so we can grey out the
151 // source view only if we execute long enough (i.e. we're not just
153 if (currState
!== prevState
&& currState
=== 'running') {
154 startedRunning
= now
;
157 // If we just became paused, check for eval watch
158 if (currState
!== prevState
&& currState
=== 'paused') {
159 if ($('#eval-watch').is(':checked')) {
160 submitEval(); // don't clear eval input
164 // Update current execution state
165 if (currFileName
=== '' && currLine
=== 0) {
166 $('#current-fileline').text('');
168 $('#current-fileline').text(String(currFileName
) + ':' + String(currLine
));
170 if (currFuncName
=== '' && currPc
=== 0) {
171 $('#current-funcpc').text('');
173 $('#current-funcpc').text(String(currFuncName
) + '() pc ' + String(currPc
));
175 $('#current-state').text(String(currState
));
178 if (currState
!== prevState
|| currAttached
!== prevAttached
|| forceButtonUpdate
) {
179 $('#stepinto-button').prop('disabled', !currAttached
|| currState
!== 'paused');
180 $('#stepover-button').prop('disabled', !currAttached
|| currState
!== 'paused');
181 $('#stepout-button').prop('disabled', !currAttached
|| currState
!== 'paused');
182 $('#resume-button').prop('disabled', !currAttached
|| currState
!== 'paused');
183 $('#pause-button').prop('disabled', !currAttached
|| currState
!== 'running');
184 $('#attach-button').prop('disabled', currAttached
);
186 $('#attach-button').removeClass('enabled');
188 $('#attach-button').addClass('enabled');
190 $('#detach-button').prop('disabled', !currAttached
);
191 $('#eval-button').prop('disabled', !currAttached
);
192 $('#add-breakpoint-button').prop('disabled', !currAttached
);
193 $('#delete-all-breakpoints-button').prop('disabled', !currAttached
);
194 $('.delete-breakpoint-button').prop('disabled', !currAttached
);
195 $('#putvar-button').prop('disabled', !currAttached
);
196 $('#getvar-button').prop('disabled', !currAttached
);
197 $('#heap-dump-download-button').prop('disabled', !currAttached
);
199 if (currState
!== 'running' || forceButtonUpdate
) {
200 // Remove pending highlight once we're no longer running.
201 $('#pause-button').removeClass('pending');
202 $('#eval-button').removeClass('pending');
204 forceButtonUpdate
= false;
206 // Make source window grey when running for a longer time, use a small
207 // delay to avoid flashing grey when stepping.
208 if (currState
=== 'running' && now
- startedRunning
>= 500) {
209 $('#source-pre').removeClass('notrunning');
210 $('#current-state').removeClass('notrunning');
212 $('#source-pre').addClass('notrunning');
213 $('#current-state').addClass('notrunning');
216 // Force source view to match currFileName only when running or when
217 // just became paused (from running or detached).
218 var fetchSource
= false;
219 if (typeof currFileName
=== 'string') {
220 if (currState
=== 'running' ||
221 (prevState
!== 'paused' && currState
=== 'paused') ||
222 (currAttached
!== prevAttached
)) {
223 if (activeFileName
!== currFileName
) {
225 activeFileName
= currFileName
;
226 activeLine
= currLine
;
227 activeHighlight
= null;
228 requestSourceRefetch();
233 // Force line update (scrollTop) only when running or just became paused.
234 // Otherwise let user browse and scroll source files freely.
236 if ((prevState
!== 'paused' && currState
=== 'paused') ||
237 currState
=== 'running') {
238 loadedLinePending
= currLine
|| 0;
244 * Init socket.io and add handlers
247 var socket
= io(); // returns a Manager
249 setInterval(function () {
250 socket
.emit('keepalive', {
251 userAgent
: (navigator
|| {}).userAgent
255 socket
.on('connect', function () {
256 $('#socketio-info').text('connected');
257 currState
= 'connected';
261 socket
.on('disconnect', function () {
262 $('#socketio-info').text('not connected');
263 currState
= 'disconnected';
265 socket
.on('reconnecting', function () {
266 $('#socketio-info').text('reconnecting');
267 currState
= 'reconnecting';
269 socket
.on('error', function (err
) {
270 $('#socketio-info').text(err
);
273 socket
.on('replaced', function () {
274 // XXX: how to minimize the chance we'll further communciate with the
275 // server or reconnect to it? socket.reconnection()?
277 // We'd like to window.close() here but can't (not allowed from scripts).
278 // Alert is the next best thing.
279 alert('Debugger connection replaced by a new one, do you have multiple tabs open? If so, please close this tab.');
282 socket
.on('keepalive', function (msg
) {
283 // Not really interesting in the UI
284 // $('#server-info').text(new Date() + ': ' + JSON.stringify(msg));
287 socket
.on('basic-info', function (msg
) {
288 $('#duk-version').text(String(msg
.duk_version
));
289 $('#duk-git-describe').text(String(msg
.duk_git_describe
));
290 $('#target-info').text(String(msg
.target_info
));
291 $('#endianness').text(String(msg
.endianness
));
294 socket
.on('exec-status', function (msg
) {
295 // Not 100% reliable if callstack has several functions of the same name
296 if (bytecodeDialogOpen
&& (currFileName
!= msg
.fileName
|| currFuncName
!= msg
.funcName
)) {
297 socket
.emit('get-bytecode', {});
300 currFileName
= msg
.fileName
;
301 currFuncName
= msg
.funcName
;
304 currState
= msg
.state
;
305 currAttached
= msg
.attached
;
307 // Duktape now restricts execution status updates quite effectively so
308 // there's no need to rate limit UI updates now.
312 prevState
= currState
;
313 prevAttached
= currAttached
;
316 // Update the "console" output based on lines sent by the server. The server
317 // rate limits these updates to keep the browser load under control. Even
318 // better would be for the client to pull this (and other stuff) on its own.
319 socket
.on('output-lines', function (msg
) {
320 var elem
= $('#output');
324 for (i
= 0, n
= msg
.length
; i
< n
; i
++) {
326 if (ent
.type
=== 'print') {
327 elem
.append($('<div></div>').text(ent
.message
));
328 } else if (ent
.type
=== 'alert') {
329 elem
.append($('<div class="alert"></div>').text(ent
.message
));
330 } else if (ent
.type
=== 'log') {
331 elem
.append($('<div class="log loglevel' + ent
.level
+ '"></div>').text(ent
.message
));
332 } else if (ent
.type
=== 'debugger-info') {
333 elem
.append($('<div class="debugger-info"><div>').text(ent
.message
));
334 } else if (ent
.type
=== 'debugger-debug') {
335 elem
.append($('<div class="debugger-debug"><div>').text(ent
.message
));
337 elem
.append($('<div></div>').text(ent
.message
));
341 // http://stackoverflow.com/questions/14918787/jquery-scroll-to-bottom-of-div-even-after-it-updates
342 // Stop queued animations so that we always scroll quickly to bottom
343 $('#output').stop(true);
344 $('#output').animate({ scrollTop
: $('#output')[0].scrollHeight
}, 1000);
347 socket
.on('callstack', function (msg
) {
348 var elem
= $('#callstack');
351 currCallstack
= msg
.callstack
;
354 msg
.callstack
.forEach(function (e
) {
355 s1
= $('<a class="rest"></a>').text(e
.fileName
+ ':' + e
.lineNumber
+ ' (pc ' + e
.pc
+ ')'); // float
356 s1
.on('click', function () {
357 activeFileName
= e
.fileName
;
358 activeLine
= e
.lineNumber
|| 1;
359 activeHighlight
= activeLine
;
360 requestSourceRefetch();
362 s2
= $('<span class="func"></span>').text(e
.funcName
+ '()');
363 div
= $('<div></div>');
370 socket
.on('locals', function (msg
) {
371 var elem
= $('#locals');
375 currLocals
= msg
.locals
;
378 for (i
= 0, n
= msg
.locals
.length
; i
< n
; i
++) {
380 s1
= $('<span class="value"></span>').text(e
.value
); // float
381 s2
= $('<span class="key"></span>').text(e
.key
);
382 div
= $('<div></div>');
389 socket
.on('debug-stats', function (msg
) {
390 $('#debug-rx-bytes').text(formatBytes(msg
.rxBytes
));
391 $('#debug-rx-dvalues').text(msg
.rxDvalues
);
392 $('#debug-rx-messages').text(msg
.rxMessages
);
393 $('#debug-rx-kbrate').text((msg
.rxBytesPerSec
/ 1024).toFixed(2));
394 $('#debug-tx-bytes').text(formatBytes(msg
.txBytes
));
395 $('#debug-tx-dvalues').text(msg
.txDvalues
);
396 $('#debug-tx-messages').text(msg
.txMessages
);
397 $('#debug-tx-kbrate').text((msg
.txBytesPerSec
/ 1024).toFixed(2));
400 socket
.on('breakpoints', function (msg
) {
401 var elem
= $('#breakpoints');
405 currBreakpoints
= msg
.breakpoints
;
409 // First line is special
410 div
= $('<div></div>');
411 sub
= $('<button id="delete-all-breakpoints-button"></button>').text('Delete all breakpoints');
412 sub
.on('click', function () {
413 socket
.emit('delete-all-breakpoints');
416 sub
= $('<input id="add-breakpoint-file"></input>').val('file.js');
418 sub
= $('<span></span>').text(':');
420 sub
= $('<input id="add-breakpoint-line"></input>').val('123');
422 sub
= $('<button id="add-breakpoint-button"></button>').text('Add breakpoint');
423 sub
.on('click', function () {
424 socket
.emit('add-breakpoint', {
425 fileName
: $('#add-breakpoint-file').val(),
426 lineNumber
: Number($('#add-breakpoint-line').val())
430 sub
= $('<span id="breakpoint-hint"></span>').text('or dblclick source');
434 // Active breakpoints follow
435 msg
.breakpoints
.forEach(function (bp
) {
439 div
= $('<div class="breakpoint-line"></div>');
440 sub
= $('<button class="delete-breakpoint-button"></button>').text('Delete');
441 sub
.on('click', function () {
442 socket
.emit('delete-breakpoint', {
443 fileName
: bp
.fileName
,
444 lineNumber
: bp
.lineNumber
448 sub
= $('<a></a>').text((bp
.fileName
|| '?') + ':' + (bp
.lineNumber
|| 0));
449 sub
.on('click', function () {
450 activeFileName
= bp
.fileName
|| '';
451 activeLine
= bp
.lineNumber
|| 1;
452 activeHighlight
= activeLine
;
453 requestSourceRefetch();
459 forceButtonUpdate
= true;
463 socket
.on('eval-result', function (msg
) {
464 $('#eval-output').text((msg
.error
? 'ERROR: ' : '') + msg
.result
);
466 // Remove eval button "pulsating" glow when we get a result
467 $('#eval-button').removeClass('pending');
470 socket
.on('getvar-result', function (msg
) {
471 $('#var-output').text(msg
.found
? msg
.result
: 'NOTFOUND');
474 socket
.on('bytecode', function (msg
) {
478 elem
= $('#bytecode-preformatted');
481 msg
.preformatted
.split('\n').forEach(function (line
, idx
) {
482 div
= $('<div></div>');
487 bytecodeIdxHighlight
= null;
488 bytecodeIdxInstr
= msg
.idxPreformattedInstructions
;
491 $('#stepinto-button').click(function () {
492 socket
.emit('stepinto', {});
495 $('#stepover-button').click(function () {
496 socket
.emit('stepover', {});
499 $('#stepout-button').click(function () {
500 socket
.emit('stepout', {});
503 $('#pause-button').click(function () {
504 socket
.emit('pause', {});
506 // Pause may take seconds to complete so indicate it is pending.
507 $('#pause-button').addClass('pending');
510 $('#resume-button').click(function () {
511 socket
.emit('resume', {});
514 $('#attach-button').click(function () {
515 socket
.emit('attach', {});
518 $('#detach-button').click(function () {
519 socket
.emit('detach', {});
522 $('#about-button').click(function () {
523 $('#about-dialog').dialog('open');
526 $('#show-bytecode-button').click(function () {
527 bytecodeDialogOpen
= true;
528 $('#bytecode-dialog').dialog('open');
530 elem
= $('#bytecode-preformatted');
531 elem
.empty().text('Loading bytecode...');
533 socket
.emit('get-bytecode', {});
536 function submitEval() {
537 socket
.emit('eval', { input
: $('#eval-input').val() });
539 // Eval may take seconds to complete so indicate it is pending.
540 $('#eval-button').addClass('pending');
543 $('#eval-button').click(function () {
545 $('#eval-input').val('');
548 $('#getvar-button').click(function () {
549 socket
.emit('getvar', { varname
: $('#varname-input').val() });
552 $('#putvar-button').click(function () {
553 // The variable value is parsed as JSON right now, but it'd be better to
554 // also be able to parse buffer values etc.
555 var val
= JSON
.parse($('#varvalue-input').val());
556 socket
.emit('putvar', { varname
: $('#varname-input').val(), varvalue
: val
});
559 $('#source-code').dblclick(function (event
) {
560 var target
= event
.target
;
561 var elems
= $('#source-code div');
565 // XXX: any faster way; elems doesn't have e.g. indexOf()
566 for (i
= 0, n
= elems
.length
; i
< n
; i
++) {
567 if (target
=== elems
[i
]) {
572 socket
.emit('toggle-breakpoint', {
573 fileName
: loadedFileName
,
578 function setSourceText(data
) {
581 elem
= $('#source-code');
583 data
.split('\n').forEach(function (line
) {
584 div
= $('<div></div>');
589 sourceEditedLines
= [];
592 function setSourceSelect(fileName
) {
596 if (fileName
== null) {
597 $('#source-select').val('__none__');
601 elem
= $('#source-select option');
602 for (i
= 0, n
= elem
.length
; i
< n
; i
++) {
603 // Exact match is required.
604 t
= $(elem
[i
]).val();
605 if (t
=== fileName
) {
606 $('#source-select').val(t
);
613 * AJAX request handling to fetch source files
616 function requestSourceRefetch() {
617 // If previous update is pending, abort and start a new one.
618 if (sourceFetchXhr
) {
619 sourceFetchXhr
.abort();
620 sourceFetchXhr
= null;
623 // Make copies of the requested file/line so that we have the proper
624 // values in case they've changed.
625 var fileName
= activeFileName
;
626 var lineNumber
= activeLine
;
628 // AJAX request for the source.
629 sourceFetchXhr
= $.ajax({
632 data
: JSON
.stringify({ fileName
: fileName
}),
633 contentType
: 'application/json',
634 success: function (data
, status
, jqxhr
) {
637 sourceFetchXhr
= null;
639 loadedFileName
= fileName
;
640 loadedLineCount
= data
.split('\n').length
; // XXX: ignore issue with last empty line for now
641 loadedFileExecuting
= (loadedFileName
=== currFileName
);
643 setSourceSelect(fileName
);
644 loadedLinePending
= activeLine
|| 1;
645 highlightLine
= activeHighlight
; // may be null
647 activeHighlight
= null;
650 // XXX: hacky transition, make source change visible
651 $('#source-pre').fadeTo('fast', 0.25, function () {
652 $('#source-pre').fadeTo('fast', 1.0);
655 error: function (jqxhr
, status
, err
) {
656 // Not worth alerting about because source fetch errors happen
657 // all the time, e.g. for dynamically evaluated code.
659 sourceFetchXhr
= null;
661 // XXX: prevent retry of no-such-file by negative caching?
662 loadedFileName
= fileName
;
664 loadedFileExecuting
= false;
665 setSourceText('// Cannot load source file: ' + fileName
);
666 setSourceSelect(null);
667 loadedLinePending
= 1;
669 activeHighlight
= null;
672 // XXX: error transition here
673 $('#source-pre').fadeTo('fast', 0.25, function () {
674 $('#source-pre').fadeTo('fast', 1.0);
682 * AJAX request for fetching the source list
685 function fetchSourceList() {
689 data
: JSON
.stringify({}),
690 contentType
: 'application/json',
691 success: function (data
, status
, jqxhr
) {
692 var elem
= $('#source-select');
694 data
= JSON
.parse(data
);
697 var opt
= $('<option></option>').attr({ 'value': '__none__' }).text('No source file selected');
699 data
.forEach(function (ent
) {
700 var opt
= $('<option></option>').attr({ 'value': ent
}).text(ent
);
703 elem
.change(function () {
704 activeFileName
= elem
.val();
706 requestSourceRefetch();
709 error: function (jqxhr
, status
, err
) {
710 // This is worth alerting about as the UI is somewhat unusable
711 // if we don't get a source list.
713 alert('Failed to load source list: ' + err
);
723 $(document
).ready(function () {
724 var showAbout
= true;
726 // About dialog, shown automatically on first startup.
727 $('#about-dialog').dialog({
729 hide
: 'fade', // puff
730 show
: 'fade', // slide, puff
736 $('#bytecode-dialog').dialog({
738 hide
: 'fade', // puff
739 show
: 'fade', // slide, puff
743 bytecodeDialogOpen
= false;
744 bytecodeIdxHighlight
= null;
745 bytecodeIdxInstr
= 0;
749 // http://diveintohtml5.info/storage.html
750 if (typeof localStorage
!== 'undefined') {
751 if (localStorage
.getItem('about-shown')) {
754 localStorage
.setItem('about-shown', 'yes');
758 $('#about-dialog').dialog('open');
761 // onclick handler for exec status text
762 function loadCurrFunc() {
763 activeFileName
= currFileName
;
764 activeLine
= currLine
;
765 requestSourceRefetch();
767 $('#exec-other').on('click', loadCurrFunc
);
769 // Enter handling for eval input
770 // https://forum.jquery.com/topic/bind-html-input-to-enter-key-keypress
771 $('#eval-input').keypress(function (event
) {
772 if (event
.keyCode
== 13) {
774 $('#eval-input').val('');
778 // Eval watch handling
779 $('#eval-watch').change(function () {
783 forceButtonUpdate
= true;