// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // Simple interactive debugger shell that connects to the Dart VM's debugger // connection port. import "dart:io"; import "dart:json" as json; import "dart:utf"; import "dart:async"; Map outstandingCommands; Socket vmSock; String vmData; OutputStream vmStream; int seqNum = 0; int isolate_id = -1; bool verbose = false; // The current stack trace, while the VM is paused. It's a list // of activation frames. List stackTrace; // The current activation frame, while the VM is paused. Map curFrame; void printHelp() { print(""" q Quit debugger shell bt Show backtrace r Resume execution s Single step so Step over si Step into sbp [] Set breakpoint rbp Remove breakpoint with given id po Print object info for given id pl [] Print list element/slice pc Print class info for given id ll List loaded libraries plib Print library info for given library id slib Set library id debuggable pg Print all global variables visible within given library id ls List loaded scripts in library gs Get source text of script in library epi Set exception pause info i Interrupt execution of given isolate id h Print help """); } void quitShell() { vmStream.close(); vmSock.close(); stdin.close(); } Future sendCmd(Map cmd) { var completer = new Completer(); int id = cmd["id"]; outstandingCommands[id] = completer; if (verbose) { print("sending: '${json.stringify(cmd)}'"); } vmStream.writeString(json.stringify(cmd)); return completer.future; } void processCommand(String cmdLine) { seqNum++; var args = cmdLine.split(' '); if (args.length == 0) { return; } var command = args[0]; var simple_commands = { 'r':'resume', 's':'stepOver', 'si':'stepInto', 'so':'stepOut'}; if (simple_commands[command] != null) { var cmd = { "id": seqNum, "command": simple_commands[command], "params": { "isolateId" : isolate_id } }; sendCmd(cmd).then((result) => handleGenericResponse(result)); stackTrace = curFrame = null; } else if (command == "bt") { var cmd = { "id": seqNum, "command": "getStackTrace", "params": { "isolateId" : isolate_id } }; sendCmd(cmd).then((result) => handleStackTraceResponse(result)); } else if (command == "ll") { var cmd = { "id": seqNum, "command": "getLibraries", "params": { "isolateId" : isolate_id } }; sendCmd(cmd).then((result) => handleGetLibraryResponse(result)); } else if (command == "sbp" && args.length >= 2) { var url, line; if (args.length == 2) { url = stackTrace[0]["location"]["url"]; line = int.parse(args[1]); } else { url = args[1]; line = int.parse(args[2]); } var cmd = { "id": seqNum, "command": "setBreakpoint", "params": { "isolateId" : isolate_id, "url": url, "line": line }}; sendCmd(cmd).then((result) => handleSetBpResponse(result)); } else if (command == "rbp" && args.length == 2) { var cmd = { "id": seqNum, "command": "removeBreakpoint", "params": { "isolateId" : isolate_id, "breakpointId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGenericResponse(result)); } else if (command == "ls" && args.length == 2) { var cmd = { "id": seqNum, "command": "getScriptURLs", "params": { "isolateId" : isolate_id, "libraryId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGetScriptsResponse(result)); } else if (command == "po" && args.length == 2) { var cmd = { "id": seqNum, "command": "getObjectProperties", "params": { "isolateId" : isolate_id, "objectId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGetObjPropsResponse(result)); } else if (command == "pl" && args.length >= 3) { var cmd; if (args.length == 3) { cmd = { "id": seqNum, "command": "getListElements", "params": { "isolateId" : isolate_id, "objectId": int.parse(args[1]), "index": int.parse(args[2]) } }; } else { cmd = { "id": seqNum, "command": "getListElements", "params": { "isolateId" : isolate_id, "objectId": int.parse(args[1]), "index": int.parse(args[2]), "length": int.parse(args[3]) } }; } sendCmd(cmd).then((result) => handleGetListResponse(result)); } else if (command == "pc" && args.length == 2) { var cmd = { "id": seqNum, "command": "getClassProperties", "params": { "isolateId" : isolate_id, "classId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGetClassPropsResponse(result)); } else if (command == "plib" && args.length == 2) { var cmd = { "id": seqNum, "command": "getLibraryProperties", "params": {"isolateId" : isolate_id, "libraryId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGetLibraryPropsResponse(result)); } else if (command == "slib" && args.length == 3) { var cmd = { "id": seqNum, "command": "setLibraryProperties", "params": {"isolateId" : isolate_id, "libraryId": int.parse(args[1]), "debuggingEnabled": args[2] } }; sendCmd(cmd).then((result) => handleSetLibraryPropsResponse(result)); } else if (command == "pg" && args.length == 2) { var cmd = { "id": seqNum, "command": "getGlobalVariables", "params": { "isolateId" : isolate_id, "libraryId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGetGlobalVarsResponse(result)); } else if (command == "gs" && args.length == 3) { var cmd = { "id": seqNum, "command": "getScriptSource", "params": { "isolateId" : isolate_id, "libraryId": int.parse(args[1]), "url": args[2] } }; sendCmd(cmd).then((result) => handleGetSourceResponse(result)); } else if (command == "epi" && args.length == 2) { var cmd = { "id": seqNum, "command": "setPauseOnException", "params": { "isolateId" : isolate_id, "exceptions": args[1] } }; sendCmd(cmd).then((result) => handleGenericResponse(result)); } else if (command == "i" && args.length == 2) { var cmd = { "id": seqNum, "command": "interrupt", "params": { "isolateId": int.parse(args[1]) } }; sendCmd(cmd).then((result) => handleGenericResponse(result)); } else if (command == "q") { quitShell(); } else if (command == "h") { printHelp(); } else { print("command '$command' not understood, try h for help"); } } String remoteObject(value) { var kind = value["kind"]; var text = value["text"]; var id = value["objectId"]; if (kind == "string") { return "(string, id $id) '$text'"; } else if (kind == "list") { var len = value["length"]; return "(list, id $id, len $len) $text"; } else if (kind == "object") { return "(obj, id $id) $text"; } else { return "$text"; } } printNamedObject(obj) { var name = obj["name"]; var value = obj["value"]; print(" $name = ${remoteObject(value)}"); } handleGetObjPropsResponse(response) { Map props = response["result"]; int class_id = props["classId"]; if (class_id == -1) { print(" null"); return; } List fields = props["fields"]; print(" class id: $class_id"); for (int i = 0; i < fields.length; i++) { printNamedObject(fields[i]); } } handleGetListResponse(response) { Map result = response["result"]; if (result["elements"] != null) { // List slice. var index = result["index"]; var length = result["length"]; List elements = result["elements"]; assert(length == elements.length); for (int i = 0; i < length; i++) { var kind = elements[i]["kind"]; var text = elements[i]["text"]; print(" ${index + i}: ($kind) $text"); } } else { // One element, a remote object. print(result); print(" ${remoteObject(result)}"); } } handleGetClassPropsResponse(response) { Map props = response["result"]; assert(props["name"] != null); int libId = props["libraryId"]; assert(libId != null); print(" class ${props["name"]} (library id: $libId)"); List fields = props["fields"]; if (fields.length > 0) { print(" static fields:"); for (int i = 0; i < fields.length; i++) { printNamedObject(fields[i]); } } } handleGetLibraryPropsResponse(response) { Map props = response["result"]; assert(props["url"] != null); print(" library url: ${props["url"]}"); assert(props["debuggingEnabled"] != null); print(" debugging enabled: ${props["debuggingEnabled"]}"); List imports = props["imports"]; assert(imports != null); if (imports.length > 0) { print(" imports:"); for (int i = 0; i < imports.length; i++) { print(" id ${imports[i]["libraryId"]} prefix ${imports[i]["prefix"]}"); } } List globals = props["globals"]; assert(globals != null); if (globals.length > 0) { print(" global variables:"); for (int i = 0; i < globals.length; i++) { printNamedObject(globals[i]); } } } handleSetLibraryPropsResponse(response) { Map props = response["result"]; assert(props["debuggingEnabled"] != null); print(" debugging enabled: ${props["debuggingEnabled"]}"); } handleGetGlobalVarsResponse(response) { List globals = response["result"]["globals"]; for (int i = 0; i < globals.length; i++) { printNamedObject(globals[i]); } } handleGetSourceResponse(response) { Map result = response["result"]; String source = result["text"]; print("Source text:\n$source\n--------"); } void handleGetLibraryResponse(response) { Map result = response["result"]; List libs = result["libraries"]; print("Loaded libraries:"); print(libs); for (int i = 0; i < libs.length; i++) { print(" ${libs[i]["id"]} ${libs[i]["url"]}"); } } void handleGetScriptsResponse(response) { Map result = response["result"]; List urls = result["urls"]; print("Loaded scripts:"); for (int i = 0; i < urls.length; i++) { print(" $i ${urls[i]}"); } } void handleSetBpResponse(response) { Map result = response["result"]; var id = result["breakpointId"]; assert(id != null); print("Set BP $id"); } void handleGenericResponse(response) { if (response["error"] != null) { print("Error: ${response["error"]}"); } } void handleStackTraceResponse(response) { Map result = response["result"]; List callFrames = result["callFrames"]; assert(callFrames != null); printStackTrace(callFrames); } void printStackFrame(frame_num, Map frame) { var fname = frame["functionName"]; var libId = frame["libraryId"]; var url = frame["location"]["url"]; var line = frame["location"]["lineNumber"]; print("$frame_num $fname ($url:$line) (lib $libId)"); List locals = frame["locals"]; for (int i = 0; i < locals.length; i++) { printNamedObject(locals[i]); } } void printStackTrace(List frames) { for (int i = 0; i < frames.length; i++) { printStackFrame(i, frames[i]); } } void handlePausedEvent(msg) { assert(msg["params"] != null); var reason = msg["params"]["reason"]; isolate_id = msg["params"]["id"]; stackTrace = msg["params"]["callFrames"]; assert(stackTrace != null); assert(stackTrace.length >= 1); curFrame = stackTrace[0]; if (reason == "breakpoint") { print("Isolate $isolate_id paused on breakpoint"); } else if (reason == "interrupted") { print("Isolate $isolate_id paused due to an interrupt"); } else { assert(reason == "exception"); var excObj = msg["params"]["exception"]; print("Isolate $isolate_id paused on exception"); print(remoteObject(excObj)); } print("Stack trace:"); printStackTrace(stackTrace); } void processVmMessage(String jsonString) { var msg = json.parse(jsonString); if (msg == null) { return; } var event = msg["event"]; if (event == "paused") { handlePausedEvent(msg); return; } if (event == "breakpointResolved") { Map params = msg["params"]; assert(params != null); print("BP ${params["breakpointId"]} resolved and " "set at line ${params["line"]}."); return; } if (event == "isolate") { Map params = msg["params"]; assert(params != null); print("Isolate ${params["id"]} has been ${params["reason"]}."); return; } if (msg["id"] != null) { var id = msg["id"]; if (outstandingCommands.containsKey(id)) { if (msg["error"] != null) { print("VM says: ${msg["error"]}"); } else { var completer = outstandingCommands[id]; completer.complete(msg); } outstandingCommands.remove(id); } } } void processVmData(String data) { final printMessages = false; if (vmData == null || vmData.length == 0) { vmData = data; } else { vmData = vmData.concat(data); } int msg_len = jsonObjectLength(vmData); if (printMessages && msg_len == 0) { print("have partial or illegal json message" " of ${vmData.length} chars:\n'$vmData'"); return; } while (msg_len > 0 && msg_len <= vmData.length) { if (msg_len == vmData.length) { if (printMessages) { print("have one full message:\n$vmData"); } processVmMessage(vmData); vmData = null; return; } if (printMessages) { print("at least one message: '$vmData'"); } var msg = vmData.substring(0, msg_len); if (printMessages) { print("first message: $msg"); } processVmMessage(msg); vmData = vmData.substring(msg_len); msg_len = jsonObjectLength(vmData); } if (printMessages) { print("leftover vm data '$vmData'"); } } /** * Skip past a JSON object value. * The object value must start with '{' and continues to the * matching '}'. No attempt is made to otherwise validate the contents * as JSON. If it is invalid, a later [parseJson] will fail. */ int jsonObjectLength(String string) { int skipWhitespace(int index) { while (index < string.length) { String char = string[index]; if (char != " " && char != "\n" && char != "\r" && char != "\t") break; index++; } return index; } int skipString(int index) { assert(string[index - 1] == '"'); while (index < string.length) { String char = string[index]; if (char == '"') return index + 1; if (char == r'\') index++; if (index == string.length) return index; index++; } return index; } int index = 0; index = skipWhitespace(index); // Bail out if the first non-whitespace character isn't '{'. if (index == string.length || string[index] != '{') return 0; int nesting = 0; while (index < string.length) { String char = string[index++]; if (char == '{') { nesting++; } else if (char == '}') { nesting--; if (nesting == 0) return index; } else if (char == '"') { // Strings can contain braces. Skip their content. index = skipString(index); } } return 0; } void debuggerMain() { outstandingCommands = new Map(); vmSock = new Socket("127.0.0.1", 5858); vmStream = vmSock.outputStream; var stdinStream = new StringInputStream(stdin); stdinStream.onLine = () { processCommand(stdinStream.readLine()); }; var vmInStream = vmSock.inputStream; vmInStream.onData = () { String s = decodeUtf8(vmInStream.read()); processVmData(s); }; vmInStream.onError = (err) { print("Error in debug connection: $err"); quitShell(); }; vmInStream.onClosed = () { print("VM debugger connection closed"); quitShell(); }; } void main() { Options options = new Options(); List arguments = options.arguments; if (arguments.length > 0) { arguments = ['--debug', '--verbose_debug']..addAll(arguments); Process.start(options.executable, arguments).then((Process process) { process.onExit = (int exitCode) { print('${Strings.join(arguments, " ")} exited with $exitCode'); }; process.stdin.close(); // Redirecting both stdout and stderr of the child process to // stdout. This should help users keep track of which errors // are coming from the debugger, and which errors are coming // from the process being debugged. process.stderr.pipe(stdout); process.stdout.pipe(stdout); debuggerMain(); }); } else { debuggerMain(); } }