blob: ff51f9ba5c2ae5bfedb7324459bb7af176d7b503 [file] [log] [blame]
// Copyright (c) 2013, 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.
// Library used by debugger wire protocol tests (standalone VM debugging).
library DartDebugger;
import "dart:async";
import "dart:io";
import "dart:math";
import "dart:utf";
import "dart:json" as JSON;
// Whether or not to print the debugger wire messages on the console.
var verboseWire = false;
// The number of attempts made to find an unused debugger port.
var retries = 0;
// Class to buffer wire protocol data from debug target and
// break it down to individual json messages.
class JsonBuffer {
String buffer = null;
append(String s) {
if (buffer == null || buffer.length == 0) {
buffer = s;
} else {
buffer += s;
}
}
String getNextMessage() {
if (buffer == null) return null;
int msgLen = objectLength();
if (msgLen == 0) return null;
String msg = null;
if (msgLen == buffer.length) {
msg = buffer;
buffer = null;
} else {
assert(msgLen < buffer.length);
msg = buffer.substring(0, msgLen);
buffer = buffer.substring(msgLen);
}
return msg;
}
bool haveGarbage() {
if (buffer == null || buffer.length == 0) return false;
var i = 0, char = " ";
while (i < buffer.length) {
char = buffer[i];
if (char != " " && char != "\n" && char != "\r" && char != "\t") break;
i++;
}
if (i >= buffer.length) {
return false;
} else {
return char != "{";
}
}
// Returns the character length of the next json message in the
// buffer, or 0 if there is only a partial message in the buffer.
// 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 JSON.parse() will fail.
int objectLength() {
int skipWhitespace(int index) {
while (index < buffer.length) {
String char = buffer[index];
if (char != " " && char != "\n" && char != "\r" && char != "\t") break;
index++;
}
return index;
}
int skipString(int index) {
assert(buffer[index - 1] == '"');
while (index < buffer.length) {
String char = buffer[index];
if (char == '"') return index + 1;
if (char == r'\') index++;
if (index == buffer.length) return index;
index++;
}
return index;
}
int index = 0;
index = skipWhitespace(index);
// Bail out if the first non-whitespace character isn't '{'.
if (index == buffer.length || buffer[index] != '{') return 0;
int nesting = 0;
while (index < buffer.length) {
String char = buffer[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;
}
}
getJsonValue(Map jsonMsg, String path) {
List properties = path.split(new RegExp(":"));
assert(properties.length >= 1);
var node = jsonMsg;
for (int i = 0; i < properties.length; i++) {
if (node == null) return null;
String property = properties[i];
var index = null;
if (property.endsWith("]")) {
var bracketPos = property.lastIndexOf("[");
if (bracketPos <= 0) return null;
var indexStr = property.substring(bracketPos + 1, property.length - 1);
try {
index = int.parse(indexStr);
} on FormatException {
print("$indexStr is not a valid array index");
return null;
}
property = property.substring(0, bracketPos);
}
if (node is Map) {
node = node[property];
} else {
return null;
}
if (index != null) {
if (node is List && node.length > index) {
node = node[index];
} else {
return null;
}
}
}
return node;
}
// Returns true if [template] is a subset of [map].
bool matchMaps(Map template, Map msg) {
bool isMatch = true;
template.forEach((k, v) {
if (msg.containsKey(k)) {
var receivedValue = msg[k];
if ((v is Map) && (receivedValue is Map)) {
if (!matchMaps(v, receivedValue)) isMatch = false;
} else if (v == null) {
// null in the template matches everything.
} else if (v != receivedValue) {
isMatch = false;
}
} else {
isMatch = false;
}
});
return isMatch;
}
class Command {
var template;
void send(Debugger debugger) {
debugger.sendMessage(template);
}
void matchResponse(Debugger debugger) {
Map response = debugger.currentMessage;
var id = template["id"];
assert(id != null && id >= 0);
if (response["id"] != id) {
debugger.error("Expected messaged id $id but got ${response["id"]}.");
}
}
}
class FrameMatcher extends Command {
int frameIndex;
List<String> functionNames;
FrameMatcher(this.frameIndex, this.functionNames) {
template = {"id": 0, "command": "getStackTrace", "params": {"isolateId": 0}};
}
void matchResponse(Debugger debugger) {
super.matchResponse(debugger);
var msg = debugger.currentMessage;
List frames = getJsonValue(msg, "result:callFrames");
assert(frames != null);
if (debugger.scriptUrl == null) {
var name = frames[0]["functionName"];
if (name == "main") {
// Extract script url of debugged script.
debugger.scriptUrl = frames[0]["location"]["url"];
assert(debugger.scriptUrl != null);
}
}
if (frames.length < functionNames.length) {
debugger.error("stack trace not long enough "
"to match ${functionNames.length} frames");
return;
}
for (int i = 0; i < functionNames.length; i++) {
var idx = i + frameIndex;
var name = frames[idx]["functionName"];
assert(name != null);
if (name != functionNames[i]) {
debugger.error("call frame $idx: "
"expected function name '${functionNames[i]}' but found '$name'");
return;
}
}
}
}
MatchFrame(int frameIndex, String functionName) {
return new FrameMatcher(frameIndex, [ functionName ]);
}
MatchFrames(List<String> functionNames) {
return new FrameMatcher(0, functionNames);
}
class RunCommand extends Command {
RunCommand.resume() {
template = {"id": 0, "command": "resume", "params": {"isolateId": 0}};
}
RunCommand.step() {
template = {"id": 0, "command": "stepOver", "params": {"isolateId": 0}};
}
RunCommand.stepInto() {
template = {"id": 0, "command": "stepInto", "params": {"isolateId": 0}};
}
RunCommand.stepOut() {
template = {"id": 0, "command": "stepOut", "params": {"isolateId": 0}};
}
void send(Debugger debugger) {
debugger.sendMessage(template);
debugger.isPaused = false;
}
}
Resume() => new RunCommand.resume();
Step() => new RunCommand.step();
StepInto() => new RunCommand.stepInto();
StepOut() => new RunCommand.stepOut();
class SetBreakpointCommand extends Command {
int line;
SetBreakpointCommand(int this.line) {
template = {"id": 0,
"command": "setBreakpoint",
"params": { "isolateId": 0,
"url": null,
"line": null }};
}
void send(Debugger debugger) {
assert(debugger.scriptUrl != null);
template["params"]["url"] = debugger.scriptUrl;
template["params"]["line"] = line;
debugger.sendMessage(template);
}
}
SetBreakpoint(int line) => new SetBreakpointCommand(line);
// A debug script is a list of Command objects.
class DebugScript {
List entries;
DebugScript(List scriptEntries) {
entries = new List.from(scriptEntries.reversed);
entries.add(MatchFrame(0, "main"));
}
bool get isEmpty => entries.isEmpty;
get currentEntry => entries.last;
advance() => entries.removeLast();
add(entry) => entries.add(entry);
}
class Debugger {
// Debug target process properties.
Process targetProcess;
int portNumber;
Socket socket;
JsonBuffer responses = new JsonBuffer();
DebugScript script;
int seqNr = 0; // Sequence number of next debugger command message.
Command lastCommand = null; // Most recent command sent to target.
List<String> errors = new List();
// Data collected from debug target.
Map currentMessage = null; // Currently handled message sent by target.
String scriptUrl = null;
bool shutdownEventSeen = false;
int isolateId = 0;
bool isPaused = false;
Debugger(this.targetProcess, this.portNumber, this.script) {
stdin.listen((_) {});
var stdoutStringStream = targetProcess.stdout
.transform(new StringDecoder())
.transform(new LineTransformer());
stdoutStringStream.listen((line) {
if (line == "Debugger initialized") {
openConnection();
}
print("TARG: $line");
});
var stderrStringStream = targetProcess.stderr
.transform(new StringDecoder())
.transform(new LineTransformer());
stderrStringStream.listen((line) {
print("TARG: $line");
});
}
// Handle debugger events, updating the debugger state.
void handleEvent(Map<String,dynamic> msg) {
if (msg["event"] == "isolate") {
if (msg["params"]["reason"] == "created") {
isolateId = msg["params"]["id"];
assert(isolateId != null);
print("Debuggee isolate id $isolateId created.");
} else if (msg["params"]["reason"] == "shutdown") {
print("Debuggee isolate id ${msg["params"]["id"]} shut down.");
shutdownEventSeen = true;
if (!script.isEmpty) {
error("Premature isolate shutdown event seen.");
}
}
} else if (msg["event"] == "breakpointResolved") {
var bpId = msg["params"]["breakpointId"];
assert(bpId != null);
var isolateId = msg["params"]["isolateId"];
assert(isolateId != null);
var location = msg["params"]["location"];
assert(location != null);
print("Isolate $isolateId: breakpoint $bpId resolved"
" at location $location");
// We may want to maintain a table of breakpoints in the future.
} else if (msg["event"] == "paused") {
isPaused = true;
} else {
error("unknown debugger event received");
}
}
// Handle one JSON message object and match it to the
// expected events and responses in the debugging script.
void handleMessage(Map<String,dynamic> receivedMsg) {
currentMessage = receivedMsg;
if (receivedMsg["event"] != null) {
handleEvent(receivedMsg);
if (errorsDetected) {
error("Error while handling debugger event");
error("Event received from debug target: $receivedMsg");
}
} else if (receivedMsg["id"] != null) {
// This is a response to the last command we sent.
assert(lastCommand != null);
lastCommand.matchResponse(this);
lastCommand = null;
if (errorsDetected) {
error("Error while matching response to debugger command");
error("Response received from debug target: $receivedMsg");
}
}
}
// Send next debugger command in the script, if a response
// form the last command has been received and processed.
void sendNextCommand() {
if (lastCommand == null) {
if (script.currentEntry is Command) {
script.currentEntry.send(this);
lastCommand = script.currentEntry;
seqNr++;
script.advance();
}
}
}
// Handle data received over the wire from the debug target
// process. Split input from JSON wire format into individual
// message objects (maps).
void handleMessages() {
var msg = responses.getNextMessage();
while (msg != null) {
if (verboseWire) print("RECV: $msg");
if (responses.haveGarbage()) {
error("Error: leftover text after message: '${responses.buffer}'");
error("Previous message may be malformed, was: '$msg'");
close(killDebugee: true);
return;
}
var msgObj = JSON.parse(msg);
handleMessage(msgObj);
if (errorsDetected) {
error("Error while handling script entry");
error("Message received from debug target: $msg");
close(killDebugee: true);
return;
}
if (shutdownEventSeen) {
close();
return;
}
if (isPaused) sendNextCommand();
msg = responses.getNextMessage();
}
}
runScript(List entries) {
script = new DebugScript(entries);
openConnection();
}
// Send a debugger command to the target VM.
void sendMessage(Map<String,dynamic> msg) {
if (msg["id"] != null) {
msg["id"] = seqNr;
}
if (msg["params"] != null && msg["params"]["isolateId"] != null) {
msg["params"]["isolateId"] = isolateId;
}
String jsonMsg = JSON.stringify(msg);
if (verboseWire) print("SEND: $jsonMsg");
socket.write(jsonMsg);
}
bool get errorsDetected => errors.length > 0;
// Record error message.
void error(String s) {
errors.add(s);
}
void openConnection() {
Socket.connect("127.0.0.1", portNumber).then((s) {
this.socket = s;
var stringStream = socket.transform(new StringDecoder());
stringStream.listen((str) {
try {
responses.append(str);
handleMessages();
} catch(e, trace) {
print("Unexpected exception:\n$e\n$trace");
close(killDebugee: true);
}
},
onDone: () {
print("Connection closed by debug target");
close(killDebugee: true);
},
onError: (e) {
print("Error '$e' detected in input stream from debug target");
var trace = getAttachedStackTrace(e);
if (trace != null) print("StackTrace: $trace");
close(killDebugee: true);
});
},
onError: (e) {
String msg = "Error while connecting to debugee: $e";
var trace = getAttachedStackTrace(e);
if (trace != null) msg += "\nStackTrace: $trace";
error(msg);
close(killDebugee: true);
});
}
void close({killDebugee: false}) {
if (errorsDetected) {
for (int i = 0; i < errors.length; i++) print(errors[i]);
}
if (socket != null) socket.close();
if (killDebugee) {
targetProcess.kill();
print("Target process killed");
}
if (errorsDetected) throw "Errors detected";
exit(errors.length);
}
}
bool RunScript(List script) {
var options = new Options();
if (options.arguments.contains("--debuggee")) {
return false;
}
verboseWire = options.arguments.contains("--wire");
// Pick a port in the upper half of the port number range.
var seed = new DateTime.now().millisecondsSinceEpoch;
Random random = new Random(seed);
var debugPort = random.nextInt(32000) + 32000;
print('using debug port $debugPort ...');
ServerSocket.bind('127.0.0.1', debugPort).then((ServerSocket s) {
s.close();
var targetOpts = [ "--debug:$debugPort" ];
// --verbose_debug is necessary so the test knows when the debuggee
// is initialized.
targetOpts.add("--verbose_debug");
targetOpts.add(options.script);
targetOpts.add("--debuggee");
print('args: ${targetOpts.join(" ")}');
Process.start(options.executable, targetOpts).then((Process process) {
print("Debug target process started");
process.stdin.close();
process.exitCode.then((int exitCode) {
if (exitCode != 0) throw "bad exit code: $exitCode";
print("Debug target process exited with exit code $exitCode");
});
var debugger =
new Debugger(process, debugPort, new DebugScript(script));
});
},
onError: (e) {
if (++retries >= 3) {
print('unable to find unused port: $e');
var trace = getAttachedStackTrace(e);
if (trace != null) print("StackTrace: $trace");
return -1;
} else {
// Retry with another random port.
RunScript(script);
}
});
return true;
}