mirror of
https://github.com/dart-lang/sdk
synced 2024-09-21 12:31:20 +00:00
207292adb1
Notify the debugger of stack overflow errors after the stack has been unwound. No location information is sent with the pause event. R=devoncarew@google.com Review URL: https://codereview.chromium.org//382653002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@38125 260f80e4-7a28-3924-810f-c04153c831b5
1497 lines
41 KiB
Dart
1497 lines
41 KiB
Dart
// 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);
|
|
});
|
|
}
|