Files
sdk/tests/standalone/debugger/debug_lib.dart
T
ager@google.com 33be759e00 Fix debugger test.
1. Fix JSON import.

2. Add exit code check to catch such errors in the future.

3. Only kill the debugee on errors. If there are no errors
   it will die on its own with exit code 0. If we kill it
   explicitly it can get killed (and give exit code != 0)
   before it has the chance to exit on its own.

R=sgjesse@google.com,tball@google.com,hausner@google.com
BUG=

Review URL: https://codereview.chromium.org//12086080

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@17889 260f80e4-7a28-3924-810f-c04153c831b5
2013-01-31 09:09:05 +00:00

534 lines
15 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.
// Library used by debugger wire protocol tests (standalone VM debugging).
library DartDebugger;
import "dart:io";
import "dart:utf";
import "dart:json" as JSON;
// TODO(hausner): need to select a different port number for each
// test that runs in parallel.
var debugPort = 5860;
// Whether or not to print debug target process on the console.
var showDebuggeeOutput = true;
// Whether or not to print the debugger wire messages on the console.
var verboseWire = false;
// 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 = buffer.concat(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;
}
// Returns the character length of the newxt 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 BreakpointEvent {
String functionName;
var template = { "event": "paused", "params": { "reason": "breakpoint" }};
BreakpointEvent({String function: null}) {
functionName = function;
}
void match(Debugger debugger) {
var msg = debugger.currentMessage;
if (!matchMaps(template, msg)) {
debugger.error("message does not match $template");
}
var name = getJsonValue(msg, "params:callFrames[0]:functionName");
if (name == "main") {
// Extract script url of debugged script.
var scriptUrl = getJsonValue(msg, "params:callFrames[0]:location:url");
assert(scriptUrl != null);
debugger.scriptUrl = scriptUrl;
}
if (functionName != null) {
var name = getJsonValue(msg, "params:callFrames[0]:functionName");
if (functionName != name) {
debugger.error("expected function name $functionName but got $name");
}
}
}
}
Breakpoint({String function}) {
return new BreakpointEvent(function: function);
}
class Matcher {
void match(Debugger debugger);
}
class FrameMatcher extends Matcher {
int frameIndex;
List<String> functionNames;
FrameMatcher(this.frameIndex, this.functionNames);
void match(Debugger debugger) {
var msg = debugger.currentMessage;
List frames = getJsonValue(msg, "params:callFrames");
assert(frames != 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 property = "params:callFrames[$idx]:functionName";
var name = getJsonValue(msg, property);
if (name == null) {
debugger.error("property '$property' not found");
return;
}
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 Command {
var template;
Command();
Command.resume() {
template = {"id": 0, "command": "resume", "params": {"isolateId": 0}};
}
Command.step() {
template = {"id": 0, "command": "stepOver", "params": {"isolateId": 0}};
}
Map makeMsg(int cmdId, int isolateId) {
template["id"] = cmdId;
if ((template["params"] != null)
&& (template["params"]["isolateId"] != null)) {
template["params"]["isolateId"] = isolateId;
}
return template;
}
void send(Debugger debugger) {
template["id"] = debugger.seqNr;
template["params"]["isolateId"] = debugger.isolateId;
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"]}.");
}
}
}
Resume() => new Command.resume();
Step() => new Command.step();
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;
super.send(debugger);
}
}
SetBreakpoint(int line) => new SetBreakpointCommand(line);
// A debug script is a list of Event, Matcher and Command objects.
class DebugScript {
List entries;
int currentIndex;
DebugScript(List this.entries) : currentIndex = 0;
get currentEntry {
if (currentIndex < entries.length) return entries[currentIndex];
return null;
}
advance() {
currentIndex++;
}
}
class Debugger {
// Debug target process properties.
Process targetProcess;
int portNumber;
Socket socket;
OutputStream to;
StringInputStream from;
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;
Debugger(this.targetProcess, this.portNumber) {
var targetStdout = new StringInputStream(targetProcess.stdout);
targetStdout.onLine = () {
var s = targetStdout.readLine();
if (showDebuggeeOutput) {
print("TARG: $s");
}
};
var targetStderr = new StringInputStream(targetProcess.stderr);
targetStderr.onLine = () {
var s = targetStderr.readLine();
if (showDebuggeeOutput) {
print("TARG: $s");
}
};
}
// Handle debugger events for which there is no explicit
// entry in the debug script, for example isolate create and
// shutdown events, breakpoint resolution events, etc.
bool handleImplicitEvents(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.currentEntry != null) {
error("Premature isolate shutdown event seen.");
}
}
return true;
} else if (msg["event"] == "breakpointResolved") {
// Ignore the event. We may want to maintain a table of
// breakpoints in the future.
return true;
}
return false;
}
// 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;
var isHandled = handleImplicitEvents(receivedMsg);
if (isHandled) return;
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");
}
return;
}
// This message must be an event that is expected by the script.
assert(receivedMsg["event"] != null);
if ((script.currentEntry == null) || (script.currentEntry is Command)) {
// Error: unexpected event received.
error("unexpected event received: $receivedMsg");
return;
} else {
// Match received message with expected event.
script.currentEntry.match(this);
if (errorsDetected) return;
script.advance();
while (script.currentEntry is Matcher) {
script.currentEntry.match(this);
if (errorsDetected) return;
script.advance();
}
}
}
// 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");
var msgObj = JSON.parse(msg);
handleMessage(msgObj);
if (errorsDetected) {
error("Error while handling script entry ${script.currentIndex}");
error("Message received from debug target: $msg");
close(killDebugee: true);
return;
}
if (shutdownEventSeen) {
close();
return;
}
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) {
String jsonMsg = JSON.stringify(msg);
if (verboseWire) print("SEND: $jsonMsg");
to.writeString(jsonMsg, Encoding.UTF_8);
}
bool get errorsDetected => errors.length > 0;
// Record error message.
void error(String s) {
errors.add(s);
}
void openConnection() {
socket = new Socket("127.0.0.1", portNumber);
to = socket.outputStream;
from = new StringInputStream(socket.inputStream, Encoding.UTF_8);
from.onData = () {
try {
responses.append(from.read());
handleMessages();
} catch(e, trace) {
print("Unexpected exception:\n$e\n$trace");
close(killDebugee: true);
}
};
from.onClosed = () {
print("Connection closed by debug target");
close(killDebugee: true);
};
from.onError = (e) {
print("Error '$e' detected in input stream from debug target");
close(killDebugee: true);
};
}
void close({killDebugee: false}) {
if (errorsDetected) {
for (int i = 0; i < errors.length; i++) print(errors[i]);
}
to.close();
socket.close();
if (killDebugee) {
targetProcess.kill();
print("Target process killed");
}
Expect.isTrue(!errorsDetected);
stdin.close();
stdout.close();
stderr.close();
}
}
bool RunScript(List script) {
var options = new Options();
if (options.arguments.contains("--debuggee")) {
return false;
}
showDebuggeeOutput = options.arguments.contains("--verbose");
verboseWire = options.arguments.contains("--wire");
var targetOpts = [ "--debug:$debugPort" ];
if (showDebuggeeOutput) targetOpts.add("--verbose_debug");
targetOpts.add(options.script);
targetOpts.add("--debuggee");
Process.start(options.executable, targetOpts).then((Process process) {
print("Debug target process started");
process.stdin.close();
process.stdout.onData = process.stdout.read;
process.stderr.onData = process.stderr.read;
process.onExit = (int exitCode) {
Expect.equals(0, exitCode);
print("Debug target process exited with exit code $exitCode");
};
var debugger = new Debugger(process, debugPort);
stdin.onClosed = () => debugger.close(killDebugee: true);
stdin.onError = (error) => debugger.close(killDebugee: true);
debugger.runScript(script);
});
return true;
}