Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 // Library used by debugger wire protocol tests (standalone VM debugging). | |
| 6 | |
| 7 library DartDebugger; | |
| 8 | |
| 9 import "dart:io"; | |
| 10 import "dart:utf"; | |
| 11 import "dart:json"; | |
| 12 | |
| 13 // TODO(hausner): need to select a different port number for each | |
| 14 // test that runs in parallel. | |
| 15 var debugPort = 5860; | |
| 16 | |
| 17 // Whether or not to print debug target process on the console. | |
| 18 var showDebuggeeOutput = true; | |
| 19 | |
| 20 // Whether or not to print the debugger wire messages on the console. | |
| 21 var verboseWire = false; | |
| 22 | |
| 23 // Class to buffer wire protocol data from debug target and | |
| 24 // break it down to individual json messages. | |
| 25 class JsonBuffer { | |
| 26 String buffer = null; | |
| 27 | |
| 28 append(String s) { | |
| 29 if (buffer == null || buffer.length == 0) { | |
| 30 buffer = s; | |
| 31 } else { | |
| 32 buffer = buffer.concat(s); | |
| 33 } | |
| 34 } | |
| 35 | |
| 36 String getNextMessage() { | |
| 37 if (buffer == null) return null; | |
| 38 int msgLen = objectLength(); | |
| 39 if (msgLen == 0) return null; | |
| 40 String msg = null; | |
| 41 if (msgLen == buffer.length) { | |
| 42 msg = buffer; | |
| 43 buffer = null; | |
| 44 } else { | |
| 45 msg = buffer.substring(0, msgLen); | |
| 46 buffer = buffer.substring(msgLen); | |
| 47 } | |
| 48 return msg; | |
| 49 } | |
| 50 | |
| 51 // Skip past a JSON object value. | |
| 52 // The object value must start with '{' and continues to the | |
|
siva
2012/12/19 18:28:58
and continue to the
| |
| 53 // matching '}'. No attempt is made to otherwise validate the contents | |
| 54 // as JSON. If it is invalid, a later JSON.parse() will fail. | |
| 55 int objectLength() { | |
| 56 int skipWhitespace(int index) { | |
| 57 while (index < buffer.length) { | |
| 58 String char = buffer[index]; | |
| 59 if (char != " " && char != "\n" && char != "\r" && char != "\t") break; | |
| 60 index++; | |
| 61 } | |
| 62 return index; | |
| 63 } | |
| 64 int skipString(int index) { | |
| 65 assert(buffer[index - 1] == '"'); | |
| 66 while (index < buffer.length) { | |
| 67 String char = buffer[index]; | |
| 68 if (char == '"') return index + 1; | |
| 69 if (char == r'\') index++; | |
| 70 if (index == buffer.length) return index; | |
| 71 index++; | |
| 72 } | |
| 73 return index; | |
| 74 } | |
| 75 int index = 0; | |
| 76 index = skipWhitespace(index); | |
| 77 // Bail out if the first non-whitespace character isn't '{'. | |
| 78 if (index == buffer.length || buffer[index] != '{') return 0; | |
| 79 int nexting = 0; | |
| 80 while (index < buffer.length) { | |
| 81 String char = buffer[index++]; | |
| 82 if (char == '{') { | |
| 83 nexting++; | |
| 84 } else if (char == '}') { | |
| 85 nexting--; | |
| 86 if (nexting == 0) return index; | |
| 87 } else if (char == '"') { | |
| 88 // Strings can contain braces. Skip their content. | |
| 89 index = skipString(index); | |
| 90 } | |
| 91 } | |
| 92 return 0; | |
| 93 } | |
| 94 } | |
| 95 | |
| 96 | |
| 97 getJsonValue(Map jsonMsg, String path) { | |
| 98 List properties = path.split(new RegExp(":")); | |
| 99 assert(properties.length >= 1); | |
| 100 var node = jsonMsg; | |
| 101 for (int i = 0; i < properties.length; i++) { | |
| 102 if (node == null) return null; | |
| 103 String property = properties[i]; | |
| 104 var index = null; | |
| 105 if (property.endsWith("]")) { | |
| 106 var bracketPos = property.lastIndexOf("["); | |
| 107 if (bracketPos <= 0) return null; | |
| 108 var indexStr = property.substring(bracketPos + 1, property.length - 1); | |
| 109 try { | |
| 110 index = int.parse(indexStr); | |
| 111 } on FormatException { | |
| 112 print("$indexStr is not a valid array index"); | |
| 113 return null; | |
| 114 } | |
| 115 property = property.substring(0, bracketPos); | |
| 116 } | |
| 117 if (node is Map) { | |
| 118 node = node[property]; | |
| 119 } else { | |
| 120 return null; | |
| 121 } | |
| 122 if (index != null) { | |
| 123 if (node is List && node.length > index) { | |
| 124 node = node[index]; | |
| 125 } else { | |
| 126 return null; | |
| 127 } | |
| 128 } | |
| 129 } | |
| 130 return node; | |
| 131 } | |
| 132 | |
| 133 | |
| 134 // Returns true if [template] is a subset of [map]. | |
| 135 bool matchMaps(Map template, Map msg) { | |
| 136 bool isMatch = true; | |
| 137 template.forEach((k, v) { | |
| 138 if (msg.containsKey(k)) { | |
| 139 var receivedValue = msg[k]; | |
| 140 if ((v is Map) && (receivedValue is Map)) { | |
| 141 if (!matchMaps(v, receivedValue)) isMatch = false; | |
| 142 } else if (v == null) { | |
| 143 // null in the template matches everything. | |
| 144 } else if (v != receivedValue) { | |
| 145 isMatch = false; | |
| 146 } | |
| 147 } else { | |
| 148 isMatch = false; | |
| 149 } | |
| 150 }); | |
| 151 return isMatch; | |
| 152 } | |
| 153 | |
| 154 | |
| 155 class BreakpointEvent { | |
| 156 String functionName; | |
| 157 var template = { "event": "paused", "params": { "reason": "breakpoint" }}; | |
| 158 | |
| 159 BreakpointEvent({String function: null}) { | |
| 160 functionName = function; | |
| 161 } | |
| 162 | |
| 163 void match(Debugger debugger) { | |
| 164 var msg = debugger.currentMessage; | |
| 165 if (!matchMaps(template, msg)) debugger.error("message does not match $templ ate"); | |
| 166 var name = getJsonValue(msg, "params:callFrames[0]:functionName"); | |
| 167 if (name == "main") { | |
| 168 // Extract script url of debugged script. | |
| 169 var scriptUrl = getJsonValue(msg, "params:callFrames[0]:location:url"); | |
| 170 assert(scriptUrl != null); | |
| 171 debugger.scriptUrl = scriptUrl; | |
| 172 } | |
| 173 if (functionName != null) { | |
| 174 var name = getJsonValue(msg, "params:callFrames[0]:functionName"); | |
| 175 if (functionName != name) { | |
| 176 debugger.error("expected function name $functionName but got $name"); | |
| 177 } | |
| 178 } | |
| 179 } | |
| 180 } | |
| 181 | |
| 182 Breakpoint({String function}) { | |
| 183 return new BreakpointEvent(function: function); | |
| 184 } | |
| 185 | |
| 186 class Matcher { | |
| 187 void match(Debugger debugger); | |
| 188 } | |
| 189 | |
| 190 class FrameMatcher extends Matcher { | |
| 191 int frameIndex; | |
| 192 List<String> functionNames; | |
| 193 | |
| 194 FrameMatcher(this.frameIndex, this.functionNames); | |
| 195 | |
| 196 void match(Debugger debugger) { | |
| 197 var msg = debugger.currentMessage; | |
| 198 List frames = getJsonValue(msg, "params:callFrames"); | |
| 199 assert(frames != null); | |
| 200 if (frames.length < functionNames.length) { | |
| 201 debugger.error("stack trace not long enough to match ${functionNames.lengt h} frames"); | |
| 202 return; | |
| 203 } | |
| 204 for (int i = 0; i < functionNames.length; i++) { | |
|
siva
2012/12/19 18:28:58
You seem to use var for all local variables always
| |
| 205 var idx = i + frameIndex; | |
| 206 var property = "params:callFrames[$idx]:functionName"; | |
| 207 var name = getJsonValue(msg, property); | |
| 208 if (name == null) { | |
| 209 debugger.error("property '$property' not found"); | |
| 210 return; | |
| 211 } | |
| 212 if (name != functionNames[i]) { | |
| 213 debugger.error("call frame $idx: " | |
| 214 "expected function name '${functionNames[i]}' but found '$name'"); | |
| 215 return; | |
| 216 } | |
| 217 } | |
| 218 } | |
| 219 } | |
| 220 | |
| 221 | |
| 222 MatchFrame(int frameIndex, String functionName) { | |
| 223 return new FrameMatcher(frameIndex, [ functionName ]); | |
| 224 } | |
| 225 | |
| 226 MatchFrames(List<String> functionNames) { | |
| 227 return new FrameMatcher(0, functionNames); | |
| 228 } | |
| 229 | |
| 230 | |
| 231 class Command { | |
| 232 var template; | |
| 233 Command(); | |
| 234 Command.resume() { | |
| 235 template = {"id": 0, "command": "resume", "params": {"isolateId": 0}}; | |
| 236 } | |
| 237 Command.step() { | |
| 238 template = {"id": 0, "command": "stepOver", "params": {"isolateId": 0}}; | |
| 239 } | |
| 240 Map makeMsg(int cmdId, int isolateId) { | |
| 241 template["id"] = cmdId; | |
| 242 if ((template["params"] != null) && (template["params"]["isolateId"] != null )) { | |
| 243 template["params"]["isolateId"] = isolateId; | |
| 244 } | |
| 245 return template; | |
| 246 } | |
| 247 | |
| 248 void send(Debugger debugger) { | |
| 249 template["id"] = debugger.seqNr; | |
| 250 template["params"]["isolateId"] = debugger.isolateId; | |
| 251 debugger.sendMessage(template); | |
| 252 } | |
| 253 | |
| 254 void matchResponse(Debugger debugger) { | |
| 255 Map response = debugger.currentMessage; | |
| 256 var id = template["id"]; | |
| 257 assert(id != null && id >= 0); | |
| 258 if (response["id"] != id) { | |
| 259 debugger.error("Expected messaged id $id but got ${response["id"]}."); | |
| 260 } | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 Resume() => new Command.resume(); | |
| 265 Step() => new Command.step(); | |
| 266 | |
| 267 class SetBreakpointCommand extends Command { | |
| 268 int line; | |
| 269 SetBreakpointCommand(int this.line) { | |
| 270 template = {"id": 0, | |
| 271 "command": "setBreakpoint", | |
| 272 "params": { "isolateId": 0, | |
| 273 "url": null, | |
| 274 "line": null }}; | |
| 275 } | |
| 276 void send(Debugger debugger) { | |
| 277 assert(debugger.scriptUrl != null); | |
| 278 template["params"]["url"] = debugger.scriptUrl; | |
| 279 template["params"]["line"] = line; | |
| 280 super.send(debugger); | |
| 281 } | |
| 282 } | |
| 283 | |
| 284 SetBreakpoint(int line) => new SetBreakpointCommand(line); | |
| 285 | |
| 286 | |
| 287 // A debug script is a list of Event, Matcher and Command objects. | |
| 288 class DebugScript { | |
| 289 List entries; | |
| 290 int currentIndex; | |
| 291 DebugScript(List this.entries) : currentIndex = 0; | |
| 292 get currentEntry { | |
| 293 if (currentIndex < entries.length) return entries[currentIndex]; | |
| 294 return null; | |
| 295 } | |
| 296 advance() { | |
| 297 currentIndex++; | |
| 298 } | |
| 299 } | |
| 300 | |
| 301 | |
| 302 class Debugger { | |
| 303 // Debug target process properties. | |
| 304 Process targetProcess; | |
| 305 int portNumber; | |
| 306 Socket socket; | |
| 307 OutputStream to; | |
| 308 StringInputStream from; | |
| 309 JsonBuffer responses = new JsonBuffer(); | |
| 310 | |
| 311 DebugScript script; | |
| 312 int seqNr = 0; // Sequence number of next debugger command message. | |
| 313 Command lastCommand = null; // Most recent command sent to target. | |
| 314 List<String> errors = new List(); | |
| 315 | |
| 316 // Data collected from debug target. | |
| 317 Map currentMessage = null; // Currently handled message sent by target. | |
| 318 String scriptUrl = null; | |
| 319 bool shutdownEventSeen = false; | |
| 320 int isolateId = 0; | |
| 321 | |
| 322 Debugger(this.targetProcess, this.portNumber) { | |
| 323 var targetStdout = new StringInputStream(targetProcess.stdout); | |
| 324 targetStdout.onLine = () { | |
| 325 var s = targetStdout.readLine(); | |
| 326 if (showDebuggeeOutput) { | |
| 327 print("TARG: $s"); | |
| 328 } | |
| 329 }; | |
| 330 var targetStderr = new StringInputStream(targetProcess.stderr); | |
| 331 targetStderr.onLine = () { | |
| 332 var s = targetStderr.readLine(); | |
| 333 if (showDebuggeeOutput) { | |
| 334 print("TARG: $s"); | |
| 335 } | |
| 336 }; | |
| 337 } | |
| 338 | |
| 339 // Handle debugger events for which there is no explicit | |
| 340 // entry in the debug script, for example isolate create and | |
| 341 // shutdown events, breakpoint resolution events, etc. | |
| 342 bool handleImplicitEvents(Map<String,dynamic> msg) { | |
| 343 if (msg["event"] == "isolate") { | |
| 344 if (msg["params"]["reason"] == "created") { | |
| 345 isolateId = msg["params"]["id"]; | |
| 346 assert(isolateId != null); | |
| 347 print("Debuggee isolate id $isolateId created."); | |
| 348 } else if (msg["params"]["reason"] == "shutdown") { | |
| 349 print("Debuggee isolate id ${msg["params"]["id"]} shut down."); | |
| 350 shutdownEventSeen = true; | |
| 351 } | |
| 352 return true; | |
| 353 } else if (msg["event"] == "breakpointResolved") { | |
| 354 // Ignore the event. We may want to maintain a table of | |
| 355 // breakpoints in the future. | |
| 356 return true; | |
| 357 } | |
| 358 return false; | |
| 359 } | |
| 360 | |
| 361 // Handle one JSON message object and match it to the | |
| 362 // expected events and responses in the debugging script. | |
| 363 void handleMessage(Map<String,dynamic> receivedMsg) { | |
| 364 currentMessage = receivedMsg; | |
| 365 var isHandled = handleImplicitEvents(receivedMsg); | |
| 366 if (isHandled) return; | |
| 367 | |
| 368 if (receivedMsg["id"] != null) { | |
| 369 // This is a response to the last command we sent. | |
| 370 assert(lastCommand != null); | |
| 371 lastCommand.matchResponse(this); | |
| 372 lastCommand = null; | |
| 373 if (errorsDetected) { | |
| 374 error("Error while matching response to debugger command"); | |
| 375 error("Response received from debug target: $receivedMsg"); | |
| 376 } | |
| 377 return; | |
| 378 } | |
| 379 | |
| 380 // This message must be an event that is expected by the script. | |
| 381 assert(receivedMsg["event"] != null); | |
| 382 if ((script.currentEntry == null) || (script.currentEntry is Command)) { | |
| 383 // Error: unexpected event received. | |
| 384 error("unexpected event received: $receivedMsg"); | |
| 385 return; | |
| 386 } else { | |
| 387 // Match received message with expected event. | |
| 388 script.currentEntry.match(this); | |
| 389 if (errorsDetected) return; | |
| 390 script.advance(); | |
| 391 while (script.currentEntry is Matcher) { | |
| 392 script.currentEntry.match(this); | |
| 393 if (errorsDetected) return; | |
| 394 script.advance(); | |
| 395 } | |
| 396 } | |
| 397 } | |
| 398 | |
| 399 // Send next debugger command in the script, if a response | |
| 400 // form the last command has been received and processed. | |
|
siva
2012/12/19 18:28:58
from
| |
| 401 void sendNextCommand() { | |
| 402 if (lastCommand == null) { | |
| 403 if (script.currentEntry is Command) { | |
| 404 script.currentEntry.send(this); | |
| 405 lastCommand = script.currentEntry; | |
| 406 seqNr++; | |
| 407 script.advance(); | |
| 408 } | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 // Handle data received over the wire from the debug target | |
| 413 // process. Split input from JSON wire format into individual | |
| 414 // message objects (maps). | |
| 415 void handleMessages() { | |
| 416 var msg = responses.getNextMessage(); | |
| 417 while (msg != null) { | |
| 418 if (verboseWire) print("RECV: $msg"); | |
| 419 var msgObj = JSON.parse(msg); | |
| 420 handleMessage(msgObj); | |
| 421 if (errorsDetected) { | |
| 422 error("Error while handling script entry ${script.currentIndex}"); | |
| 423 error("Message received from debug target: $msg"); | |
| 424 close(); | |
| 425 return; | |
| 426 } | |
| 427 if (shutdownEventSeen) { | |
| 428 close(); | |
| 429 return; | |
| 430 } | |
| 431 sendNextCommand(); | |
|
siva
2012/12/19 18:28:58
I am wondering if you would need to drain all the
| |
| 432 msg = responses.getNextMessage(); | |
| 433 } | |
| 434 } | |
| 435 | |
| 436 runScript(List entries) { | |
| 437 script = new DebugScript(entries); | |
| 438 openConnection(); | |
| 439 } | |
| 440 | |
| 441 // Send a debugger command to the target VM. | |
| 442 void sendMessage(Map<String,dynamic> msg) { | |
| 443 String jsonMsg = JSON.stringify(msg); | |
| 444 if (verboseWire) print("SEND: $jsonMsg"); | |
| 445 to.writeString(jsonMsg, Encoding.UTF_8); | |
| 446 } | |
| 447 | |
| 448 bool get errorsDetected => errors.length > 0; | |
| 449 | |
| 450 // Record error message. | |
| 451 void error(String s) { | |
| 452 errors.add(s); | |
| 453 } | |
| 454 | |
| 455 void openConnection() { | |
| 456 socket = new Socket("127.0.0.1", portNumber); | |
| 457 to = socket.outputStream; | |
| 458 from = new StringInputStream(socket.inputStream, Encoding.UTF_8); | |
| 459 from.onData = () { | |
| 460 try { | |
| 461 responses.append(from.read()); | |
| 462 handleMessages(); | |
| 463 } catch(e, trace) { | |
| 464 print("Unexpected exception:\n$e\n$trace"); | |
| 465 close(); | |
| 466 } | |
| 467 }; | |
| 468 from.onClosed = () { | |
| 469 print("Connection closed by debug target"); | |
| 470 close(); | |
| 471 }; | |
| 472 from.onError = (e) { | |
| 473 print("Error '$e' detected in input stream from debug target"); | |
| 474 close(); | |
| 475 }; | |
| 476 } | |
| 477 | |
| 478 void close() { | |
| 479 if (errorsDetected) { | |
| 480 for (int i = 0; i < errors.length; i++) print(errors[i]); | |
| 481 } | |
| 482 to.close(); | |
| 483 socket.close(); | |
| 484 targetProcess.kill(); | |
| 485 print("Target process killed"); | |
| 486 Expect.isTrue(!errorsDetected); | |
| 487 stdin.close(); | |
| 488 stdout.close(); | |
| 489 stderr.close(); | |
| 490 } | |
| 491 } | |
| 492 | |
| 493 | |
| 494 bool RunScript(List script) { | |
| 495 var options = new Options(); | |
| 496 if (options.arguments.contains("--debuggee")) { | |
| 497 return false; | |
| 498 } | |
| 499 showDebuggeeOutput = options.arguments.contains("--verbose"); | |
| 500 verboseWire = options.arguments.contains("--wire"); | |
| 501 | |
| 502 var targetOpts = [ "--debug:$debugPort" ]; | |
| 503 if (showDebuggeeOutput) targetOpts.add("--verbose_debug"); | |
| 504 targetOpts.add(options.script); | |
| 505 targetOpts.add("--debuggee"); | |
| 506 | |
| 507 Process.start(options.executable, targetOpts).then((Process process) { | |
| 508 print("Debug target process started"); | |
| 509 process.stdin.close(); | |
| 510 process.onExit = (int exitCode) { | |
| 511 print("Debug target process exited with exit code $exitCode"); | |
| 512 }; | |
| 513 var debugger = new Debugger(process, debugPort); | |
| 514 stdin.onClosed = () => debugger.close(); | |
| 515 stdin.onError = (error) => debugger.close(); | |
| 516 debugger.runScript(script); | |
| 517 }); | |
| 518 return true; | |
| 519 } | |
| OLD | NEW |