diff options
author | erikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-31 16:25:09 +0000 |
---|---|---|
committer | erikkay@google.com <erikkay@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-07-31 16:25:09 +0000 |
commit | 5ced5eb46be9e6c2d2b72a421d930cee6d7fcf89 (patch) | |
tree | ffccdfbb0ccd8a7f2663d0144e7684c8ffee0194 /chrome/browser/debugger/resources | |
parent | f18953d3c2a4eae2bc33b1eecce96e28f0815a64 (diff) | |
download | chromium_src-5ced5eb46be9e6c2d2b72a421d930cee6d7fcf89.zip chromium_src-5ced5eb46be9e6c2d2b72a421d930cee6d7fcf89.tar.gz chromium_src-5ced5eb46be9e6c2d2b72a421d930cee6d7fcf89.tar.bz2 |
Initial cleanup and refactoring to make debugger UI use DHTML and get rid of the last of its native UI. This is done using a DOMUIHost subclass and a new TabContents type.
This checkin also fixes a few minor issues:
* hitting the keyboard accelerator brings the current debugger window to front
* text is grayed out when in "running" mode rather than "paused"
* up/down arrows have command-line history (transient)
* some text used to get eaten when you first bring up the window ("attached to <tabname>"), this is now handled
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@180 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/debugger/resources')
-rw-r--r-- | chrome/browser/debugger/resources/debugger.css | 35 | ||||
-rw-r--r-- | chrome/browser/debugger/resources/debugger.html | 34 | ||||
-rw-r--r-- | chrome/browser/debugger/resources/debugger.js | 124 | ||||
-rw-r--r-- | chrome/browser/debugger/resources/debugger_resources.h | 6 | ||||
-rw-r--r-- | chrome/browser/debugger/resources/debugger_resources.rc | 19 | ||||
-rw-r--r-- | chrome/browser/debugger/resources/debugger_shell.js | 1246 |
6 files changed, 1464 insertions, 0 deletions
diff --git a/chrome/browser/debugger/resources/debugger.css b/chrome/browser/debugger/resources/debugger.css new file mode 100644 index 0000000..ed9df2f --- /dev/null +++ b/chrome/browser/debugger/resources/debugger.css @@ -0,0 +1,35 @@ +/** + * Style for javascript debugger. See debugger.html. + */ + +html,body { + margin: 0px; + padding: 0px; + height: 100%; +} +#output { + font-family: monospace; + background-color: #ffffff; + min-height: 100%; +} +#outer { + height: 100%; + width: 100%; + white-space: pre-wrap; + padding: 0px 0px 24px 0px; +} +#command-line { + bottom: 0px; + /* not quite sure why this 5px is necessary */ + right: 5px; + left: 0px; + position: fixed; + padding: 0px; + margin: 0px; +} +#command-line-text { + height: 20px; + display: block; + width: 100%; + font-family: monospace; +} diff --git a/chrome/browser/debugger/resources/debugger.html b/chrome/browser/debugger/resources/debugger.html new file mode 100644 index 0000000..7eab990 --- /dev/null +++ b/chrome/browser/debugger/resources/debugger.html @@ -0,0 +1,34 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<!-- + The UI for the javascript debugger window. +--> + <head> + <title>JavaScript Debugger</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF8" /> + <link rel="stylesheet" href="debugger.css" type="text/css" /> + <script type="text/javascript" src="debugger.js"></script> + </head> + + <body> + + <table id='outer'> + <tr> + <td valign='bottom' id='output'>Chrome JavaScript Debugger<br />Type 'help' for a list of commands.<br /></td> + </tr> + </table> + + <div id='command-line'> + <!-- TODO(erikkay) - use addEventListener instead --> + <input id='command-line-text' + onkeydown="return keydown(event);" + onkeypress="return keypress(event);" type="text" /> + </div> + + <script type="text/javascript" language="javascript"> + loaded(); + </script> + + </body> +</html> diff --git a/chrome/browser/debugger/resources/debugger.js b/chrome/browser/debugger/resources/debugger.js new file mode 100644 index 0000000..a52a5c0 --- /dev/null +++ b/chrome/browser/debugger/resources/debugger.js @@ -0,0 +1,124 @@ +/** + * @fileoverview Helper functions and objects for the JS debugger UI. + * @see debugger.html + */ + +/** + * Called at the end of <body>. + */ +function loaded() { + focusOnCommandLine(); +}; + +/** + * Sets focus to command-line-text element. + */ +function focusOnCommandLine() { + var input = document.getElementById('command-line-text'); + input.focus(); +}; + +/** + * Called by chrome code when there's output to display. + */ +function appendText(txt) { + var output = document.getElementById('output'); + output.appendChild(document.createTextNode(txt)); + output.appendChild(document.createElement('br')); + document.body.scrollTop = document.body.scrollHeight; +}; + +// command object stores command-line history state. +var command = { + history: [], + history_index: 0, + pending: null +}; + +/** + * Execute a debugger command, add it to the command history and display it in + * the output window. + */ +function executeCommand(str) { + appendText("$ " + str); + // Sends field.value to DebuggerContents.HandleCommand. + chrome.send("command", [str]); + command.history.push(str); + command.history_index = command.history.length; + command.pending = null; +}; + +/** + * Display the previous history item in the given text field. + */ +function selectPreviousCommand(field) { + if (command.history_index > 0) { + // Remember the current field value as a pending command if we're at the + // end (it's something the user typed in). + if (command.history_index == command.history.length) + command.pending = field.value; + command.history_index--; + field.value = command.history[command.history_index]; + field.select(); + } +}; + +/** + * Display the next history item in the given text field. + */ +function selectNextCommand(field) { + if (command.history_index < command.history.length) { + command.history_index++; + if (command.history_index == command.history.length) { + field.value = command.pending || ""; + } else { + field.value = command.history[command.history_index]; + } + field.select(); + } +}; + + +/** + * command-line-text's onkeypress handler + */ +function keypress(e) { + var field = e.target; + var key = e.keyCode; + if (key == 13) { // enter + executeCommand(field.value); + field.value = ""; + return false; + } + return true; +}; + +/** + * command-line-text's onkeydown handler + */ +function keydown(e) { + var field = e.target; + var key = e.keyCode; + if (key == 38) { // up arrow + selectPreviousCommand(field); + return false; + } else if (key == 40) { // down arrow + selectNextCommand(field); + return false; + } + return true; +}; + +/** + * Called by chrome code to set the current state as to whether the debugger + * is stopped at a breakpoint or is running. + */ +function setDebuggerBreak(is_broken) { + var out = document.getElementById('output'); + if (is_broken) { + out.style.color = "black"; + focusOnCommandLine(); + } else { + out.style.color = "gray"; + } +}; diff --git a/chrome/browser/debugger/resources/debugger_resources.h b/chrome/browser/debugger/resources/debugger_resources.h new file mode 100644 index 0000000..0dbf643 --- /dev/null +++ b/chrome/browser/debugger/resources/debugger_resources.h @@ -0,0 +1,6 @@ +// TODO(tc): Come up with a way to automate the generation of these +// IDs so they don't collide with other rc files. +#define IDR_DEBUGGER_SHELL_JS 600 +#define IDR_DEBUGGER_HTML 601 +#define IDR_DEBUGGER_CSS 602 +#define IDR_DEBUGGER_JS 603 diff --git a/chrome/browser/debugger/resources/debugger_resources.rc b/chrome/browser/debugger/resources/debugger_resources.rc new file mode 100644 index 0000000..13c2e87 --- /dev/null +++ b/chrome/browser/debugger/resources/debugger_resources.rc @@ -0,0 +1,19 @@ +// Resources used by browser/debugger/*.
+//
+// Paths in this file are relative to SolutionDir.
+
+#ifdef APSTUDIO_INVOKED
+ #error // Don't open in the Visual Studio resource editor!
+#endif //APSTUDIO_INVOKED
+
+#include "browser\\debugger\\resources\\debugger_resources.h"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// data resources
+//
+
+IDR_DEBUGGER_SHELL_JS BINDATA "browser\\debugger\\resources\\debugger_shell.js"
+IDR_DEBUGGER_HTML BINDATA "browser\\debugger\\resources\\debugger.html"
+IDR_DEBUGGER_CSS BINDATA "browser\\debugger\\resources\\debugger.css"
+IDR_DEBUGGER_JS BINDATA "browser\\debugger\\resources\\debugger.js"
diff --git a/chrome/browser/debugger/resources/debugger_shell.js b/chrome/browser/debugger/resources/debugger_shell.js new file mode 100644 index 0000000..ab89c0d --- /dev/null +++ b/chrome/browser/debugger/resources/debugger_shell.js @@ -0,0 +1,1246 @@ +/** + * @fileoverview Shell objects and global helper functions for Chrome + * automation shell / debugger. This file is loaded into the global namespace + * of the interactive shell, so users can simply call global functions + * directly. + */ + +// TODO(erikkay): look into how this can be split up into multiple files +// It's currently loaded explicitly by Chrome, so maybe I need an "include" +// or "source" builtin to allow a core source file to reference multiple +// sub-files. + +/** + * Sequence number of the DebugCommand. + */ +DebugCommand.next_seq_ = 0; + +/** + * Command messages to be sent to the debugger. + * @constructor + */ +function DebugCommand(str) { + this.command = undefined; + // first, strip off of the leading word as the command + var argv = str.split(' '); + this.user_command = argv.shift(); + // the rest of the string is argv to the command + str = argv.join(' '); + if (DebugCommand.aliases[this.user_command]) + this.user_command = DebugCommand.aliases[this.user_command]; + if (this.parseArgs_(str) == 1) + this.type = "request"; + if (this.command == undefined) + this.command = this.user_command; +}; + +// Mapping of some control characters to avoid the \uXXXX syntax for most +// commonly used control cahracters. +const ctrlCharMap_ = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' +}; + +// Regular expression matching ", \ and control characters (0x00 - 0x1F) +// globally. +const ctrlCharMatch_ = /["\\\\\x00-\x1F]/g; + +/** + * Convert a String to its JSON representation. + * @param {String} value - String to be converted + * @return {String} JSON formatted String + */ +DebugCommand.stringToJSON = function(value) { + // Check for" , \ and control characters (0x00 - 0x1F). + if (ctrlCharMatch_.test(value)) { + // Replace ", \ and control characters (0x00 - 0x1F). + return '"' + value.replace(ctrlCharMatch_, function (char) { + // Use charmap if possible. + var mapped = ctrlCharMap_[char]; + if (mapped) return mapped; + mapped = char.charCodeAt(); + // Convert control character to unicode escape sequence. + var dig1 = (Math.floor(mapped / 16)); + var dig2 = (mapped % 16) + return '\\u00' + dig1.toString(16) + dig2.toString(16); + }) + + '"'; + } + + // Simple string with no special characters. + return '"' + value + '"'; +}; + +/** + * @return {bool} True if x is an integer. + */ +DebugCommand.isInt = function(x) { + var y = parseInt(x); + if (isNaN(y)) + return false; + return x == y && x.toString() == y.toString(); +}; + +/** + * @return {float} log base 10 of num + */ +DebugCommand.log10 = function(num) { + return Math.log(num)/Math.log(10); +}; + +/** + * Take an object and encode it (non-recursively) as a JSON dict. + * @param {Object} obj - object to encode + */ +DebugCommand.toJSON = function(obj) { + // TODO(erikkay): use a real JSON library + var json = '{'; + for (var key in obj) { + if (json.length > 1) + json += ","; + var val = obj[key]; + if (!DebugCommand.isInt(val)) { + val = DebugCommand.stringToJSON(val.toString()); + } + json += '"' + key + '":' + val; + } + json += '}'; + return json; +}; + +/** + * Encode the DebugCommand object into the V8 debugger JSON protocol format. + * @see http://wiki/Main/V8Debugger + */ +DebugCommand.prototype.toJSONProtocol = function() { + // TODO(erikkay): use a real JSON library + var json = '{'; + json += '"seq":"' + this.seq; + json += '","type":"' + this.type; + json += '","command":"' + this.command + '"'; + if (this.arguments) { + json += ',"arguments":' + DebugCommand.toJSON(this.arguments); + } + json += '}' + return json; +} + +/** + * Encode the contents of this message and send it to the debugger. + * @param {Object} tab - tab being debugged. This is an internal + * Chrome object. + */ +DebugCommand.prototype.sendToDebugger = function(tab) { + this.seq = DebugCommand.next_seq_++; + str = this.toJSONProtocol(); + dprint("sending: " + str); + tab.sendToDebugger(str); +}; + +DebugCommand.trim = function(str) { + return str.replace(/^\s*/, '').replace(/\s*$/, ''); +}; + +/** + * Strip off a trailing parameter after a ':'. As the identifier for the + * source can contain ':' characters (e.g. 'http://www....) something after + * a ':' is only considered a parameter if it is numeric. + * @return {Array} two element array, the trimmed string and the parameter, + * or -1 if no parameter + */ +DebugCommand.stripTrailingParameter = function(str, opt_separator) { + var sep = opt_separator || ':'; + var index = str.lastIndexOf(sep); + // If a separator character if found strip if numeric. + if (index != -1) { + var value = parseInt(str.substring(index + 1, str.length), 10); + if (isNaN(value) || value < 0) { + return [str, -1]; + } + str = str.substring(0, index); + return [str, value]; + } + return [str, -1]; +}; + +/** + * Format source and location strings based on source location input data. + * @param {Object} script - script information object + * @param {String} source - source code for the current location + * @param {int} line - line number (0-based) + * @param {String} func - function name + * @return {array} [location(string), source line(string), line number(int)] + */ +DebugCommand.getSourceLocation = function(script, source, line, func) { + // source line is 0-based, we present as 1-based + line++; + + // TODO(erikkay): take column into account as well + if (source) + source = "" + line + ": " + source; + var location; + if (func) { + location = func + ", "; + } + location += script ? script.name : '[no source]'; + return [location, source, line]; +}; + +/** + * Aliases for debugger commands. + */ +DebugCommand.aliases = { + 'b': 'break', + 'bi': 'break_info', + 'br': 'break', + 'bt': 'backtrace', + 'c': 'continue', + 'f': 'frame', + 'h': 'help', + '?': 'help', + 'ls': 'source', + 'n': 'next', + 'p': 'print', + 's': 'step', + 'so': 'stepout', +}; + +/** + * Parses arguments to simple commands which have no arguments. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.parseSimpleCommand_ = function(str) { + return str.length ? -1 : 1; +}; + +/** + * Parses arguments to "args" and "locals" command, and initializes + * the underlying DebugCommand (which is a frame request). + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseArgsAndLocals_ = function(str) { + this.command = "frame"; + return str.length ? -1 : 1; +}; + +/** + * Parses arguments to "break_info" command, and executes it. + * "break_info" has an optional argument, which is the breakpoint + * identifier. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 0 for success + */ +DebugCommand.prototype.parseBreakInfo_ = function(str) { + this.type = "shell"; + + // Array of breakpoints to be printed by this command + // (default to all breakpoints) + var breakpointsToPrint = shell_.breakpoints; + + if (str.length > 0) { + // User specified an invalid breakpoint (not a number) + if (!str.match(/^\s*\d+\s*$/)) + return -1; // invalid usage + + // Check that the specified breakpoint identifier exists + var id = parseInt(str); + var info = shell_.breakpoints[id]; + if (!info) { + print("Error: Invalid breakpoint"); + return 0; // success (of sorts) + } + breakpointsToPrint = [info]; + } else { + // breakpointsToPrint.length isn't accurate, because of + // deletions + var num_breakpoints = 0; + for (var i in breakpointsToPrint) num_breakpoints++; + + print("Num breakpoints: " + num_breakpoints); + } + + DebugShell.printBreakpoints_(breakpointsToPrint); + + return 0; // success +} + +/** + * Parses arguments to "step" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseStep_ = function(str, opt_stepaction) { + this.command = "continue"; + action = opt_stepaction || "in"; + this.arguments = {"stepaction" : action} + if (str.length) { + count = parseInt(str); + if (count > 0) { + this.arguments["stepcount"] = count; + } else { + return -1; + } + } + return 1; +}; + +/** + * Parses arguments to "step" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseStepOut_ = function(str) { + return this.parseStep_(str, "out"); +}; + +/** + * Parses arguments to "next" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseNext_ = function(str) { + return this.parseStep_(str, "next"); +}; + +/** + * Parse the arguments to "print" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return 1 - always succeeds + */ +DebugCommand.prototype.parsePrint_ = function(str) { + this.command = "evaluate"; + this.arguments = { "expression" : str }; + // If the page is in the running state, then we force the expression to + // evaluate in the global context to avoid evaluating in a random context. + if (shell_.running) + this.arguments["global"] = true; + return 1; +}; + +/** + * Handle the response to a "print" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responsePrint_ = function(msg) { + body = msg["body"]; + if (body['text'] != undefined) { + print(body['text']); + } else { + // TODO(erikkay): is "text" ever not set? + print("can't print response"); + } +}; + +/** + * Parses arguments to "break" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success, 0 for handled internally + */ +DebugCommand.prototype.parseBreak_ = function(str) { + function stripTrailingParameter() { + var ret = DebugCommand.stripTrailingParameter(str, ':'); + str = ret[0]; + return ret[1]; + } + + if (str.length == 0) { + this.type = "shell"; + return 0; + } else { + var parts = str.split(/\s+/); + var condition = null; + if (parts.length > 1) { + str = parts.shift(); + condition = parts.join(" "); + } + + this.command = "setbreakpoint"; + + // Locate ...[:line[:column]] if present. + var line = -1; + var column = -1; + line = stripTrailingParameter(); + if (line != -1) { + line -= 1; + var l = stripTrailingParameter(); + if (l != -1) { + column = line; + line = l - 1; + } + } + + if (line == -1 && column == -1) { + this.arguments = { 'type' : 'function', + 'target' : str }; + } else { + var script = shell_.matchScript(str, line); + if (script) { + this.arguments = { 'type' : 'script', + 'target' : script.name }; + } else { + this.arguments = { 'type' : 'function', + 'target' : str }; + } + this.arguments.line = line; + if (column != -1) + this.arguments.position = column; + } + if (condition) + this.arguments.condition = condition; + } + return 1; +}; + +/** + * Handle the response to a "break" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseBreak_ = function(msg) { + var info = new BreakpointInfo( + parseInt(msg.body.breakpoint), + msg.command.arguments.type, + msg.command.arguments.target, + msg.command.arguments.line, + msg.command.arguments.position, + msg.command.arguments.condition); + shell_.addedBreakpoint(info); +}; + +/** + * Parses arguments to "backtrace" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ + DebugCommand.prototype.parseBacktrace_ = function(str) { + if (str.length > 0) { + var parts = str.split(/\s+/); + var non_empty_parts = parts.filter(function(s) { return s.length > 0; }); + // We need exactly two arguments. + if (non_empty_parts.length != 2) { + return -1; + } + var from = parseInt(non_empty_parts[0], 10); + var to = parseInt(non_empty_parts[1], 10); + // The two arguments have to be integers. + if (from != non_empty_parts[0] || to != non_empty_parts[1]) { + return -1; + } + this.arguments = { 'fromFrame': from, 'toFrame': to + 1 }; + } else { + // Default to fetching the first 10 frames. + this.arguments = { 'fromFrame': 0, 'toFrame': 10 }; + } + return 1; +}; + +/** + * Handle the response to a "backtrace" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseBacktrace_ = function(msg) { + body = msg["body"]; + if (body && body.totalFrames) { + print('Frames #' + body.fromFrame + ' to #' + (body.toFrame - 1) + + ' of ' + body.totalFrames + ":"); + for (var i = 0; i < body.frames.length; i++) { + print(body.frames[i].text); + } + } else { + print("unimplemented (sorry)"); + } +}; + + +/** + * Parses arguments to "clear" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseClearCommand_ = function(str) { + this.command = "clearbreakpoint"; + if (str.length > 0) { + var i = parseInt(str, 10); + if (i != str) { + return -1; + } + this.arguments = { 'breakpoint': i }; + } + return 1; +} + +/** + * Handle the response to a "clear" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseClear_ = function(msg) { + shell_.clearedBreakpoint(parseInt(msg.command.arguments.breakpoint)); +} + +/** + * Parses arguments to "frame" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseFrame_ = function(str) { + if (str.length > 0) { + var i = parseInt(str, 10); + if (i != str) { + return -1; + } + this.arguments = { 'number': i }; + } + return 1; +}; + +/** + * Handle the response to a "frame" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseFrame_ = function(msg) { + body = msg.body; + loc = DebugCommand.getSourceLocation(body.func.script, + body.sourceLineText, body.line, body.func.name); + print("#" + body.index + " " + loc[0]); + print(loc[1]); + shell_.current_frame = body.index; + shell_.current_line = loc[2]; + shell_.current_script = body.func.script; +}; + +/** + * Handle the response to a "args" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object (for "frame" command) + */ +DebugCommand.responseArgs_ = function(msg) { + DebugCommand.printVariables_(msg.body.arguments); +} + +/** + * Handle the response to a "locals" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object (for "frame" command) + */ +DebugCommand.responseLocals_ = function(msg) { + DebugCommand.printVariables_(msg.body.locals); +} + +DebugCommand.printVariables_ = function(variables) { + for (var i = 0; i < variables.length; i++) { + print(variables[i].name + " = " + + DebugCommand.toPreviewString_(variables[i].value)); + } +} + +DebugCommand.toPreviewString_ = function(value) { + // TODO(ericroman): pretty print arrays and objects, recursively. + // TODO(ericroman): truncate length of preview if too long? + if (value.type == "string") { + // Wrap the string in quote marks and JS-escape + return DebugCommand.stringToJSON(value.text); + } + return value.text; +} + +/** + * Parses arguments to "scripts" command. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseScripts_ = function(str) { + return 1 +}; + +/** + * Handle the response to a "scripts" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseScripts_ = function(msg) { + scripts = msg.body; + shell_.scripts = []; + for (var i in scripts) { + var script = scripts[i]; + + // Add this script to the internal list of scripts. + shell_.scripts.push(script); + + // Print result if this response was the result of a user command. + if (msg.command.from_user) { + var name = script.name; + if (name) { + if (script.lineOffset > 0) { + print(name + " (lines " + script.lineOffset + "-" + + (script.lineOffset + script.lineCount - 1) + ")"); + } else { + print(name + " (lines " + script.lineCount + ")"); + } + } else { + // For unnamed scripts (typically eval) display some source. + var sourceStart = script.sourceStart; + if (sourceStart.length > 40) + sourceStart = sourceStart.substring(0, 37) + '...'; + print("[unnamed] (source:\"" + sourceStart + "\")"); + } + } + } +}; + +/** + * Parses arguments to "source" command. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseSource_ = function(str) { + this.arguments = {}; + if (this.current_frame > 0) + this.arguments.frame = this.current_frame; + if (str.length) { + var args = str.split(" "); + if (args.length == 1) { + // with 1 argument n, we print 10 lines starting at n + var num = parseInt(args[0]); + if (num > 0) { + this.arguments.fromLine = num - 1; + this.arguments.toLine = this.arguments.fromLine + 10; + } else { + return -1; + } + } else if (args.length == 2) { + // with 2 arguments x and y, we print from line x to line x + y + var from = parseInt(args[0]); + var len = parseInt(args[1]); + if (from > 0 && len > 0) { + this.arguments.fromLine = from - 1; + this.arguments.toLine = this.arguments.fromLine + len; + } else { + return -1; + } + } else { + return -1; + } + if (this.arguments.fromLine < 0) + return -1; + if (this.arguments.toLine <= this.arguments.fromLine) + return -1; + } else if (shell_.current_line > 0) { + // with no arguments, we print 11 lines with the current line as the center + this.arguments.fromLine = + Math.max(0, shell_.current_line - 6); + this.arguments.toLine = this.arguments.fromLine + 11; + } + return 1; +}; + +/** + * Handle the response to a "source" command and display output to user. + * @see http://wiki/Main/V8Debugger + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseSource_ = function(msg) { + var body = msg.body; + var from_line = parseInt(body.fromLine) + 1; + var source = body.source; + var lines = source.split('\n'); + var maxdigits = 1 + Math.floor(DebugCommand.log10(from_line + lines.length)) + for (var num in lines) { + // there's an extra newline at the end + if (num >= (lines.length - 1) && lines[num].length == 0) + break; + spacer = maxdigits - (1 + Math.floor(DebugCommand.log10(from_line))) + var line = ""; + if (from_line == shell_.current_line) { + for (var i = 0; i < (maxdigits + 2); i++) + line += ">"; + } else { + for (var i = 0; i < spacer; i++) + line += " "; + line += from_line + ": "; + } + line += lines[num]; + print(line); + from_line++; + } +}; + +/** + * Parses arguments to "help" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return 0 for handled internally + */ +DebugCommand.parseHelp_ = function(str) { + DebugCommand.help(str); + return 0; +}; + +/** + * Takes argument and evaluates it in the context of the shell to allow commands + * to be escaped to the outer shell. Used primarily for development purposes. + * @see DebugCommand.commands + * @param {string} str The expression to be evaluated + * @return 0 for handled internally + */ +DebugCommand.parseShell_ = function(str) { + print(eval(str)); + return 0; +} + +DebugCommand.parseShellDebug_ = function(str) { + shell_.debug = !shell_.debug; + if (shell_.debug) { + print("shell debugging enabled"); + } else { + print("shell debugging disabled"); + } + return 0; +} + +/** + * Parses a user-entered command string. + * @param {string} str The arguments to be parsed. + */ +DebugCommand.prototype.parseArgs_ = function(str) { + if (str.length) + str = DebugCommand.trim(str); + var cmd = DebugCommand.commands[this.user_command]; + if (cmd) { + var parse = cmd['parse']; + if (parse == undefined) { + print('>>>can\'t find parse func for ' + this.user_command); + this.type = "error"; + } else { + var ret = parse.call(this, str); + if (ret > 0) { + this.type = "request"; + } else if (ret < 0) { + this.type = "handled"; + DebugCommand.help(this.user_command); + } + } + } else { + this.type = "handled"; + print('unknown command: ' + this.user_command); + DebugCommand.help(); + } +}; + +/** + * Displays command help or all help. + * @param {string} opt_str Which command to print help for. + */ +DebugCommand.help = function(opt_str) { + if (opt_str) { + var cmd = DebugCommand.commands[opt_str]; + var usage = cmd.usage; + print('usage: ' + usage); + // Print additional details for the command. + if (cmd.help) { + print(cmd.help); + } + } else { + if (shell_.running) { + print('Status: page is running'); + } else { + print('Status: page is paused'); + } + print('Available commands:'); + for (var key in DebugCommand.commands) { + var cmd = DebugCommand.commands[key]; + if (!cmd['hidden'] && (!shell_.running || cmd['while_running'])) { + var usage = cmd.usage; + print(' ' + usage); + } + } + } +}; + +/** + * Valid commands, their argument parser and their associated usage text. + */ +DebugCommand.commands = { + 'args': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, + 'usage': 'args', + 'help': 'summarize the arguments to the current function.', + 'response': DebugCommand.responseArgs_ }, + 'break': { 'parse': DebugCommand.prototype.parseBreak_, + 'response': DebugCommand.responseBreak_, + 'usage': 'break [location] <condition>', + 'help': 'location is one of <function> | <script:function> | <script:line> | <script:line:pos>', + 'while_running': true }, + 'break_info': { 'parse': DebugCommand.prototype.parseBreakInfo_, + 'usage': 'break_info [breakpoint #]', + 'help': 'list the current breakpoints, or the details on a single one', + 'while_running': true }, + 'backtrace': { 'parse': DebugCommand.prototype.parseBacktrace_, + 'response': DebugCommand.responseBacktrace_, + 'usage': 'backtrace [from frame #] [to frame #]' }, + 'clear': { 'parse': DebugCommand.prototype.parseClearCommand_, + 'response': DebugCommand.responseClear_, + 'usage': 'clear <breakpoint #>', + 'while_running': true }, + 'continue': { 'parse': DebugCommand.parseSimpleCommand_, + 'usage': 'continue' }, + 'frame': { 'parse': DebugCommand.prototype.parseFrame_, + 'response': DebugCommand.responseFrame_, + 'usage': 'frame <frame #>' }, + 'help': { 'parse': DebugCommand.parseHelp_, + 'usage': 'help [command]', + 'while_running': true }, + 'locals': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, + 'usage': 'locals', + 'help': 'summarize the local variables for current frame', + 'response': DebugCommand.responseLocals_ }, + 'next': { 'parse': DebugCommand.prototype.parseNext_, + 'usage': 'next' } , + 'print': { 'parse': DebugCommand.prototype.parsePrint_, + 'response': DebugCommand.responsePrint_, + 'usage': 'print <expression>', + 'while_running': true }, + 'scripts': { 'parse': DebugCommand.prototype.parseScripts_, + 'response': DebugCommand.responseScripts_, + 'usage': 'scripts', + 'while_running': true }, + 'source': { 'parse': DebugCommand.prototype.parseSource_, + 'response': DebugCommand.responseSource_, + 'usage': 'source [from line] | [<from line> <num lines>]' }, + 'step': { 'parse': DebugCommand.prototype.parseStep_, + 'usage': 'step' }, + 'stepout': { 'parse': DebugCommand.prototype.parseStepOut_, + 'usage': 'stepout' }, + // local eval for debugging - remove this later + 'shell': { 'parse': DebugCommand.parseShell_, + 'usage': 'shell <expression>', + 'while_running': true, + 'hidden': true }, + 'shelldebug': { 'parse': DebugCommand.parseShellDebug_, + 'usage': 'shelldebug', + 'while_running': true, + 'hidden': true }, +}; + + +/** + * Debug shell using the new JSON protocol + * @param {Object} tab - which tab is to be debugged. This is an internal + * Chrome object. + * @constructor + */ +function DebugShell(tab) { + this.tab = tab; + this.tab.attach(); + this.ready = true; + this.running = true; + this.current_command = undefined; + this.pending_commands = []; + this.debug = false; + this.last_msg = undefined; + this.last_command = undefined; + this.current_line = -1; + this.current_pos = -1; + this.current_frame = 0; + this.current_script = undefined; + this.scripts = []; + + // Mapping of breakpoints id --> info. + // Must use numeric keys. + this.breakpoints = []; +}; + +DebugShell.prototype.set_ready = function(ready) { + if (ready != this.ready) { + this.ready = ready; + chrome.setDebuggerReady(this.ready); + } +}; + +DebugShell.prototype.set_running = function(running) { + if (running != this.running) { + this.running = running; + chrome.setDebuggerBreak(!this.running); + } +}; + +/** + * Execute a constructed DebugCommand object if possible, otherwise pend. + * @param cmd {DebugCommand} - command to execute + */ +DebugShell.prototype.process_command = function(cmd) { + if (this.current_command) { + this.pending_commands.push(cmd); + dprint("pending command: " + DebugCommand.toJSON(cmd)); + } else if (cmd.type == "shell") { + if (cmd.user_command == "break") { + if (this.running) { + this.tab.debugBreak(); + this.set_ready(false); + } else { + print(">>already paused"); + } + } + this.last_command = cmd; + } else if (cmd.type == "request") { + // If the page is running, then the debugger isn't listening to certain + // requests. + var cmd_info = DebugCommand.commands[cmd.user_command]; + if (this.running && !cmd_info['while_running']) { + print(cmd.user_command + " can only be run while paused"); + } else { + this.current_command = cmd; + cmd.sendToDebugger(this.tab); + this.set_ready(false); + } + } + this.last_command = cmd; +}; + +/** + * Handle a break event from the debugger. + * @param msg {Object} - event protocol message to handle + */ +DebugShell.prototype.event_break = function(msg) { + this.current_frame = 0; + this.set_running(false); + this.set_ready(true); + if (msg.body) { + var body = msg.body; + this.current_script = body.script; + var loc = DebugCommand.getSourceLocation(body.script, + body.sourceLineText, body.sourceLine, body.invocationText); + var location = loc[0]; + var source = loc[1]; + this.current_line = loc[2]; + if (msg.body.breakpoints) { + var breakpoints = msg.body.breakpoints; + print("paused at breakpoint " + breakpoints.join(",") + ": " + + location); + for (var i = 0; i < breakpoints.length; i++) + this.didHitBreakpoint(parseInt(breakpoints[i])); + } else if (body.scriptData == "") { + print("paused"); + } else { + // step, stepout, next, "break" and a "debugger" line in the code + // are all treated the same (they're not really distinguishable anyway) + if (location != this.last_break_location) { + // We only print the location (function + script) when it changes, + // so as we step, you only see the source line when you transition + // to a new script and/or function. + print(location); + } + } + if (source) + print(source); + this.last_break_location = location; + } +}; + +/** + * Handle an exception event from the debugger. + * @param msg {Object} - event protocol message to handle + */ +DebugShell.prototype.event_exception = function(msg) { + this.set_running(false); + this.set_ready(true); + if (msg.body) { + if (msg.body["uncaught"]) { + print("uncaught exception " + msg.body["exception"].text); + } else { + print("paused at exception " + msg.body["exception"].text); + } + } +}; + +DebugShell.prototype.matchScript = function(script_match, line) { + var script = null; + // In the v8 debugger, all scripts have a name, line offset and line count + // Script names are usually URLs which are a pain to have to type again and + // again, so we match the tail end of the script name. This makes it easy + // to type break foo.js:23 rather than + // http://www.foo.com/bar/baz/quux/test/foo.js:23. In addition to the tail + // of the name we also look at the lines the script cover. If there are + // several scripts with the same tail including the requested line we match + // the first one encountered. + // TODO(sgjesse) Find how to handle several matching scripts. + var candidate_scripts = []; + for (var i in this.scripts) { + if (this.scripts[i].name && + this.scripts[i].name.indexOf(script_match) >= 0) { + candidate_scripts.push(this.scripts[i]); + } + } + for (var i in candidate_scripts) { + var s = candidate_scripts[i]; + var from = s.lineOffset; + var to = from + s.lineCount; + if (from <= line && line < to) { + script = s; + break; + } + } + if (script) + return script; + else + return null; +} + +// The Chrome Subshell interface requires: +// prompt(), command(), response(), exit() and on_disconnect() + +/** + * Called by Chrome Shell to get a prompt string to display. + */ +DebugShell.prototype.prompt = function() { + if (this.current_command) + return ''; + if (!this.running) + return 'v8(paused)> '; + else + return 'v8(running)> '; +}; + +/** + * Called by Chrome Shell when command input has been received from the user. + */ +DebugShell.prototype.command = function(str) { + if (this.tab) { + str = DebugCommand.trim(str); + if (str.length) { + var cmd = new DebugCommand(str); + cmd.from_user = true; + this.process_command(cmd); + } + } else { + print(">>not connected to a tab"); + } +}; + +/** + * Called by Chrome Shell when a response to a previous command has been + * received. + */ +DebugShell.prototype.response = function(str) { + var msg; + try { + dprint("received: " + str); + msg = eval('(' + str + ')'); + this.last_msg = msg; + } catch (error) { + print(error.toString(), str); + return; + } + if (msg.type == "event") { + ev = msg["event"] + if (ev == "break") { + this.event_break(msg); + } else if (ev == "exception") { + this.event_exception(msg); + } else if (ev == "attach") { + var title = this.tab.title; + if (!title) + title = "Untitled"; + print('attached to ' + title); + // on attach, we update our current script list + var cmd = new DebugCommand("scripts"); + cmd.from_user = false; + this.process_command(cmd); + } + } else if (msg.type == "response") { + if (msg.request_seq != undefined) { + if (!this.current_command || this.current_command.seq != msg.request_seq){ + throw("received response to unknown command " + DebugCommand.toJSON(msg)); + } + } else { + // TODO(erikkay): should we reject these when they happen? + print(">>no request_seq in response " + DebugCommand.toJSON(msg)); + } + var cmd = DebugCommand.commands[this.current_command.user_command] + msg.command = this.current_command; + this.current_command = null + if (msg.running != undefined) { + this.set_running(msg.running); + } + if (!msg['success']) { + print(msg['message']); + } else { + var response = cmd['response']; + if (response != undefined) { + response.call(this, msg); + } + } + this.set_ready(true); + if (this.pending_commands.length) { + this.process_command(this.pending_commands.shift()); + } + } +}; + +/** + * Called when a breakpoint has been set. + * @param {BreakpointInfo} info - details of breakpoint set. + */ +DebugShell.prototype.addedBreakpoint = function(info) { + print("set breakpoint #" + info.id); + this.breakpoints[info.id] = info; +} + +/** + * Called when a breakpoint has been cleared. + * @param {int} id - the breakpoint number that was cleared. + */ +DebugShell.prototype.clearedBreakpoint = function(id) { + assertIsNumberType(id, "clearedBreakpoint called with invalid id"); + + print("cleared breakpoint #" + id); + delete this.breakpoints[id]; +} + +/** + * Called when a breakpoint has been reached. + * @param {int} id - the breakpoint number that was hit. + */ +DebugShell.prototype.didHitBreakpoint = function(id) { + assertIsNumberType(id, "didHitBreakpoint called with invalid id"); + + var info = this.breakpoints[id]; + if (!info) + throw "Could not find breakpoint #" + id; + + info.hit_count ++; +} + +/** + * Print a summary of the specified breakpoints. + * + * @param {Array<BreakpointInfo>} breakpointsToPrint - List of breakpoints. The + * index is unused (id is determined from the info). + */ +DebugShell.printBreakpoints_ = function(breakpoints) { + // TODO(ericroman): this would look much nicer if we could output as an HTML + // table. I tried outputting as formatted text table, but this looks aweful + // once it triggers wrapping (which is very likely if the target is a script) + + // Output as a comma separated list of key=value + for (var i in breakpoints) { + var b = breakpoints[i]; + var props = ["id", "hit_count", "type", "target", "line", "position", + "condition"]; + var propertyList = []; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + var val = b[prop]; + if (val != undefined) + propertyList.push(prop + "=" + val); + } + print(propertyList.join(", ")); + } +} + +/** + * Called by Chrome Shell when the outer shell is detaching from debugging + * this tab. + */ +DebugShell.prototype.exit = function() { + if (this.tab) { + this.tab.detach(); + this.tab = null; + } +}; + +/** + * Called by the Chrome Shell when the tab that the shell is debugging + * went away. + */ +DebugShell.prototype.on_disconnect = function() { + print(">>lost connection to tab"); + this.tab = null; +}; + + +/** + * Structure that holds the details about a breakpoint. + * @constructor + * + * @param {int} id - breakpoint number + * @param {string} type - "script" or "function" + * @param {string} target - either a function name, or script url + * @param {int} line - line number in the script, or undefined + * @param {int} position - column in the script, or undefined + * @param {string} condition - boolean expression, or undefined + */ +function BreakpointInfo(id, type, target, line, position, condition) { + this.id = id; + this.type = type; + this.target = target; + + if (line != undefined) + this.line = line; + if (position != undefined) + this.position = position; + if (condition != undefined) + this.condition = condition; + + this.hit_count = 0; + + // Check that the id is numeric, otherwise will run into problems later + assertIsNumberType(this.id, "id is not a number"); +} + +/** + * Global function to enter the debugger using DebugShell. + * User can access this in the external shell by simply typing "debug()". + * This is called by the Chrome Shell when the shell attaches to a tab. + * @param {Object} opt_tab - which tab is to be debugged. This is an internal + * Chrome object. + */ +function debug(opt_tab) { + shell(new DebugShell(opt_tab || chrome.browser[0].tab[0])); +}; + +/** + * Print debugging message when DebugShell's debug flag is true. + */ +function dprint(str) { + if (shell_ && shell_.debug) { + print(str); + } +}; + +/** + * Helper that throws error if x is not a number + * @param x {object} - object to test type of + * @param error_message {string} - error to throw on failure + */ +function assertIsNumberType(x, error_message) { + if (typeof x != "number") + throw error_message; +} |