blob: 42484594d0891c288318e02834a82bf9d7065c10 [file] [log] [blame]
// 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:convert";
import "dart:io";
import "dart:async";
import "ddbg/lib/commando.dart";
class TargetIsolate {
int id;
// The location of the last paused event.
Map pausedLocation = null;
TargetIsolate(this.id);
bool get isPaused => pausedLocation != null;
}
Map<int, TargetIsolate> targetIsolates= new Map<int, TargetIsolate>();
Map<int, Completer> outstandingCommands;
Socket vmSock;
String vmData;
Commando cmdo;
var vmSubscription;
int seqNum = 0;
Process targetProcess;
final verbose = false;
final printMessages = false;
TargetIsolate currentIsolate;
TargetIsolate mainIsolate;
void printHelp() {
print("""
q Quit debugger shell
bt Show backtrace
r Resume execution
s Single step
so Step over
si Step into
sbp [<file>] <line> Set breakpoint
rbp <id> Remove breakpoint with given id
po <id> Print object info for given id
eval obj <id> <expr> Evaluate expr on object id
eval cls <id> <expr> Evaluate expr on class id
eval lib <id> <expr> Evaluate expr in toplevel of library id
pl <id> <idx> [<len>] Print list element/slice
pc <id> Print class info for given id
ll List loaded libraries
plib <id> Print library info for given library id
slib <id> <true|false> Set library id debuggable
pg <id> Print all global variables visible within given library id
ls <lib_id> List loaded scripts in library
gs <lib_id> <script_url> Get source text of script in library
tok <lib_id> <script_url> Get line and token table of script in library
epi <none|all|unhandled> Set exception pause info
li List ids of all isolates in the VM
sci <id> Set current target isolate
i <id> Interrupt execution of given isolate id
h Print help
""");
}
String formatLocation(Map location) {
if (location == null) return "";
var fileName = location["url"].split("/").last;
return "file: $fileName lib: ${location['libraryId']} token: ${location['tokenOffset']}";
}
void quitShell() {
vmSubscription.cancel();
vmSock.close();
cmdo.done();
}
Future sendCmd(Map<String, dynamic> cmd) {
var completer = new Completer.sync();
int id = cmd["id"];
outstandingCommands[id] = completer;
if (verbose) {
print("sending: '${JSON.encode(cmd)}'");
}
vmSock.write(JSON.encode(cmd));
return completer.future;
}
bool checkCurrentIsolate() {
if (currentIsolate != null) {
return true;
}
print("Need valid current isolate");
return false;
}
bool checkPaused() {
if (!checkCurrentIsolate()) return false;
if (currentIsolate.isPaused) return true;
print("Current isolate must be paused");
return false;
}
typedef void HandlerType(Map response);
HandlerType showPromptAfter(void handler(Map response)) {
// Hide the command prompt immediately.
return (response) {
handler(response);
cmdo.show();
};
}
void processCommand(String cmdLine) {
void huh() {
print("'$cmdLine' not understood, try h for help");
}
seqNum++;
cmdLine = cmdLine.trim();
var args = cmdLine.split(' ');
if (args.length == 0) {
return;
}
var command = args[0];
var resume_commands =
{ 'r':'resume', 's':'stepOver', 'si':'stepInto', 'so':'stepOut'};
if (resume_commands[command] != null) {
if (!checkPaused()) return;
var cmd = { "id": seqNum,
"command": resume_commands[command],
"params": { "isolateId" : currentIsolate.id } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleResumedResponse));
} else if (command == "bt") {
var cmd = { "id": seqNum,
"command": "getStackTrace",
"params": { "isolateId" : currentIsolate.id } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleStackTraceResponse));
} else if (command == "ll") {
var cmd = { "id": seqNum,
"command": "getLibraries",
"params": { "isolateId" : currentIsolate.id } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetLibraryResponse));
} else if (command == "sbp" && args.length >= 2) {
var url, line;
if (args.length == 2 && currentIsolate.pausedLocation != null) {
url = currentIsolate.pausedLocation["url"];
assert(url != null);
line = int.parse(args[1]);
} else {
url = args[1];
line = int.parse(args[2]);
}
var cmd = { "id": seqNum,
"command": "setBreakpoint",
"params": { "isolateId" : currentIsolate.id,
"url": url,
"line": line }};
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleSetBpResponse));
} else if (command == "rbp" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "removeBreakpoint",
"params": { "isolateId" : currentIsolate.id,
"breakpointId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command == "ls" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "getScriptURLs",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetScriptsResponse));
} else if (command == "eval" && args.length > 3) {
var expr = args.getRange(3, args.length).join(" ");
var target = args[1];
if (target == "obj") {
target = "objectId";
} else if (target == "cls") {
target = "classId";
} else if (target == "lib") {
target = "libraryId";
} else {
huh();
return;
}
var cmd = { "id": seqNum,
"command": "evaluateExpr",
"params": { "isolateId": currentIsolate.id,
target: int.parse(args[2]),
"expression": expr } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleEvalResponse));
} else if (command == "po" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "getObjectProperties",
"params": { "isolateId" : currentIsolate.id,
"objectId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetObjPropsResponse));
} else if (command == "pl" && args.length >= 3) {
var cmd;
if (args.length == 3) {
cmd = { "id": seqNum,
"command": "getListElements",
"params": { "isolateId" : currentIsolate.id,
"objectId": int.parse(args[1]),
"index": int.parse(args[2]) } };
} else {
cmd = { "id": seqNum,
"command": "getListElements",
"params": { "isolateId" : currentIsolate.id,
"objectId": int.parse(args[1]),
"index": int.parse(args[2]),
"length": int.parse(args[3]) } };
}
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetListResponse));
} else if (command == "pc" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "getClassProperties",
"params": { "isolateId" : currentIsolate.id,
"classId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetClassPropsResponse));
} else if (command == "plib" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "getLibraryProperties",
"params": {"isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetLibraryPropsResponse));
} else if (command == "slib" && args.length == 3) {
var cmd = { "id": seqNum,
"command": "setLibraryProperties",
"params": {"isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"debuggingEnabled": args[2] } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleSetLibraryPropsResponse));
} else if (command == "pg" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "getGlobalVariables",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetGlobalVarsResponse));
} else if (command == "gs" && args.length == 3) {
var cmd = { "id": seqNum,
"command": "getScriptSource",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"url": args[2] } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetSourceResponse));
} else if (command == "tok" && args.length == 3) {
var cmd = { "id": seqNum,
"command": "getLineNumberTable",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"url": args[2] } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetLineTableResponse));
} else if (command == "epi" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "setPauseOnException",
"params": { "isolateId" : currentIsolate.id,
"exceptions": args[1] } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command == "li") {
var cmd = { "id": seqNum, "command": "getIsolateIds" };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGetIsolatesResponse));
} else if (command == "sci" && args.length == 2) {
var id = int.parse(args[1]);
if (targetIsolates[id] != null) {
currentIsolate = targetIsolates[id];
print("Setting current target isolate to $id");
} else {
print("$id is not a valid isolate id");
}
} else if (command == "i" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "interrupt",
"params": { "isolateId": int.parse(args[1]) } };
cmdo.hide();
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command == "q") {
quitShell();
} else if (command == "h") {
printHelp();
} else {
huh();
}
}
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 if (kind == "function") {
var location = formatLocation(value['location']);
var name = value['name'];
var signature = value['signature'];
return "(closure ${name}${signature} $location)";
} else {
return "$text";
}
}
printNamedObject(obj) {
var name = obj["name"];
var value = obj["value"];
print(" $name = ${remoteObject(value)}");
}
handleGetObjPropsResponse(Map 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(Map 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(Map 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(Map 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(Map response) {
Map props = response["result"];
assert(props["debuggingEnabled"] != null);
print(" debugging enabled: ${props["debuggingEnabled"]}");
}
handleGetGlobalVarsResponse(Map response) {
List globals = response["result"]["globals"];
for (int i = 0; i < globals.length; i++) {
printNamedObject(globals[i]);
}
}
handleGetSourceResponse(Map response) {
Map result = response["result"];
String source = result["text"];
print("Source text:\n$source\n--------");
}
handleGetLineTableResponse(Map response) {
Map result = response["result"];
var info = result["lines"];
print("Line info table:\n$info");
}
void handleGetIsolatesResponse(Map response) {
Map result = response["result"];
List ids = result["isolateIds"];
assert(ids != null);
print("List of isolates:");
for (int id in ids) {
TargetIsolate isolate = targetIsolates[id];
var state = (isolate != null) ? "running" : "<unknown isolate>";
if (isolate != null && isolate.isPaused) {
var loc = formatLocation(isolate.pausedLocation);
state = "paused at $loc";
}
var marker = " ";
if (currentIsolate != null && id == currentIsolate.id) {
marker = "*";
}
print("$marker $id $state");
}
}
void handleGetLibraryResponse(Map 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(Map 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 handleEvalResponse(Map response) {
Map result = response["result"];
print(remoteObject(result));
}
void handleSetBpResponse(Map response) {
Map result = response["result"];
var id = result["breakpointId"];
assert(id != null);
print("Set BP $id");
}
void handleGenericResponse(Map response) {
if (response["error"] != null) {
print("Error: ${response["error"]}");
}
}
void handleResumedResponse(Map response) {
if (response["error"] != null) {
print("Error: ${response["error"]}");
return;
}
assert(currentIsolate != null);
currentIsolate.pausedLocation = null;
}
void handleStackTraceResponse(Map 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 loc = formatLocation(frame["location"]);
print("$frame_num $fname ($loc)");
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"];
int isolateId = msg["params"]["isolateId"];
assert(isolateId != null);
var isolate = targetIsolates[isolateId];
assert(isolate != null);
assert(!isolate.isPaused);
var location = msg["params"]["location"];;
assert(location != null);
isolate.pausedLocation = location;
if (reason == "breakpoint") {
print("Isolate $isolateId paused on breakpoint");
print("location: ${formatLocation(location)}");
} else if (reason == "interrupted") {
print("Isolate $isolateId paused due to an interrupt");
print("location: ${formatLocation(location)}");
} else {
assert(reason == "exception");
var excObj = msg["params"]["exception"];
print("Isolate $isolateId paused on exception");
print(remoteObject(excObj));
}
}
void handleIsolateEvent(msg) {
Map params = msg["params"];
assert(params != null);
var isolateId = params["id"];
var reason = params["reason"];
if (reason == "created") {
print("Isolate $isolateId has been created.");
assert(targetIsolates[isolateId] == null);
targetIsolates[isolateId] = new TargetIsolate(isolateId);
if (mainIsolate == null) {
mainIsolate = targetIsolates[isolateId];
currentIsolate = mainIsolate;
print("Current isolate set to ${currentIsolate.id}.");
}
} else {
assert(reason == "shutdown");
var isolate = targetIsolates.remove(isolateId);
assert(isolate != null);
if (isolate == mainIsolate) {
mainIsolate = null;
print("Main isolate ${isolate.id} has terminated.");
} else {
print("Isolate ${isolate.id} has terminated.");
}
if (isolate == currentIsolate) {
currentIsolate = mainIsolate;
if (currentIsolate == null && !targetIsolates.isEmpty) {
currentIsolate = targetIsolates.first;
}
if (currentIsolate != null) {
print("Setting current isolate to ${currentIsolate.id}.");
} else {
print("All isolates have terminated.");
}
}
}
}
void processVmMessage(String jsonString) {
var msg = JSON.decode(jsonString);
if (msg == null) {
return;
}
var event = msg["event"];
if (event == "isolate") {
cmdo.hide();
handleIsolateEvent(msg);
cmdo.show();
return;
}
if (event == "paused") {
cmdo.hide();
handlePausedEvent(msg);
cmdo.show();
return;
}
if (event == "breakpointResolved") {
Map params = msg["params"];
assert(params != null);
var isolateId = params["isolateId"];
var location = formatLocation(params["location"]);
cmdo.hide();
print("BP ${params["breakpointId"]} resolved in isolate $isolateId"
" at $location.");
cmdo.show();
return;
}
if (msg["id"] != null) {
var id = msg["id"];
if (outstandingCommands.containsKey(id)) {
var completer = outstandingCommands.remove(id);
if (msg["error"] != null) {
print("VM says: ${msg["error"]}");
// TODO(turnidge): Rework how hide/show happens. For now we
// show here explicitly.
cmdo.show();
} else {
completer.complete(msg);
}
}
}
}
bool haveGarbageVmData() {
if (vmData == null || vmData.length == 0) return false;
var i = 0, char = " ";
while (i < vmData.length) {
char = vmData[i];
if (char != " " && char != "\n" && char != "\r" && char != "\t") break;
i++;
}
if (i >= vmData.length) {
return false;
} else {
return char != "{";
}
}
void processVmData(String data) {
if (vmData == null || vmData.length == 0) {
vmData = data;
} else {
vmData = vmData + data;
}
if (haveGarbageVmData()) {
print("Error: have garbage data from VM: '$vmData'");
return;
}
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"); }
vmData = vmData.substring(msg_len);
if (haveGarbageVmData()) {
print("Error: garbage data after previous message: '$vmData'");
print("Previous message was: '$msg'");
return;
}
processVmMessage(msg);
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;
}
List<String> debuggerCommandCompleter(List<String> commandParts) {
List<String> completions = new List<String>();
// TODO(turnidge): Have a global command table and use it to for
// help messages, command completion, and command dispatching. For now
// we hardcode the list here.
//
// TODO(turnidge): Implement completion for arguments as well.
List<String> allCommands = ['q', 'bt', 'r', 's', 'so', 'si', 'sbp', 'rbp',
'po', 'eval', 'pl', 'pc', 'll', 'plib', 'slib',
'pg', 'ls', 'gs', 'tok', 'epi', 'li', 'i', 'h'];
// Completion of first word in the command.
if (commandParts.length == 1) {
String prefix = commandParts.last;
for (String command in allCommands) {
if (command.startsWith(prefix)) {
completions.add(command);
}
}
}
return completions;
}
void debuggerMain() {
outstandingCommands = new Map<int, Completer>();
Socket.connect("127.0.0.1", 5858).then((s) {
vmSock = s;
vmSock.setOption(SocketOption.TCP_NODELAY, true);
var stringStream = vmSock.transform(UTF8.decoder);
vmSubscription = stringStream.listen(
(String data) {
processVmData(data);
},
onDone: () {
print("VM debugger connection closed");
quitShell();
},
onError: (err) {
print("Error in debug connection: $err");
// TODO(floitsch): do we want to print the stack trace?
quitShell();
});
cmdo = new Commando(stdin, stdout, processCommand,
completer : debuggerCommandCompleter);
});
}
void main(List<String> args) {
if (args.length > 0) {
if (verbose) {
args = <String>['--debug', '--verbose_debug']..addAll(args);
} else {
args = <String>['--debug']..addAll(args);
}
Process.start(Platform.executable, args).then((Process process) {
targetProcess = process;
process.stdin.close();
// TODO(turnidge): For now we only show full lines of output
// from the debugged process. Should show each character.
process.stdout
.transform(UTF8.decoder)
.transform(new LineSplitter())
.listen((String line) {
// Hide/show command prompt across asynchronous output.
if (cmdo != null) {
cmdo.hide();
}
print("$line");
if (cmdo != null) {
cmdo.show();
}
});
process.exitCode.then((int exitCode) {
if (exitCode == 0) {
print('Program exited normally.');
} else {
print('Program exited with code $exitCode.');
}
});
debuggerMain();
});
} else {
debuggerMain();
}
}