blob: 1d1199b670b7a0d8bb3faf5de661ed25f627c3c9 [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 "dart:math";
import "ddbg/lib/commando.dart";
class TargetScript {
// The text of a script.
String source = null;
// A mapping from line number to source text.
List<String> lineToSource = null;
// A mapping from token offset to line number.
Map<int,int> tokenToLine = null;
}
const UnknownLocation = const {};
class TargetIsolate {
int id;
// The location of the last paused event.
Map pausedLocation = null;
TargetIsolate(this.id);
bool get isPaused => pausedLocation != null;
String get pausedUrl => pausedLocation != null ? pausedLocation["url"] : null;
Map<String, TargetScript> scripts = {};
}
Map<int, TargetIsolate> targetIsolates= new Map<int, TargetIsolate>();
Map<int, Completer> outstandingCommands;
Socket vmSock;
String vmData;
var cmdSubscription;
Commando cmdo;
var vmSubscription;
int seqNum = 0;
bool isDebugging = false;
Process targetProcess = null;
bool suppressNextExitCode = false;
final verbose = false;
final printMessages = false;
TargetIsolate currentIsolate;
TargetIsolate mainIsolate;
int debugPort = 5858;
String formatLocation(Map location) {
if (location == null) return "";
var fileName = location["url"].split("/").last;
return "file: $fileName lib: ${location['libraryId']} token: ${location['tokenOffset']}";
}
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 (vmSock == null) {
print("There is no active script. Try 'help run'.");
return false;
}
if (currentIsolate == null) {
print('There is no current isolate.');
return false;
}
return true;
}
void setCurrentIsolate(TargetIsolate isolate) {
if (isolate != currentIsolate) {
currentIsolate = isolate;
if (mainIsolate == null) {
print("Main isolate is ${isolate.id}");
mainIsolate = isolate;
}
print("Current isolate is now ${isolate.id}");
}
}
bool checkPaused() {
if (!checkCurrentIsolate()) return false;
if (currentIsolate.isPaused) return true;
print("Current isolate must be paused");
return false;
}
// These settings are allowed in the 'set' and 'show' debugger commands.
var validSettings = ['vm', 'vmargs', 'script', 'args'];
// The current values for all settings.
var settings = new Map();
String _leftJustify(text, int width) {
StringBuffer buffer = new StringBuffer();
buffer.write(text);
while (buffer.length < width) {
buffer.write(' ');
}
return buffer.toString();
}
// TODO(turnidge): Move all commands here.
List<Command> commandList =
[ new HelpCommand(),
new QuitCommand(),
new RunCommand(),
new KillCommand(),
new ConnectCommand(),
new DisconnectCommand(),
new SetCommand(),
new ShowCommand() ];
List<Command> matchCommand(String commandName, bool exactMatchWins) {
List matches = [];
for (var command in commandList) {
if (command.name.startsWith(commandName)) {
if (exactMatchWins && command.name == commandName) {
// Exact match
return [command];
} else {
matches.add(command);
}
}
}
return matches;
}
abstract class Command {
String get name;
Future run(List<String> args);
}
class HelpCommand extends Command {
final name = 'help';
final helpShort = 'Show a list of debugger commands';
final helpLong ="""
Show a list of debugger commands or get more information about a
particular command.
Usage:
help
help <command>
""";
Future run(List<String> args) {
if (args.length == 1) {
print("Debugger commands:\n");
for (var command in commandList) {
print(' ${_leftJustify(command.name, 11)} ${command.helpShort}');
}
// TODO(turnidge): Convert all commands to use the Command class.
print("""
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 fr <n> <expr> Evaluate expr on stack frame index n
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
""");
print("For more information about a particular command, type:\n\n"
" help <command>\n");
print("Commands may be abbreviated: e.g. type 'h' for 'help.\n");
} else if (args.length == 2) {
var commandName = args[1];
var matches = matchCommand(commandName, true);
if (matches.length == 0) {
print("Command '$commandName' not recognized. "
"Try 'help' for a list of commands.");
} else {
for (var command in matches) {
print("---- ${command.name} ----\n${command.helpLong}");
}
}
} else {
print("Command '$command' not recognized. "
"Try 'help' for a list of commands.");
}
return new Future.value();
}
}
class QuitCommand extends Command {
final name = 'quit';
final helpShort = 'Quit the debugger.';
final helpLong ="""
Quit the debugger.
Usage:
quit
""";
Future run(List<String> args) {
if (args.length > 1) {
print("Unexpected arguments to $name command.");
return new Future.value();
}
return debuggerQuit();
}
}
class SetCommand extends Command {
final name = 'set';
final helpShort = 'Change the value of a debugger setting.';
final helpLong ="""
Change the value of a debugger setting.
Usage:
set <setting> <value>
Valid settings are:
${validSettings.join('\n ')}.
See also 'help show'.
""";
Future run(List<String> args) {
if (args.length < 3 || !validSettings.contains(args[1])) {
print("Undefined $name command. Try 'help $name'.");
return new Future.value();
}
var option = args[1];
var value = args.getRange(2, args.length).join(' ');
settings[option] = value;
return new Future.value();
}
}
class ShowCommand extends Command {
final name = 'show';
final helpShort = 'Show the current value of a debugger setting.';
final helpLong ="""
Show the current value of a debugger setting.
Usage:
show
show <setting>
If no <setting> is specified, all current settings are shown.
Valid settings are:
${validSettings.join('\n ')}.
See also 'help set'.
""";
Future run(List<String> args) {
if (args.length == 1) {
for (var option in validSettings) {
var value = settings[option];
print("$option = '$value'");
}
} else if (args.length == 2 && validSettings.contains(args[1])) {
var option = args[1];
var value = settings[option];
if (value == null) {
print('$option has not been set.');
} else {
print("$option = '$value'");
}
return new Future.value();
} else {
print("Undefined $name command. Try 'help $name'.");
}
return new Future.value();
}
}
class RunCommand extends Command {
final name = 'run';
final helpShort = "Run the currrent script.";
final helpLong ="""
Runs the current script.
Usage:
run
run <args>
The current script will be run on the current vm. The 'vm' and
'vmargs' settings are used to specify the current vm and vm arguments.
The 'script' and 'args' settings are used to specify the current
script and script arguments.
For more information on settings type 'help show' or 'help set'.
If <args> are provided to the run command, it is the same as typing
'set args <args>' followed by 'run'.
""";
Future run(List<String> cmdArgs) {
if (isDebugging) {
// TODO(turnidge): Implement modal y/n dialog to stop running script.
print("There is already a running dart process. "
"Try 'kill'.");
return new Future.value();
}
assert(targetProcess == null);
if (settings['script'] == null) {
print("There is no script specified. "
"Use 'set script' to set the current script.");
return new Future.value();
}
if (cmdArgs.length > 1) {
settings['args'] = cmdArgs.getRange(1, cmdArgs.length);
}
// Build the process arguments.
var processArgs = ['--debug:$debugPort'];
if (verbose) {
processArgs.add('--verbose_debug');
}
if (settings['vmargs'] != null) {
processArgs.addAll(settings['vmargs'].split(' '));
}
processArgs.add(settings['script']);
if (settings['args'] != null) {
processArgs.addAll(settings['args'].split(' '));
}
String vm = settings['vm'];
isDebugging = true;
cmdo.hide();
return Process.start(vm, processArgs).then((process) {
print("Started process ${process.pid} '$vm ${processArgs.join(' ')}'");
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) {
cmdo.hide();
// TODO(turnidge): Escape output in any way?
print(line);
cmdo.show();
});
process.stderr
.transform(UTF8.decoder)
.transform(new LineSplitter())
.listen((String line) {
cmdo.hide();
print(line);
cmdo.show();
});
process.exitCode.then((int exitCode) {
cmdo.hide();
if (suppressNextExitCode) {
suppressNextExitCode = false;
} else {
if (exitCode == 0) {
print('Process exited normally.');
} else {
print('Process exited with code $exitCode.');
}
}
targetProcess = null;
cmdo.show();
});
// Wait for the vm to open the debugging port.
return openVmSocket(0);
});
}
}
class KillCommand extends Command {
final name = 'kill';
final helpShort = 'Kill the currently executing script.';
final helpLong ="""
Kill the currently executing script.
Usage:
kill
""";
Future run(List<String> cmdArgs) {
if (!isDebugging) {
print('There is no running script.');
return new Future.value();
}
if (targetProcess == null) {
print("The active dart process was not started with 'run'. "
"Try 'disconnect' instead.");
return new Future.value();
}
assert(targetProcess != null);
bool result = targetProcess.kill();
if (result) {
print('Process killed.');
suppressNextExitCode = true;
} else {
print('Unable to kill process ${targetProcess.pid}');
}
return new Future.value();
}
}
class ConnectCommand extends Command {
final name = 'connect';
final helpShort = "Connect to a running dart script.";
final helpLong ="""
Connect to a running dart script.
Usage:
connect
connect <port>
The debugger will connect to a dart script which has already been
started with the --debug option. If no port is provided, the debugger
will attempt to connect on the default debugger port.
""";
Future run(List<String> cmdArgs) {
if (cmdArgs.length > 2) {
print("Too many arguments to 'connect'.");
}
if (isDebugging) {
// TODO(turnidge): Implement modal y/n dialog to stop running script.
print("There is already a running dart process. "
"Try 'kill'.");
return new Future.value();
}
assert(targetProcess == null);
if (cmdArgs.length == 2) {
debugPort = int.parse(cmdArgs[1]);
}
isDebugging = true;
cmdo.hide();
return openVmSocket(0);
}
}
class DisconnectCommand extends Command {
final name = 'disconnect';
final helpShort = "Disconnect from a running dart script.";
final helpLong ="""
Disconnect from a running dart script.
Usage:
disconnect
The debugger will disconnect from a dart script's debugging port. The
script must have been connected to earlier with the 'connect' command.
""";
Future run(List<String> cmdArgs) {
if (cmdArgs.length > 1) {
print("Too many arguments to 'disconnect'.");
}
if (!isDebugging) {
// TODO(turnidge): Implement modal y/n dialog to stop running script.
print("There is no active dart process. "
"Try 'connect'.");
return new Future.value();
}
if (targetProcess != null) {
print("The active dart process was started with 'run'. "
"Try 'kill'.");
}
cmdo.hide();
return closeVmSocket();
}
}
typedef void HandlerType(Map response);
HandlerType showPromptAfter(void handler(Map response)) {
return (response) {
handler(response);
cmdo.show();
};
}
void processCommand(String cmdLine) {
void huh() {
print("'$cmdLine' not understood, try 'help' for help.");
}
cmdo.hide();
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()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": resume_commands[command],
"params": { "isolateId" : currentIsolate.id } };
sendCmd(cmd).then(showPromptAfter(handleResumedResponse));
} else if (command == "bt") {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getStackTrace",
"params": { "isolateId" : currentIsolate.id } };
sendCmd(cmd).then(showPromptAfter(handleStackTraceResponse));
} else if (command == "ll") {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getLibraries",
"params": { "isolateId" : currentIsolate.id } };
sendCmd(cmd).then(showPromptAfter(handleGetLibraryResponse));
} else if (command == "sbp" && args.length >= 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var url, line;
if (args.length == 2 && currentIsolate.pausedUrl != null) {
url = currentIsolate.pausedUrl;
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 }};
sendCmd(cmd).then(showPromptAfter(handleSetBpResponse));
} else if (command == "rbp" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "removeBreakpoint",
"params": { "isolateId" : currentIsolate.id,
"breakpointId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command == "ls" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getScriptURLs",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGetScriptsResponse));
} else if (command == "eval" && args.length > 3) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
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 if (target == "fr") {
target = "frameId";
} else {
huh();
return;
}
var cmd = { "id": seqNum,
"command": "evaluateExpr",
"params": { "isolateId": currentIsolate.id,
target: int.parse(args[2]),
"expression": expr } };
sendCmd(cmd).then(showPromptAfter(handleEvalResponse));
} else if (command == "po" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getObjectProperties",
"params": { "isolateId" : currentIsolate.id,
"objectId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGetObjPropsResponse));
} else if (command == "pl" && args.length >= 3) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
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]) } };
}
sendCmd(cmd).then(showPromptAfter(handleGetListResponse));
} else if (command == "pc" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getClassProperties",
"params": { "isolateId" : currentIsolate.id,
"classId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGetClassPropsResponse));
} else if (command == "plib" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getLibraryProperties",
"params": {"isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGetLibraryPropsResponse));
} else if (command == "slib" && args.length == 3) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "setLibraryProperties",
"params": {"isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"debuggingEnabled": args[2] } };
sendCmd(cmd).then(showPromptAfter(handleSetLibraryPropsResponse));
} else if (command == "pg" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getGlobalVariables",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGetGlobalVarsResponse));
} else if (command == "gs" && args.length == 3) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getScriptSource",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"url": args[2] } };
sendCmd(cmd).then(showPromptAfter(handleGetSourceResponse));
} else if (command == "tok" && args.length == 3) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "getLineNumberTable",
"params": { "isolateId" : currentIsolate.id,
"libraryId": int.parse(args[1]),
"url": args[2] } };
sendCmd(cmd).then(showPromptAfter(handleGetLineTableResponse));
} else if (command == "epi" && args.length == 2) {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum,
"command": "setPauseOnException",
"params": { "isolateId" : currentIsolate.id,
"exceptions": args[1] } };
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command == "li") {
if (!checkCurrentIsolate()) {
cmdo.show();
return;
}
var cmd = { "id": seqNum, "command": "getIsolateIds" };
sendCmd(cmd).then(showPromptAfter(handleGetIsolatesResponse));
} else if (command == "sci" && args.length == 2) {
var id = int.parse(args[1]);
if (targetIsolates[id] != null) {
setCurrentIsolate(targetIsolates[id]);
} else {
print("$id is not a valid isolate id");
}
cmdo.show();
} else if (command == "i" && args.length == 2) {
var cmd = { "id": seqNum,
"command": "interrupt",
"params": { "isolateId": int.parse(args[1]) } };
sendCmd(cmd).then(showPromptAfter(handleGenericResponse));
} else if (command.length == 0) {
huh();
cmdo.show();
} else {
// TODO(turnidge): Use this for all commands.
var matches = matchCommand(command, true);
if (matches.length == 0) {
huh();
cmdo.show();
} else if (matches.length == 1) {
matches[0].run(args).then((_) {
cmdo.show();
});
} else {
var matchNames = matches.map((handler) => handler.name);
print("Ambigous command '$command' : ${matchNames.toList()}");
cmdo.show();
}
}
}
void processError(error, trace) {
cmdo.hide();
print("\nInternal error:\n$error\n$trace");
cmdo.show();
}
void processDone() {
debuggerQuit();
}
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("#${_leftJustify(frame_num,2)} $fname at $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]);
}
}
Map<int, int> parseLineNumberTable(List<List<int>> table) {
Map tokenToLine = {};
for (var line in table) {
// Each entry begins with a line number...
var lineNumber = line[0];
for (var pos = 1; pos < line.length; pos += 2) {
// ...and is followed by (token offset, col number) pairs.
// We ignore the column numbers.
var tokenOffset = line[pos];
tokenToLine[tokenOffset] = lineNumber;
}
}
return tokenToLine;
}
Future<TargetScript> getTargetScript(Map location) {
var isolate = targetIsolates[currentIsolate.id];
var url = location['url'];
var script = isolate.scripts[url];
if (script != null) {
return new Future.value(script);
}
script = new TargetScript();
// Ask the vm for the source and line number table.
var sourceCmd = {
"id": seqNum++,
"command": "getScriptSource",
"params": { "isolateId": currentIsolate.id,
"libraryId": location['libraryId'],
"url": url } };
var lineNumberCmd = {
"id": seqNum++,
"command": "getLineNumberTable",
"params": { "isolateId": currentIsolate.id,
"libraryId": location['libraryId'],
"url": url } };
// Send the source command
var sourceResponse = sendCmd(sourceCmd).then((response) {
Map result = response["result"];
script.source = result['text'];
// Line numbers are 1-based so add a dummy for line 0.
script.lineToSource = [''];
script.lineToSource.addAll(script.source.split('\n'));
});
// Send the line numbers command
var lineNumberResponse = sendCmd(lineNumberCmd).then((response) {
Map result = response["result"];
script.tokenToLine = parseLineNumberTable(result['lines']);
});
return Future.wait([sourceResponse, lineNumberResponse]).then((_) {
// When both commands complete, cache the result.
isolate.scripts[url] = script;
return script;
});
}
Future printLocation(String label, Map location) {
// Figure out the line number.
return getTargetScript(location).then((script) {
var lineNumber = script.tokenToLine[location['tokenOffset']];
var text = script.lineToSource[lineNumber];
if (label != null) {
var fileName = location['url'].split("/").last;
print("$label \n"
" at $fileName:$lineNumber");
}
print("${_leftJustify(lineNumber, 8)}$text");
});
}
Future 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"];;
setCurrentIsolate(isolate);
isolate.pausedLocation = (location == null) ? UnknownLocation : location;
if (reason == "breakpoint") {
assert(location != null);
var bpId = (msg["params"]["breakpointId"]);
var label = (bpId != null) ? "Breakpoint $bpId" : null;
return printLocation(label, location);
} else if (reason == "interrupted") {
assert(location != null);
return printLocation("Interrupted", location);
} else {
assert(reason == "exception");
var excObj = msg["params"]["exception"];
print("Isolate $isolateId paused on exception");
print(remoteObject(excObj));
return new Future.value();
}
}
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);
} 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.values.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).then((_) {
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("Breakpoint ${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> oldCommands = ['bt', 'r', 's', 'so', 'si', 'sbp', 'rbp',
'po', 'eval', 'pl', 'pc', 'll', 'plib', 'slib',
'pg', 'ls', 'gs', 'tok', 'epi', 'li', 'i' ];
// Completion of first word in the command.
if (commandParts.length == 1) {
String prefix = commandParts.last;
for (var command in oldCommands) {
if (command.startsWith(prefix)) {
completions.add(command);
}
}
for (var command in commandList) {
if (command.name.startsWith(prefix)) {
completions.add(command.name);
}
}
}
return completions;
}
Future closeCommando() {
var subscription = cmdSubscription;
cmdSubscription = null;
cmdo = null;
var future = subscription.cancel();
if (future != null) {
return future;
} else {
return new Future.value();
}
}
Future openVmSocket(int attempt) {
return Socket.connect("127.0.0.1", debugPort).then(
setupVmSocket,
onError: (e) {
// We were unable to connect to the debugger's port. Try again.
retryOpenVmSocket(e, attempt);
});
}
void setupVmSocket(Socket s) {
vmSock = s;
vmSock.setOption(SocketOption.TCP_NODELAY, true);
var stringStream = vmSock.transform(UTF8.decoder);
outstandingCommands = new Map<int, Completer>();
vmSubscription = stringStream.listen(
(String data) {
processVmData(data);
},
onDone: () {
cmdo.hide();
if (verbose) {
print("VM debugger connection closed");
}
closeVmSocket().then((_) {
cmdo.show();
});
},
onError: (err) {
cmdo.hide();
// TODO(floitsch): do we want to print the stack trace?
print("Error in debug connection: $err");
// TODO(turnidge): Kill the debugged process here?
closeVmSocket().then((_) {
cmdo.show();
});
});
}
Future retryOpenVmSocket(error, int attempt) {
var delay;
if (attempt < 10) {
delay = new Duration(milliseconds:10);
} else if (attempt < 20) {
delay = new Duration(seconds:1);
} else {
// Too many retries. Give up.
//
// TODO(turnidge): Kill the debugged process here?
print('Timed out waiting for debugger to start.\nError: $e');
return closeVmSocket();
}
// Wait and retry.
return new Future.delayed(delay, () {
openVmSocket(attempt + 1);
});
}
Future closeVmSocket() {
if (vmSubscription == null) {
// Already closed, nothing to do.
assert(vmSock == null);
return new Future.value();
}
isDebugging = false;
var subscription = vmSubscription;
var sock = vmSock;
// Wait for the socket to close and the subscription to be
// cancelled. Perhaps overkill, but it means we know these will be
// done.
//
// This is uglier than it needs to be since cancel can return null.
var cleanupFutures = [sock.close()];
var future = subscription.cancel();
if (future != null) {
cleanupFutures.add(future);
}
vmSubscription = null;
vmSock = null;
outstandingCommands = null;
return Future.wait(cleanupFutures);
}
void debuggerError(self, parent, zone, error, StackTrace trace) {
print('\n--------\nExiting due to unexpected error:\n'
' $error\n$trace\n');
debuggerQuit();
}
Future debuggerQuit() {
// Kill target process, if any.
if (targetProcess != null) {
if (!targetProcess.kill()) {
print('Unable to kill process ${targetProcess.pid}');
}
}
// Restore terminal settings, close connections.
return Future.wait([closeCommando(), closeVmSocket()]).then((_) {
exit(0);
// Unreachable.
return new Future.value();
});
}
void parseArgs(List<String> args) {
int pos = 0;
settings['vm'] = Platform.executable;
while (pos < args.length && args[pos].startsWith('-')) {
pos++;
}
if (pos < args.length) {
settings['vmargs'] = args.getRange(0, pos).join(' ');
settings['script'] = args[pos];
settings['args'] = args.getRange(pos + 1, args.length).join(' ');
}
}
void main(List<String> args) {
// Setup a zone which will exit the debugger cleanly on any uncaught
// exception.
var zone = Zone.ROOT.fork(specification:new ZoneSpecification(
handleUncaughtError: debuggerError));
zone.run(() {
parseArgs(args);
cmdo = new Commando(completer: debuggerCommandCompleter);
cmdSubscription = cmdo.commands.listen(processCommand,
onError: processError,
onDone: processDone);
});
}