diff options
Diffstat (limited to '')
-rw-r--r-- | src/civetweb/src/third_party/duktape-1.5.2/debugger/static/webui.js | 785 |
1 files changed, 785 insertions, 0 deletions
diff --git a/src/civetweb/src/third_party/duktape-1.5.2/debugger/static/webui.js b/src/civetweb/src/third_party/duktape-1.5.2/debugger/static/webui.js new file mode 100644 index 000000000..d400c6c35 --- /dev/null +++ b/src/civetweb/src/third_party/duktape-1.5.2/debugger/static/webui.js @@ -0,0 +1,785 @@ +/* + * Duktape debugger web client + * + * Talks to the NodeJS server using socket.io. + * + * http://unixpapa.com/js/key.html + */ + +// Update interval for custom source highlighting. +var SOURCE_UPDATE_INTERVAL = 350; + +// Source view +var activeFileName = null; // file that we want to be loaded in source view +var activeLine = null; // scroll to line once file has been loaded +var activeHighlight = null; // line that we want to highlight (if any) +var loadedFileName = null; // currently loaded (shown) file +var loadedLineCount = 0; // currently loaded file line count +var loadedFileExecuting = false; // true if currFileName (loosely) matches loadedFileName +var loadedLinePending = null; // if set, scroll loaded file to requested line +var highlightLine = null; // highlight line +var sourceEditedLines = []; // line numbers which have been modified + // (added classes etc, tracked for removing) +var sourceUpdateInterval = null; // timer for updating source view +var sourceFetchXhr = null; // current AJAX request for fetching a source file (if any) +var forceButtonUpdate = false; // hack to reset button states +var bytecodeDialogOpen = false; // bytecode dialog active +var bytecodeIdxHighlight = null; // index of currently highlighted line (or null) +var bytecodeIdxInstr = 0; // index to first line of bytecode instructions + +// Execution state +var prevState = null; // previous execution state ('paused', 'running', etc) +var prevAttached = null; // previous debugger attached state (true, false, null) +var currFileName = null; // current filename being executed +var currFuncName = null; // current function name being executed +var currLine = 0; // current line being executed +var currPc = 0; // current bytecode PC being executed +var currState = 0; // current execution state ('paused', 'running', 'detached', etc) +var currAttached = false; // current debugger attached state (true or false) +var currLocals = []; // current local variables +var currCallstack = []; // current callstack (from top to bottom) +var currBreakpoints = []; // current breakpoints +var startedRunning = 0; // timestamp when last started running (if running) + // (used to grey out the source file if running for long enough) + +/* + * Helpers + */ + +function formatBytes(x) { + if (x < 1024) { + return String(x) + ' bytes'; + } else if (x < 1024 * 1024) { + return (x / 1024).toPrecision(3) + ' kB'; + } else { + return (x / (1024 * 1024)).toPrecision(3) + ' MB'; + } +} + +/* + * Source view periodic update handling + */ + +function doSourceUpdate() { + var elem; + + // Remove previously added custom classes + sourceEditedLines.forEach(function (linenum) { + elem = $('#source-code div')[linenum - 1]; + if (elem) { + elem.classList.remove('breakpoint'); + elem.classList.remove('execution'); + elem.classList.remove('highlight'); + } + }); + sourceEditedLines.length = 0; + + // If we're executing the file shown, highlight current line + if (loadedFileExecuting) { + elem = $('#source-code div')[currLine - 1]; + if (elem) { + sourceEditedLines.push(currLine); + elem.classList.add('execution'); + } + } + + // Add breakpoints + currBreakpoints.forEach(function (bp) { + if (bp.fileName === loadedFileName) { + elem = $('#source-code div')[bp.lineNumber - 1]; + if (elem) { + sourceEditedLines.push(bp.lineNumber); + elem.classList.add('breakpoint'); + } + } + }); + + if (highlightLine !== null) { + elem = $('#source-code div')[highlightLine - 1]; + if (elem) { + sourceEditedLines.push(highlightLine); + elem.classList.add('highlight'); + } + } + + // Bytecode dialog highlight + if (loadedFileExecuting && bytecodeDialogOpen && bytecodeIdxHighlight !== bytecodeIdxInstr + currPc) { + if (typeof bytecodeIdxHighlight === 'number') { + $('#bytecode-preformatted div')[bytecodeIdxHighlight].classList.remove('highlight'); + } + bytecodeIdxHighlight = bytecodeIdxInstr + currPc; + $('#bytecode-preformatted div')[bytecodeIdxHighlight].classList.add('highlight'); + } + + // If no-one requested us to scroll to a specific line, finish. + if (loadedLinePending == null) { + return; + } + + var reqLine = loadedLinePending; + loadedLinePending = null; + + // Scroll to requested line. This is not very clean, so a better solution + // should be found: + // https://developer.mozilla.org/en-US/docs/Web/API/Element.scrollIntoView + // http://erraticdev.blogspot.fi/2011/02/jquery-scroll-into-view-plugin-with.html + // http://flesler.blogspot.fi/2007/10/jqueryscrollto.html + var tmpLine = Math.max(reqLine - 5, 0); + elem = $('#source-code div')[tmpLine]; + if (elem) { + elem.scrollIntoView(); + } +} + +// Source is updated periodically. Other code can also call doSourceUpdate() +// directly if an immediate update is needed. +sourceUpdateInterval = setInterval(doSourceUpdate, SOURCE_UPDATE_INTERVAL); + +/* + * UI update handling when exec-status update arrives + */ + +function doUiUpdate() { + var now = Date.now(); + + // Note: loadedFileName can be either from target or from server, but they + // must match exactly. We could do a loose match here, but exact matches + // are needed for proper breakpoint handling anyway. + loadedFileExecuting = (loadedFileName === currFileName); + + // If we just started running, store a timestamp so we can grey out the + // source view only if we execute long enough (i.e. we're not just + // stepping). + if (currState !== prevState && currState === 'running') { + startedRunning = now; + } + + // If we just became paused, check for eval watch + if (currState !== prevState && currState === 'paused') { + if ($('#eval-watch').is(':checked')) { + submitEval(); // don't clear eval input + } + } + + // Update current execution state + if (currFileName === '' && currLine === 0) { + $('#current-fileline').text(''); + } else { + $('#current-fileline').text(String(currFileName) + ':' + String(currLine)); + } + if (currFuncName === '' && currPc === 0) { + $('#current-funcpc').text(''); + } else { + $('#current-funcpc').text(String(currFuncName) + '() pc ' + String(currPc)); + } + $('#current-state').text(String(currState)); + + // Update buttons + if (currState !== prevState || currAttached !== prevAttached || forceButtonUpdate) { + $('#stepinto-button').prop('disabled', !currAttached || currState !== 'paused'); + $('#stepover-button').prop('disabled', !currAttached || currState !== 'paused'); + $('#stepout-button').prop('disabled', !currAttached || currState !== 'paused'); + $('#resume-button').prop('disabled', !currAttached || currState !== 'paused'); + $('#pause-button').prop('disabled', !currAttached || currState !== 'running'); + $('#attach-button').prop('disabled', currAttached); + if (currAttached) { + $('#attach-button').removeClass('enabled'); + } else { + $('#attach-button').addClass('enabled'); + } + $('#detach-button').prop('disabled', !currAttached); + $('#eval-button').prop('disabled', !currAttached); + $('#add-breakpoint-button').prop('disabled', !currAttached); + $('#delete-all-breakpoints-button').prop('disabled', !currAttached); + $('.delete-breakpoint-button').prop('disabled', !currAttached); + $('#putvar-button').prop('disabled', !currAttached); + $('#getvar-button').prop('disabled', !currAttached); + $('#heap-dump-download-button').prop('disabled', !currAttached); + } + if (currState !== 'running' || forceButtonUpdate) { + // Remove pending highlight once we're no longer running. + $('#pause-button').removeClass('pending'); + $('#eval-button').removeClass('pending'); + } + forceButtonUpdate = false; + + // Make source window grey when running for a longer time, use a small + // delay to avoid flashing grey when stepping. + if (currState === 'running' && now - startedRunning >= 500) { + $('#source-pre').removeClass('notrunning'); + $('#current-state').removeClass('notrunning'); + } else { + $('#source-pre').addClass('notrunning'); + $('#current-state').addClass('notrunning'); + } + + // Force source view to match currFileName only when running or when + // just became paused (from running or detached). + var fetchSource = false; + if (typeof currFileName === 'string') { + if (currState === 'running' || + (prevState !== 'paused' && currState === 'paused') || + (currAttached !== prevAttached)) { + if (activeFileName !== currFileName) { + fetchSource = true; + activeFileName = currFileName; + activeLine = currLine; + activeHighlight = null; + requestSourceRefetch(); + } + } + } + + // Force line update (scrollTop) only when running or just became paused. + // Otherwise let user browse and scroll source files freely. + if (!fetchSource) { + if ((prevState !== 'paused' && currState === 'paused') || + currState === 'running') { + loadedLinePending = currLine || 0; + } + } +} + +/* + * Init socket.io and add handlers + */ + +var socket = io(); // returns a Manager + +setInterval(function () { + socket.emit('keepalive', { + userAgent: (navigator || {}).userAgent + }); +}, 30000); + +socket.on('connect', function () { + $('#socketio-info').text('connected'); + currState = 'connected'; + + fetchSourceList(); +}); +socket.on('disconnect', function () { + $('#socketio-info').text('not connected'); + currState = 'disconnected'; +}); +socket.on('reconnecting', function () { + $('#socketio-info').text('reconnecting'); + currState = 'reconnecting'; +}); +socket.on('error', function (err) { + $('#socketio-info').text(err); +}); + +socket.on('replaced', function () { + // XXX: how to minimize the chance we'll further communciate with the + // server or reconnect to it? socket.reconnection()? + + // We'd like to window.close() here but can't (not allowed from scripts). + // Alert is the next best thing. + alert('Debugger connection replaced by a new one, do you have multiple tabs open? If so, please close this tab.'); +}); + +socket.on('keepalive', function (msg) { + // Not really interesting in the UI + // $('#server-info').text(new Date() + ': ' + JSON.stringify(msg)); +}); + +socket.on('basic-info', function (msg) { + $('#duk-version').text(String(msg.duk_version)); + $('#duk-git-describe').text(String(msg.duk_git_describe)); + $('#target-info').text(String(msg.target_info)); + $('#endianness').text(String(msg.endianness)); +}); + +socket.on('exec-status', function (msg) { + // Not 100% reliable if callstack has several functions of the same name + if (bytecodeDialogOpen && (currFileName != msg.fileName || currFuncName != msg.funcName)) { + socket.emit('get-bytecode', {}); + } + + currFileName = msg.fileName; + currFuncName = msg.funcName; + currLine = msg.line; + currPc = msg.pc; + currState = msg.state; + currAttached = msg.attached; + + // Duktape now restricts execution status updates quite effectively so + // there's no need to rate limit UI updates now. + + doUiUpdate(); + + prevState = currState; + prevAttached = currAttached; +}); + +// Update the "console" output based on lines sent by the server. The server +// rate limits these updates to keep the browser load under control. Even +// better would be for the client to pull this (and other stuff) on its own. +socket.on('output-lines', function (msg) { + var elem = $('#output'); + var i, n, ent; + + elem.empty(); + for (i = 0, n = msg.length; i < n; i++) { + ent = msg[i]; + if (ent.type === 'print') { + elem.append($('<div></div>').text(ent.message)); + } else if (ent.type === 'alert') { + elem.append($('<div class="alert"></div>').text(ent.message)); + } else if (ent.type === 'log') { + elem.append($('<div class="log loglevel' + ent.level + '"></div>').text(ent.message)); + } else if (ent.type === 'debugger-info') { + elem.append($('<div class="debugger-info"><div>').text(ent.message)); + } else if (ent.type === 'debugger-debug') { + elem.append($('<div class="debugger-debug"><div>').text(ent.message)); + } else { + elem.append($('<div></div>').text(ent.message)); + } + } + + // http://stackoverflow.com/questions/14918787/jquery-scroll-to-bottom-of-div-even-after-it-updates + // Stop queued animations so that we always scroll quickly to bottom + $('#output').stop(true); + $('#output').animate({ scrollTop: $('#output')[0].scrollHeight}, 1000); +}); + +socket.on('callstack', function (msg) { + var elem = $('#callstack'); + var s1, s2, div; + + currCallstack = msg.callstack; + + elem.empty(); + msg.callstack.forEach(function (e) { + s1 = $('<a class="rest"></a>').text(e.fileName + ':' + e.lineNumber + ' (pc ' + e.pc + ')'); // float + s1.on('click', function () { + activeFileName = e.fileName; + activeLine = e.lineNumber || 1; + activeHighlight = activeLine; + requestSourceRefetch(); + }); + s2 = $('<span class="func"></span>').text(e.funcName + '()'); + div = $('<div></div>'); + div.append(s1); + div.append(s2); + elem.append(div); + }); +}); + +socket.on('locals', function (msg) { + var elem = $('#locals'); + var s1, s2, div; + var i, n, e; + + currLocals = msg.locals; + + elem.empty(); + for (i = 0, n = msg.locals.length; i < n; i++) { + e = msg.locals[i]; + s1 = $('<span class="value"></span>').text(e.value); // float + s2 = $('<span class="key"></span>').text(e.key); + div = $('<div></div>'); + div.append(s1); + div.append(s2); + elem.append(div); + } +}); + +socket.on('debug-stats', function (msg) { + $('#debug-rx-bytes').text(formatBytes(msg.rxBytes)); + $('#debug-rx-dvalues').text(msg.rxDvalues); + $('#debug-rx-messages').text(msg.rxMessages); + $('#debug-rx-kbrate').text((msg.rxBytesPerSec / 1024).toFixed(2)); + $('#debug-tx-bytes').text(formatBytes(msg.txBytes)); + $('#debug-tx-dvalues').text(msg.txDvalues); + $('#debug-tx-messages').text(msg.txMessages); + $('#debug-tx-kbrate').text((msg.txBytesPerSec / 1024).toFixed(2)); +}); + +socket.on('breakpoints', function (msg) { + var elem = $('#breakpoints'); + var div; + var sub; + + currBreakpoints = msg.breakpoints; + + elem.empty(); + + // First line is special + div = $('<div></div>'); + sub = $('<button id="delete-all-breakpoints-button"></button>').text('Delete all breakpoints'); + sub.on('click', function () { + socket.emit('delete-all-breakpoints'); + }); + div.append(sub); + sub = $('<input id="add-breakpoint-file"></input>').val('file.js'); + div.append(sub); + sub = $('<span></span>').text(':'); + div.append(sub); + sub = $('<input id="add-breakpoint-line"></input>').val('123'); + div.append(sub); + sub = $('<button id="add-breakpoint-button"></button>').text('Add breakpoint'); + sub.on('click', function () { + socket.emit('add-breakpoint', { + fileName: $('#add-breakpoint-file').val(), + lineNumber: Number($('#add-breakpoint-line').val()) + }); + }); + div.append(sub); + sub = $('<span id="breakpoint-hint"></span>').text('or dblclick source'); + div.append(sub); + elem.append(div); + + // Active breakpoints follow + msg.breakpoints.forEach(function (bp) { + var div; + var sub; + + div = $('<div class="breakpoint-line"></div>'); + sub = $('<button class="delete-breakpoint-button"></button>').text('Delete'); + sub.on('click', function () { + socket.emit('delete-breakpoint', { + fileName: bp.fileName, + lineNumber: bp.lineNumber + }); + }); + div.append(sub); + sub = $('<a></a>').text((bp.fileName || '?') + ':' + (bp.lineNumber || 0)); + sub.on('click', function () { + activeFileName = bp.fileName || ''; + activeLine = bp.lineNumber || 1; + activeHighlight = activeLine; + requestSourceRefetch(); + }); + div.append(sub); + elem.append(div); + }); + + forceButtonUpdate = true; + doUiUpdate(); +}); + +socket.on('eval-result', function (msg) { + $('#eval-output').text((msg.error ? 'ERROR: ' : '') + msg.result); + + // Remove eval button "pulsating" glow when we get a result + $('#eval-button').removeClass('pending'); +}); + +socket.on('getvar-result', function (msg) { + $('#var-output').text(msg.found ? msg.result : 'NOTFOUND'); +}); + +socket.on('bytecode', function (msg) { + var elem, div; + var div; + + elem = $('#bytecode-preformatted'); + elem.empty(); + + msg.preformatted.split('\n').forEach(function (line, idx) { + div = $('<div></div>'); + div.text(line); + elem.append(div); + }); + + bytecodeIdxHighlight = null; + bytecodeIdxInstr = msg.idxPreformattedInstructions; +}); + +$('#stepinto-button').click(function () { + socket.emit('stepinto', {}); +}); + +$('#stepover-button').click(function () { + socket.emit('stepover', {}); +}); + +$('#stepout-button').click(function () { + socket.emit('stepout', {}); +}); + +$('#pause-button').click(function () { + socket.emit('pause', {}); + + // Pause may take seconds to complete so indicate it is pending. + $('#pause-button').addClass('pending'); +}); + +$('#resume-button').click(function () { + socket.emit('resume', {}); +}); + +$('#attach-button').click(function () { + socket.emit('attach', {}); +}); + +$('#detach-button').click(function () { + socket.emit('detach', {}); +}); + +$('#about-button').click(function () { + $('#about-dialog').dialog('open'); +}); + +$('#show-bytecode-button').click(function () { + bytecodeDialogOpen = true; + $('#bytecode-dialog').dialog('open'); + + elem = $('#bytecode-preformatted'); + elem.empty().text('Loading bytecode...'); + + socket.emit('get-bytecode', {}); +}); + +function submitEval() { + socket.emit('eval', { input: $('#eval-input').val() }); + + // Eval may take seconds to complete so indicate it is pending. + $('#eval-button').addClass('pending'); +} + +$('#eval-button').click(function () { + submitEval(); + $('#eval-input').val(''); +}); + +$('#getvar-button').click(function () { + socket.emit('getvar', { varname: $('#varname-input').val() }); +}); + +$('#putvar-button').click(function () { + // The variable value is parsed as JSON right now, but it'd be better to + // also be able to parse buffer values etc. + var val = JSON.parse($('#varvalue-input').val()); + socket.emit('putvar', { varname: $('#varname-input').val(), varvalue: val }); +}); + +$('#source-code').dblclick(function (event) { + var target = event.target; + var elems = $('#source-code div'); + var i, n; + var line = 0; + + // XXX: any faster way; elems doesn't have e.g. indexOf() + for (i = 0, n = elems.length; i < n; i++) { + if (target === elems[i]) { + line = i + 1; + } + } + + socket.emit('toggle-breakpoint', { + fileName: loadedFileName, + lineNumber: line + }); +}); + +function setSourceText(data) { + var elem, div; + + elem = $('#source-code'); + elem.empty(); + data.split('\n').forEach(function (line) { + div = $('<div></div>'); + div.text(line); + elem.append(div); + }); + + sourceEditedLines = []; +} + +function setSourceSelect(fileName) { + var elem; + var i, n, t; + + if (fileName == null) { + $('#source-select').val('__none__'); + return; + } + + elem = $('#source-select option'); + for (i = 0, n = elem.length; i < n; i++) { + // Exact match is required. + t = $(elem[i]).val(); + if (t === fileName) { + $('#source-select').val(t); + return; + } + } +} + +/* + * AJAX request handling to fetch source files + */ + +function requestSourceRefetch() { + // If previous update is pending, abort and start a new one. + if (sourceFetchXhr) { + sourceFetchXhr.abort(); + sourceFetchXhr = null; + } + + // Make copies of the requested file/line so that we have the proper + // values in case they've changed. + var fileName = activeFileName; + var lineNumber = activeLine; + + // AJAX request for the source. + sourceFetchXhr = $.ajax({ + type: 'POST', + url: '/source', + data: JSON.stringify({ fileName: fileName }), + contentType: 'application/json', + success: function (data, status, jqxhr) { + var elem; + + sourceFetchXhr = null; + + loadedFileName = fileName; + loadedLineCount = data.split('\n').length; // XXX: ignore issue with last empty line for now + loadedFileExecuting = (loadedFileName === currFileName); + setSourceText(data); + setSourceSelect(fileName); + loadedLinePending = activeLine || 1; + highlightLine = activeHighlight; // may be null + activeLine = null; + activeHighlight = null; + doSourceUpdate(); + + // XXX: hacky transition, make source change visible + $('#source-pre').fadeTo('fast', 0.25, function () { + $('#source-pre').fadeTo('fast', 1.0); + }); + }, + error: function (jqxhr, status, err) { + // Not worth alerting about because source fetch errors happen + // all the time, e.g. for dynamically evaluated code. + + sourceFetchXhr = null; + + // XXX: prevent retry of no-such-file by negative caching? + loadedFileName = fileName; + loadedLineCount = 1; + loadedFileExecuting = false; + setSourceText('// Cannot load source file: ' + fileName); + setSourceSelect(null); + loadedLinePending = 1; + activeLine = null; + activeHighlight = null; + doSourceUpdate(); + + // XXX: error transition here + $('#source-pre').fadeTo('fast', 0.25, function () { + $('#source-pre').fadeTo('fast', 1.0); + }); + }, + dataType: 'text' + }); +} + +/* + * AJAX request for fetching the source list + */ + +function fetchSourceList() { + $.ajax({ + type: 'POST', + url: '/sourceList', + data: JSON.stringify({}), + contentType: 'application/json', + success: function (data, status, jqxhr) { + var elem = $('#source-select'); + + data = JSON.parse(data); + + elem.empty(); + var opt = $('<option></option>').attr({ 'value': '__none__' }).text('No source file selected'); + elem.append(opt); + data.forEach(function (ent) { + var opt = $('<option></option>').attr({ 'value': ent }).text(ent); + elem.append(opt); + }); + elem.change(function () { + activeFileName = elem.val(); + activeLine = 1; + requestSourceRefetch(); + }); + }, + error: function (jqxhr, status, err) { + // This is worth alerting about as the UI is somewhat unusable + // if we don't get a source list. + + alert('Failed to load source list: ' + err); + }, + dataType: 'text' + }); +} + +/* + * Initialization + */ + +$(document).ready(function () { + var showAbout = true; + + // About dialog, shown automatically on first startup. + $('#about-dialog').dialog({ + autoOpen: false, + hide: 'fade', // puff + show: 'fade', // slide, puff + width: 500, + height: 300 + }); + + // Bytecode dialog + $('#bytecode-dialog').dialog({ + autoOpen: false, + hide: 'fade', // puff + show: 'fade', // slide, puff + width: 700, + height: 600, + close: function () { + bytecodeDialogOpen = false; + bytecodeIdxHighlight = null; + bytecodeIdxInstr = 0; + } + }); + + // http://diveintohtml5.info/storage.html + if (typeof localStorage !== 'undefined') { + if (localStorage.getItem('about-shown')) { + showAbout = false; + } else { + localStorage.setItem('about-shown', 'yes'); + } + } + if (showAbout) { + $('#about-dialog').dialog('open'); + } + + // onclick handler for exec status text + function loadCurrFunc() { + activeFileName = currFileName; + activeLine = currLine; + requestSourceRefetch(); + } + $('#exec-other').on('click', loadCurrFunc); + + // Enter handling for eval input + // https://forum.jquery.com/topic/bind-html-input-to-enter-key-keypress + $('#eval-input').keypress(function (event) { + if (event.keyCode == 13) { + submitEval(); + $('#eval-input').val(''); + } + }); + + // Eval watch handling + $('#eval-watch').change(function () { + // nop + }); + + forceButtonUpdate = true; + doUiUpdate(); +}); |