/* * 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($('
').text(ent.message)); } else if (ent.type === 'alert') { elem.append($('').text(ent.message)); } else if (ent.type === 'log') { elem.append($('').text(ent.message)); } else if (ent.type === 'debugger-info') { elem.append($('