| 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 // Simple interactive debugger shell that connects to the Dart VM's debugger | |
| 6 // connection port. | |
| 7 | |
| 8 import "dart:convert"; | |
| 9 import "dart:io"; | |
| 10 import "dart:async"; | |
| 11 import "dart:math"; | |
| 12 | |
| 13 import "ddbg/lib/commando.dart"; | |
| 14 | |
| 15 class TargetScript { | |
| 16 // The text of a script. | |
| 17 String source = null; | |
| 18 | |
| 19 // A mapping from line number to source text. | |
| 20 List<String> lineToSource = null; | |
| 21 | |
| 22 // A mapping from token offset to line number. | |
| 23 Map<int,int> tokenToLine = null; | |
| 24 } | |
| 25 | |
| 26 const UnknownLocation = const {}; | |
| 27 | |
| 28 class TargetIsolate { | |
| 29 int id; | |
| 30 // The location of the last paused event. | |
| 31 Map pausedLocation = null; | |
| 32 | |
| 33 TargetIsolate(this.id); | |
| 34 bool get isPaused => pausedLocation != null; | |
| 35 String get pausedUrl => pausedLocation != null ? pausedLocation["url"] : null; | |
| 36 | |
| 37 Map<String, TargetScript> scripts = {}; | |
| 38 } | |
| 39 | |
| 40 Map<int, TargetIsolate> targetIsolates= new Map<int, TargetIsolate>(); | |
| 41 | |
| 42 Map<int, Completer> outstandingCommands; | |
| 43 | |
| 44 Socket vmSock; | |
| 45 String vmData; | |
| 46 var cmdSubscription; | |
| 47 Commando cmdo; | |
| 48 var vmSubscription; | |
| 49 int seqNum = 0; | |
| 50 | |
| 51 bool isDebugging = false; | |
| 52 Process targetProcess = null; | |
| 53 bool suppressNextExitCode = false; | |
| 54 | |
| 55 final verbose = false; | |
| 56 final printMessages = false; | |
| 57 | |
| 58 TargetIsolate currentIsolate; | |
| 59 TargetIsolate mainIsolate; | |
| 60 | |
| 61 int debugPort = 5858; | |
| 62 | |
| 63 String formatLocation(Map location) { | |
| 64 if (location == null) return ""; | |
| 65 var fileName = location["url"].split("/").last; | |
| 66 return "file: $fileName lib: ${location['libraryId']} token: ${location['token
Offset']}"; | |
| 67 } | |
| 68 | |
| 69 | |
| 70 Future sendCmd(Map<String, dynamic> cmd) { | |
| 71 var completer = new Completer.sync(); | |
| 72 int id = cmd["id"]; | |
| 73 outstandingCommands[id] = completer; | |
| 74 if (verbose) { | |
| 75 print("sending: '${JSON.encode(cmd)}'"); | |
| 76 } | |
| 77 vmSock.write(JSON.encode(cmd)); | |
| 78 return completer.future; | |
| 79 } | |
| 80 | |
| 81 | |
| 82 bool checkCurrentIsolate() { | |
| 83 if (vmSock == null) { | |
| 84 print("There is no active script. Try 'help run'."); | |
| 85 return false; | |
| 86 } | |
| 87 if (currentIsolate == null) { | |
| 88 print('There is no current isolate.'); | |
| 89 return false; | |
| 90 } | |
| 91 return true; | |
| 92 } | |
| 93 | |
| 94 | |
| 95 void setCurrentIsolate(TargetIsolate isolate) { | |
| 96 if (isolate != currentIsolate) { | |
| 97 currentIsolate = isolate; | |
| 98 if (mainIsolate == null) { | |
| 99 print("Main isolate is ${isolate.id}"); | |
| 100 mainIsolate = isolate; | |
| 101 } | |
| 102 print("Current isolate is now ${isolate.id}"); | |
| 103 } | |
| 104 } | |
| 105 | |
| 106 | |
| 107 bool checkPaused() { | |
| 108 if (!checkCurrentIsolate()) return false; | |
| 109 if (currentIsolate.isPaused) return true; | |
| 110 print("Current isolate must be paused"); | |
| 111 return false; | |
| 112 } | |
| 113 | |
| 114 // These settings are allowed in the 'set' and 'show' debugger commands. | |
| 115 var validSettings = ['vm', 'vmargs', 'script', 'args']; | |
| 116 | |
| 117 // The current values for all settings. | |
| 118 var settings = new Map(); | |
| 119 | |
| 120 String _leftJustify(text, int width) { | |
| 121 StringBuffer buffer = new StringBuffer(); | |
| 122 buffer.write(text); | |
| 123 while (buffer.length < width) { | |
| 124 buffer.write(' '); | |
| 125 } | |
| 126 return buffer.toString(); | |
| 127 } | |
| 128 | |
| 129 // TODO(turnidge): Move all commands here. | |
| 130 List<Command> commandList = | |
| 131 [ new HelpCommand(), | |
| 132 new QuitCommand(), | |
| 133 new RunCommand(), | |
| 134 new KillCommand(), | |
| 135 new ConnectCommand(), | |
| 136 new DisconnectCommand(), | |
| 137 new SetCommand(), | |
| 138 new ShowCommand() ]; | |
| 139 | |
| 140 | |
| 141 List<Command> matchCommand(String commandName, bool exactMatchWins) { | |
| 142 List matches = []; | |
| 143 for (var command in commandList) { | |
| 144 if (command.name.startsWith(commandName)) { | |
| 145 if (exactMatchWins && command.name == commandName) { | |
| 146 // Exact match | |
| 147 return [command]; | |
| 148 } else { | |
| 149 matches.add(command); | |
| 150 } | |
| 151 } | |
| 152 } | |
| 153 return matches; | |
| 154 } | |
| 155 | |
| 156 abstract class Command { | |
| 157 String get name; | |
| 158 Future run(List<String> args); | |
| 159 } | |
| 160 | |
| 161 class HelpCommand extends Command { | |
| 162 final name = 'help'; | |
| 163 final helpShort = 'Show a list of debugger commands'; | |
| 164 final helpLong =""" | |
| 165 Show a list of debugger commands or get more information about a | |
| 166 particular command. | |
| 167 | |
| 168 Usage: | |
| 169 help | |
| 170 help <command> | |
| 171 """; | |
| 172 | |
| 173 Future run(List<String> args) { | |
| 174 if (args.length == 1) { | |
| 175 print("Debugger commands:\n"); | |
| 176 for (var command in commandList) { | |
| 177 print(' ${_leftJustify(command.name, 11)} ${command.helpShort}'); | |
| 178 } | |
| 179 | |
| 180 // TODO(turnidge): Convert all commands to use the Command class. | |
| 181 print(""" | |
| 182 bt Show backtrace | |
| 183 r Resume execution | |
| 184 s Single step | |
| 185 so Step over | |
| 186 si Step into | |
| 187 sbp [<file>] <line> Set breakpoint | |
| 188 rbp <id> Remove breakpoint with given id | |
| 189 po <id> Print object info for given id | |
| 190 eval fr <n> <expr> Evaluate expr on stack frame index n | |
| 191 eval obj <id> <expr> Evaluate expr on object id | |
| 192 eval cls <id> <expr> Evaluate expr on class id | |
| 193 eval lib <id> <expr> Evaluate expr in toplevel of library id | |
| 194 pl <id> <idx> [<len>] Print list element/slice | |
| 195 pc <id> Print class info for given id | |
| 196 ll List loaded libraries | |
| 197 plib <id> Print library info for given library id | |
| 198 slib <id> <true|false> Set library id debuggable | |
| 199 pg <id> Print all global variables visible within given library id | |
| 200 ls <lib_id> List loaded scripts in library | |
| 201 gs <lib_id> <script_url> Get source text of script in library | |
| 202 tok <lib_id> <script_url> Get line and token table of script in library | |
| 203 epi <none|all|unhandled> Set exception pause info | |
| 204 li List ids of all isolates in the VM | |
| 205 sci <id> Set current target isolate | |
| 206 i <id> Interrupt execution of given isolate id | |
| 207 """); | |
| 208 | |
| 209 print("For more information about a particular command, type:\n\n" | |
| 210 " help <command>\n"); | |
| 211 | |
| 212 print("Commands may be abbreviated: e.g. type 'h' for 'help.\n"); | |
| 213 } else if (args.length == 2) { | |
| 214 var commandName = args[1]; | |
| 215 var matches = matchCommand(commandName, true); | |
| 216 if (matches.length == 0) { | |
| 217 print("Command '$commandName' not recognized. " | |
| 218 "Try 'help' for a list of commands."); | |
| 219 } else { | |
| 220 for (var command in matches) { | |
| 221 print("---- ${command.name} ----\n${command.helpLong}"); | |
| 222 } | |
| 223 } | |
| 224 } else { | |
| 225 print("Command '$command' not recognized. " | |
| 226 "Try 'help' for a list of commands."); | |
| 227 } | |
| 228 | |
| 229 return new Future.value(); | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 | |
| 234 class QuitCommand extends Command { | |
| 235 final name = 'quit'; | |
| 236 final helpShort = 'Quit the debugger.'; | |
| 237 final helpLong =""" | |
| 238 Quit the debugger. | |
| 239 | |
| 240 Usage: | |
| 241 quit | |
| 242 """; | |
| 243 | |
| 244 Future run(List<String> args) { | |
| 245 if (args.length > 1) { | |
| 246 print("Unexpected arguments to $name command."); | |
| 247 return new Future.value(); | |
| 248 } | |
| 249 return debuggerQuit(); | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 class SetCommand extends Command { | |
| 254 final name = 'set'; | |
| 255 final helpShort = 'Change the value of a debugger setting.'; | |
| 256 final helpLong =""" | |
| 257 Change the value of a debugger setting. | |
| 258 | |
| 259 Usage: | |
| 260 set <setting> <value> | |
| 261 | |
| 262 Valid settings are: | |
| 263 ${validSettings.join('\n ')}. | |
| 264 | |
| 265 See also 'help show'. | |
| 266 """; | |
| 267 | |
| 268 Future run(List<String> args) { | |
| 269 if (args.length < 3 || !validSettings.contains(args[1])) { | |
| 270 print("Undefined $name command. Try 'help $name'."); | |
| 271 return new Future.value(); | |
| 272 } | |
| 273 var option = args[1]; | |
| 274 var value = args.getRange(2, args.length).join(' '); | |
| 275 settings[option] = value; | |
| 276 return new Future.value(); | |
| 277 } | |
| 278 } | |
| 279 | |
| 280 class ShowCommand extends Command { | |
| 281 final name = 'show'; | |
| 282 final helpShort = 'Show the current value of a debugger setting.'; | |
| 283 final helpLong =""" | |
| 284 Show the current value of a debugger setting. | |
| 285 | |
| 286 Usage: | |
| 287 show | |
| 288 show <setting> | |
| 289 | |
| 290 If no <setting> is specified, all current settings are shown. | |
| 291 | |
| 292 Valid settings are: | |
| 293 ${validSettings.join('\n ')}. | |
| 294 | |
| 295 See also 'help set'. | |
| 296 """; | |
| 297 | |
| 298 Future run(List<String> args) { | |
| 299 if (args.length == 1) { | |
| 300 for (var option in validSettings) { | |
| 301 var value = settings[option]; | |
| 302 print("$option = '$value'"); | |
| 303 } | |
| 304 } else if (args.length == 2 && validSettings.contains(args[1])) { | |
| 305 var option = args[1]; | |
| 306 var value = settings[option]; | |
| 307 if (value == null) { | |
| 308 print('$option has not been set.'); | |
| 309 } else { | |
| 310 print("$option = '$value'"); | |
| 311 } | |
| 312 return new Future.value(); | |
| 313 } else { | |
| 314 print("Undefined $name command. Try 'help $name'."); | |
| 315 } | |
| 316 return new Future.value(); | |
| 317 } | |
| 318 } | |
| 319 | |
| 320 class RunCommand extends Command { | |
| 321 final name = 'run'; | |
| 322 final helpShort = "Run the currrent script."; | |
| 323 final helpLong =""" | |
| 324 Runs the current script. | |
| 325 | |
| 326 Usage: | |
| 327 run | |
| 328 run <args> | |
| 329 | |
| 330 The current script will be run on the current vm. The 'vm' and | |
| 331 'vmargs' settings are used to specify the current vm and vm arguments. | |
| 332 The 'script' and 'args' settings are used to specify the current | |
| 333 script and script arguments. | |
| 334 | |
| 335 For more information on settings type 'help show' or 'help set'. | |
| 336 | |
| 337 If <args> are provided to the run command, it is the same as typing | |
| 338 'set args <args>' followed by 'run'. | |
| 339 """; | |
| 340 | |
| 341 Future run(List<String> cmdArgs) { | |
| 342 if (isDebugging) { | |
| 343 // TODO(turnidge): Implement modal y/n dialog to stop running script. | |
| 344 print("There is already a running dart process. " | |
| 345 "Try 'kill'."); | |
| 346 return new Future.value(); | |
| 347 } | |
| 348 assert(targetProcess == null); | |
| 349 if (settings['script'] == null) { | |
| 350 print("There is no script specified. " | |
| 351 "Use 'set script' to set the current script."); | |
| 352 return new Future.value(); | |
| 353 } | |
| 354 if (cmdArgs.length > 1) { | |
| 355 settings['args'] = cmdArgs.getRange(1, cmdArgs.length); | |
| 356 } | |
| 357 | |
| 358 // Build the process arguments. | |
| 359 var processArgs = ['--debug:$debugPort']; | |
| 360 if (verbose) { | |
| 361 processArgs.add('--verbose_debug'); | |
| 362 } | |
| 363 if (settings['vmargs'] != null) { | |
| 364 processArgs.addAll(settings['vmargs'].split(' ')); | |
| 365 } | |
| 366 processArgs.add(settings['script']); | |
| 367 if (settings['args'] != null) { | |
| 368 processArgs.addAll(settings['args'].split(' ')); | |
| 369 } | |
| 370 String vm = settings['vm']; | |
| 371 | |
| 372 isDebugging = true; | |
| 373 cmdo.hide(); | |
| 374 return Process.start(vm, processArgs).then((process) { | |
| 375 print("Started process ${process.pid} '$vm ${processArgs.join(' ')}'"); | |
| 376 targetProcess = process; | |
| 377 process.stdin.close(); | |
| 378 | |
| 379 // TODO(turnidge): For now we only show full lines of output | |
| 380 // from the debugged process. Should show each character. | |
| 381 process.stdout | |
| 382 .transform(UTF8.decoder) | |
| 383 .transform(new LineSplitter()) | |
| 384 .listen((String line) { | |
| 385 cmdo.hide(); | |
| 386 // TODO(turnidge): Escape output in any way? | |
| 387 print(line); | |
| 388 cmdo.show(); | |
| 389 }); | |
| 390 | |
| 391 process.stderr | |
| 392 .transform(UTF8.decoder) | |
| 393 .transform(new LineSplitter()) | |
| 394 .listen((String line) { | |
| 395 cmdo.hide(); | |
| 396 print(line); | |
| 397 cmdo.show(); | |
| 398 }); | |
| 399 | |
| 400 process.exitCode.then((int exitCode) { | |
| 401 cmdo.hide(); | |
| 402 if (suppressNextExitCode) { | |
| 403 suppressNextExitCode = false; | |
| 404 } else { | |
| 405 if (exitCode == 0) { | |
| 406 print('Process exited normally.'); | |
| 407 } else { | |
| 408 print('Process exited with code $exitCode.'); | |
| 409 } | |
| 410 } | |
| 411 targetProcess = null; | |
| 412 cmdo.show(); | |
| 413 }); | |
| 414 | |
| 415 // Wait for the vm to open the debugging port. | |
| 416 return openVmSocket(0); | |
| 417 }); | |
| 418 } | |
| 419 } | |
| 420 | |
| 421 class KillCommand extends Command { | |
| 422 final name = 'kill'; | |
| 423 final helpShort = 'Kill the currently executing script.'; | |
| 424 final helpLong =""" | |
| 425 Kill the currently executing script. | |
| 426 | |
| 427 Usage: | |
| 428 kill | |
| 429 """; | |
| 430 | |
| 431 Future run(List<String> cmdArgs) { | |
| 432 if (!isDebugging) { | |
| 433 print('There is no running script.'); | |
| 434 return new Future.value(); | |
| 435 } | |
| 436 if (targetProcess == null) { | |
| 437 print("The active dart process was not started with 'run'. " | |
| 438 "Try 'disconnect' instead."); | |
| 439 return new Future.value(); | |
| 440 } | |
| 441 assert(targetProcess != null); | |
| 442 bool result = targetProcess.kill(); | |
| 443 if (result) { | |
| 444 print('Process killed.'); | |
| 445 suppressNextExitCode = true; | |
| 446 } else { | |
| 447 print('Unable to kill process ${targetProcess.pid}'); | |
| 448 } | |
| 449 return new Future.value(); | |
| 450 } | |
| 451 } | |
| 452 | |
| 453 class ConnectCommand extends Command { | |
| 454 final name = 'connect'; | |
| 455 final helpShort = "Connect to a running dart script."; | |
| 456 final helpLong =""" | |
| 457 Connect to a running dart script. | |
| 458 | |
| 459 Usage: | |
| 460 connect | |
| 461 connect <port> | |
| 462 | |
| 463 The debugger will connect to a dart script which has already been | |
| 464 started with the --debug option. If no port is provided, the debugger | |
| 465 will attempt to connect on the default debugger port. | |
| 466 """; | |
| 467 | |
| 468 Future run(List<String> cmdArgs) { | |
| 469 if (cmdArgs.length > 2) { | |
| 470 print("Too many arguments to 'connect'."); | |
| 471 } | |
| 472 if (isDebugging) { | |
| 473 // TODO(turnidge): Implement modal y/n dialog to stop running script. | |
| 474 print("There is already a running dart process. " | |
| 475 "Try 'kill'."); | |
| 476 return new Future.value(); | |
| 477 } | |
| 478 assert(targetProcess == null); | |
| 479 if (cmdArgs.length == 2) { | |
| 480 debugPort = int.parse(cmdArgs[1]); | |
| 481 } | |
| 482 | |
| 483 isDebugging = true; | |
| 484 cmdo.hide(); | |
| 485 return openVmSocket(0); | |
| 486 } | |
| 487 } | |
| 488 | |
| 489 class DisconnectCommand extends Command { | |
| 490 final name = 'disconnect'; | |
| 491 final helpShort = "Disconnect from a running dart script."; | |
| 492 final helpLong =""" | |
| 493 Disconnect from a running dart script. | |
| 494 | |
| 495 Usage: | |
| 496 disconnect | |
| 497 | |
| 498 The debugger will disconnect from a dart script's debugging port. The | |
| 499 script must have been connected to earlier with the 'connect' command. | |
| 500 """; | |
| 501 | |
| 502 Future run(List<String> cmdArgs) { | |
| 503 if (cmdArgs.length > 1) { | |
| 504 print("Too many arguments to 'disconnect'."); | |
| 505 } | |
| 506 if (!isDebugging) { | |
| 507 // TODO(turnidge): Implement modal y/n dialog to stop running script. | |
| 508 print("There is no active dart process. " | |
| 509 "Try 'connect'."); | |
| 510 return new Future.value(); | |
| 511 } | |
| 512 if (targetProcess != null) { | |
| 513 print("The active dart process was started with 'run'. " | |
| 514 "Try 'kill'."); | |
| 515 } | |
| 516 | |
| 517 cmdo.hide(); | |
| 518 return closeVmSocket(); | |
| 519 } | |
| 520 } | |
| 521 | |
| 522 typedef void HandlerType(Map response); | |
| 523 | |
| 524 HandlerType showPromptAfter(void handler(Map response)) { | |
| 525 return (response) { | |
| 526 handler(response); | |
| 527 cmdo.show(); | |
| 528 }; | |
| 529 } | |
| 530 | |
| 531 void processCommand(String cmdLine) { | |
| 532 void huh() { | |
| 533 print("'$cmdLine' not understood, try 'help' for help."); | |
| 534 } | |
| 535 | |
| 536 cmdo.hide(); | |
| 537 seqNum++; | |
| 538 cmdLine = cmdLine.trim(); | |
| 539 var args = cmdLine.split(' '); | |
| 540 if (args.length == 0) { | |
| 541 return; | |
| 542 } | |
| 543 var command = args[0]; | |
| 544 | |
| 545 var resume_commands = | |
| 546 { 'r':'resume', 's':'stepOver', 'si':'stepInto', 'so':'stepOut'}; | |
| 547 if (resume_commands[command] != null) { | |
| 548 if (!checkPaused()) { | |
| 549 cmdo.show(); | |
| 550 return; | |
| 551 } | |
| 552 var cmd = { "id": seqNum, | |
| 553 "command": resume_commands[command], | |
| 554 "params": { "isolateId" : currentIsolate.id } }; | |
| 555 sendCmd(cmd).then(showPromptAfter(handleResumedResponse)); | |
| 556 } else if (command == "bt") { | |
| 557 if (!checkCurrentIsolate()) { | |
| 558 cmdo.show(); | |
| 559 return; | |
| 560 } | |
| 561 var cmd = { "id": seqNum, | |
| 562 "command": "getStackTrace", | |
| 563 "params": { "isolateId" : currentIsolate.id } }; | |
| 564 sendCmd(cmd).then(showPromptAfter(handleStackTraceResponse)); | |
| 565 } else if (command == "ll") { | |
| 566 if (!checkCurrentIsolate()) { | |
| 567 cmdo.show(); | |
| 568 return; | |
| 569 } | |
| 570 var cmd = { "id": seqNum, | |
| 571 "command": "getLibraries", | |
| 572 "params": { "isolateId" : currentIsolate.id } }; | |
| 573 sendCmd(cmd).then(showPromptAfter(handleGetLibraryResponse)); | |
| 574 } else if (command == "sbp" && args.length >= 2) { | |
| 575 if (!checkCurrentIsolate()) { | |
| 576 cmdo.show(); | |
| 577 return; | |
| 578 } | |
| 579 var url, line; | |
| 580 if (args.length == 2 && currentIsolate.pausedUrl != null) { | |
| 581 url = currentIsolate.pausedUrl; | |
| 582 line = int.parse(args[1]); | |
| 583 } else { | |
| 584 url = args[1]; | |
| 585 line = int.parse(args[2]); | |
| 586 } | |
| 587 var cmd = { "id": seqNum, | |
| 588 "command": "setBreakpoint", | |
| 589 "params": { "isolateId" : currentIsolate.id, | |
| 590 "url": url, | |
| 591 "line": line }}; | |
| 592 sendCmd(cmd).then(showPromptAfter(handleSetBpResponse)); | |
| 593 } else if (command == "rbp" && args.length == 2) { | |
| 594 if (!checkCurrentIsolate()) { | |
| 595 cmdo.show(); | |
| 596 return; | |
| 597 } | |
| 598 var cmd = { "id": seqNum, | |
| 599 "command": "removeBreakpoint", | |
| 600 "params": { "isolateId" : currentIsolate.id, | |
| 601 "breakpointId": int.parse(args[1]) } }; | |
| 602 sendCmd(cmd).then(showPromptAfter(handleGenericResponse)); | |
| 603 } else if (command == "ls" && args.length == 2) { | |
| 604 if (!checkCurrentIsolate()) { | |
| 605 cmdo.show(); | |
| 606 return; | |
| 607 } | |
| 608 var cmd = { "id": seqNum, | |
| 609 "command": "getScriptURLs", | |
| 610 "params": { "isolateId" : currentIsolate.id, | |
| 611 "libraryId": int.parse(args[1]) } }; | |
| 612 sendCmd(cmd).then(showPromptAfter(handleGetScriptsResponse)); | |
| 613 } else if (command == "eval" && args.length > 3) { | |
| 614 if (!checkCurrentIsolate()) { | |
| 615 cmdo.show(); | |
| 616 return; | |
| 617 } | |
| 618 var expr = args.getRange(3, args.length).join(" "); | |
| 619 var target = args[1]; | |
| 620 if (target == "obj") { | |
| 621 target = "objectId"; | |
| 622 } else if (target == "cls") { | |
| 623 target = "classId"; | |
| 624 } else if (target == "lib") { | |
| 625 target = "libraryId"; | |
| 626 } else if (target == "fr") { | |
| 627 target = "frameId"; | |
| 628 } else { | |
| 629 huh(); | |
| 630 return; | |
| 631 } | |
| 632 var cmd = { "id": seqNum, | |
| 633 "command": "evaluateExpr", | |
| 634 "params": { "isolateId": currentIsolate.id, | |
| 635 target: int.parse(args[2]), | |
| 636 "expression": expr } }; | |
| 637 sendCmd(cmd).then(showPromptAfter(handleEvalResponse)); | |
| 638 } else if (command == "po" && args.length == 2) { | |
| 639 if (!checkCurrentIsolate()) { | |
| 640 cmdo.show(); | |
| 641 return; | |
| 642 } | |
| 643 var cmd = { "id": seqNum, | |
| 644 "command": "getObjectProperties", | |
| 645 "params": { "isolateId" : currentIsolate.id, | |
| 646 "objectId": int.parse(args[1]) } }; | |
| 647 sendCmd(cmd).then(showPromptAfter(handleGetObjPropsResponse)); | |
| 648 } else if (command == "pl" && args.length >= 3) { | |
| 649 if (!checkCurrentIsolate()) { | |
| 650 cmdo.show(); | |
| 651 return; | |
| 652 } | |
| 653 var cmd; | |
| 654 if (args.length == 3) { | |
| 655 cmd = { "id": seqNum, | |
| 656 "command": "getListElements", | |
| 657 "params": { "isolateId" : currentIsolate.id, | |
| 658 "objectId": int.parse(args[1]), | |
| 659 "index": int.parse(args[2]) } }; | |
| 660 } else { | |
| 661 cmd = { "id": seqNum, | |
| 662 "command": "getListElements", | |
| 663 "params": { "isolateId" : currentIsolate.id, | |
| 664 "objectId": int.parse(args[1]), | |
| 665 "index": int.parse(args[2]), | |
| 666 "length": int.parse(args[3]) } }; | |
| 667 } | |
| 668 sendCmd(cmd).then(showPromptAfter(handleGetListResponse)); | |
| 669 } else if (command == "pc" && args.length == 2) { | |
| 670 if (!checkCurrentIsolate()) { | |
| 671 cmdo.show(); | |
| 672 return; | |
| 673 } | |
| 674 var cmd = { "id": seqNum, | |
| 675 "command": "getClassProperties", | |
| 676 "params": { "isolateId" : currentIsolate.id, | |
| 677 "classId": int.parse(args[1]) } }; | |
| 678 sendCmd(cmd).then(showPromptAfter(handleGetClassPropsResponse)); | |
| 679 } else if (command == "plib" && args.length == 2) { | |
| 680 if (!checkCurrentIsolate()) { | |
| 681 cmdo.show(); | |
| 682 return; | |
| 683 } | |
| 684 var cmd = { "id": seqNum, | |
| 685 "command": "getLibraryProperties", | |
| 686 "params": {"isolateId" : currentIsolate.id, | |
| 687 "libraryId": int.parse(args[1]) } }; | |
| 688 sendCmd(cmd).then(showPromptAfter(handleGetLibraryPropsResponse)); | |
| 689 } else if (command == "slib" && args.length == 3) { | |
| 690 if (!checkCurrentIsolate()) { | |
| 691 cmdo.show(); | |
| 692 return; | |
| 693 } | |
| 694 var cmd = { "id": seqNum, | |
| 695 "command": "setLibraryProperties", | |
| 696 "params": {"isolateId" : currentIsolate.id, | |
| 697 "libraryId": int.parse(args[1]), | |
| 698 "debuggingEnabled": args[2] } }; | |
| 699 sendCmd(cmd).then(showPromptAfter(handleSetLibraryPropsResponse)); | |
| 700 } else if (command == "pg" && args.length == 2) { | |
| 701 if (!checkCurrentIsolate()) { | |
| 702 cmdo.show(); | |
| 703 return; | |
| 704 } | |
| 705 var cmd = { "id": seqNum, | |
| 706 "command": "getGlobalVariables", | |
| 707 "params": { "isolateId" : currentIsolate.id, | |
| 708 "libraryId": int.parse(args[1]) } }; | |
| 709 sendCmd(cmd).then(showPromptAfter(handleGetGlobalVarsResponse)); | |
| 710 } else if (command == "gs" && args.length == 3) { | |
| 711 if (!checkCurrentIsolate()) { | |
| 712 cmdo.show(); | |
| 713 return; | |
| 714 } | |
| 715 var cmd = { "id": seqNum, | |
| 716 "command": "getScriptSource", | |
| 717 "params": { "isolateId" : currentIsolate.id, | |
| 718 "libraryId": int.parse(args[1]), | |
| 719 "url": args[2] } }; | |
| 720 sendCmd(cmd).then(showPromptAfter(handleGetSourceResponse)); | |
| 721 } else if (command == "tok" && args.length == 3) { | |
| 722 if (!checkCurrentIsolate()) { | |
| 723 cmdo.show(); | |
| 724 return; | |
| 725 } | |
| 726 var cmd = { "id": seqNum, | |
| 727 "command": "getLineNumberTable", | |
| 728 "params": { "isolateId" : currentIsolate.id, | |
| 729 "libraryId": int.parse(args[1]), | |
| 730 "url": args[2] } }; | |
| 731 sendCmd(cmd).then(showPromptAfter(handleGetLineTableResponse)); | |
| 732 } else if (command == "epi" && args.length == 2) { | |
| 733 if (!checkCurrentIsolate()) { | |
| 734 cmdo.show(); | |
| 735 return; | |
| 736 } | |
| 737 var cmd = { "id": seqNum, | |
| 738 "command": "setPauseOnException", | |
| 739 "params": { "isolateId" : currentIsolate.id, | |
| 740 "exceptions": args[1] } }; | |
| 741 sendCmd(cmd).then(showPromptAfter(handleGenericResponse)); | |
| 742 } else if (command == "li") { | |
| 743 if (!checkCurrentIsolate()) { | |
| 744 cmdo.show(); | |
| 745 return; | |
| 746 } | |
| 747 var cmd = { "id": seqNum, "command": "getIsolateIds" }; | |
| 748 sendCmd(cmd).then(showPromptAfter(handleGetIsolatesResponse)); | |
| 749 } else if (command == "sci" && args.length == 2) { | |
| 750 var id = int.parse(args[1]); | |
| 751 if (targetIsolates[id] != null) { | |
| 752 setCurrentIsolate(targetIsolates[id]); | |
| 753 } else { | |
| 754 print("$id is not a valid isolate id"); | |
| 755 } | |
| 756 cmdo.show(); | |
| 757 } else if (command == "i" && args.length == 2) { | |
| 758 var cmd = { "id": seqNum, | |
| 759 "command": "interrupt", | |
| 760 "params": { "isolateId": int.parse(args[1]) } }; | |
| 761 sendCmd(cmd).then(showPromptAfter(handleGenericResponse)); | |
| 762 } else if (command.length == 0) { | |
| 763 huh(); | |
| 764 cmdo.show(); | |
| 765 } else { | |
| 766 // TODO(turnidge): Use this for all commands. | |
| 767 var matches = matchCommand(command, true); | |
| 768 if (matches.length == 0) { | |
| 769 huh(); | |
| 770 cmdo.show(); | |
| 771 } else if (matches.length == 1) { | |
| 772 matches[0].run(args).then((_) { | |
| 773 cmdo.show(); | |
| 774 }); | |
| 775 } else { | |
| 776 var matchNames = matches.map((handler) => handler.name); | |
| 777 print("Ambiguous command '$command' : ${matchNames.toList()}"); | |
| 778 cmdo.show(); | |
| 779 } | |
| 780 } | |
| 781 } | |
| 782 | |
| 783 | |
| 784 void processError(error, trace) { | |
| 785 cmdo.hide(); | |
| 786 print("\nInternal error:\n$error\n$trace"); | |
| 787 cmdo.show(); | |
| 788 } | |
| 789 | |
| 790 | |
| 791 void processDone() { | |
| 792 debuggerQuit(); | |
| 793 } | |
| 794 | |
| 795 | |
| 796 String remoteObject(value) { | |
| 797 var kind = value["kind"]; | |
| 798 var text = value["text"]; | |
| 799 var id = value["objectId"]; | |
| 800 if (kind == "string") { | |
| 801 return "(string, id $id) '$text'"; | |
| 802 } else if (kind == "list") { | |
| 803 var len = value["length"]; | |
| 804 return "(list, id $id, len $len) $text"; | |
| 805 } else if (kind == "object") { | |
| 806 return "(obj, id $id) $text"; | |
| 807 } else if (kind == "function") { | |
| 808 var location = formatLocation(value['location']); | |
| 809 var name = value['name']; | |
| 810 var signature = value['signature']; | |
| 811 return "(closure ${name}${signature} $location)"; | |
| 812 } else { | |
| 813 return "$text"; | |
| 814 } | |
| 815 } | |
| 816 | |
| 817 | |
| 818 printNamedObject(obj) { | |
| 819 var name = obj["name"]; | |
| 820 var value = obj["value"]; | |
| 821 print(" $name = ${remoteObject(value)}"); | |
| 822 } | |
| 823 | |
| 824 | |
| 825 handleGetObjPropsResponse(Map response) { | |
| 826 Map props = response["result"]; | |
| 827 int class_id = props["classId"]; | |
| 828 if (class_id == -1) { | |
| 829 print(" null"); | |
| 830 return; | |
| 831 } | |
| 832 List fields = props["fields"]; | |
| 833 print(" class id: $class_id"); | |
| 834 for (int i = 0; i < fields.length; i++) { | |
| 835 printNamedObject(fields[i]); | |
| 836 } | |
| 837 } | |
| 838 | |
| 839 handleGetListResponse(Map response) { | |
| 840 Map result = response["result"]; | |
| 841 if (result["elements"] != null) { | |
| 842 // List slice. | |
| 843 var index = result["index"]; | |
| 844 var length = result["length"]; | |
| 845 List elements = result["elements"]; | |
| 846 assert(length == elements.length); | |
| 847 for (int i = 0; i < length; i++) { | |
| 848 var kind = elements[i]["kind"]; | |
| 849 var text = elements[i]["text"]; | |
| 850 print(" ${index + i}: ($kind) $text"); | |
| 851 } | |
| 852 } else { | |
| 853 // One element, a remote object. | |
| 854 print(result); | |
| 855 print(" ${remoteObject(result)}"); | |
| 856 } | |
| 857 } | |
| 858 | |
| 859 | |
| 860 handleGetClassPropsResponse(Map response) { | |
| 861 Map props = response["result"]; | |
| 862 assert(props["name"] != null); | |
| 863 int libId = props["libraryId"]; | |
| 864 assert(libId != null); | |
| 865 print(" class ${props["name"]} (library id: $libId)"); | |
| 866 List fields = props["fields"]; | |
| 867 if (fields.length > 0) { | |
| 868 print(" static fields:"); | |
| 869 for (int i = 0; i < fields.length; i++) { | |
| 870 printNamedObject(fields[i]); | |
| 871 } | |
| 872 } | |
| 873 } | |
| 874 | |
| 875 | |
| 876 handleGetLibraryPropsResponse(Map response) { | |
| 877 Map props = response["result"]; | |
| 878 assert(props["url"] != null); | |
| 879 print(" library url: ${props["url"]}"); | |
| 880 assert(props["debuggingEnabled"] != null); | |
| 881 print(" debugging enabled: ${props["debuggingEnabled"]}"); | |
| 882 List imports = props["imports"]; | |
| 883 assert(imports != null); | |
| 884 if (imports.length > 0) { | |
| 885 print(" imports:"); | |
| 886 for (int i = 0; i < imports.length; i++) { | |
| 887 print(" id ${imports[i]["libraryId"]} prefix ${imports[i]["prefix"]}"); | |
| 888 } | |
| 889 } | |
| 890 List globals = props["globals"]; | |
| 891 assert(globals != null); | |
| 892 if (globals.length > 0) { | |
| 893 print(" global variables:"); | |
| 894 for (int i = 0; i < globals.length; i++) { | |
| 895 printNamedObject(globals[i]); | |
| 896 } | |
| 897 } | |
| 898 } | |
| 899 | |
| 900 | |
| 901 handleSetLibraryPropsResponse(Map response) { | |
| 902 Map props = response["result"]; | |
| 903 assert(props["debuggingEnabled"] != null); | |
| 904 print(" debugging enabled: ${props["debuggingEnabled"]}"); | |
| 905 } | |
| 906 | |
| 907 | |
| 908 handleGetGlobalVarsResponse(Map response) { | |
| 909 List globals = response["result"]["globals"]; | |
| 910 for (int i = 0; i < globals.length; i++) { | |
| 911 printNamedObject(globals[i]); | |
| 912 } | |
| 913 } | |
| 914 | |
| 915 | |
| 916 handleGetSourceResponse(Map response) { | |
| 917 Map result = response["result"]; | |
| 918 String source = result["text"]; | |
| 919 print("Source text:\n$source\n--------"); | |
| 920 } | |
| 921 | |
| 922 | |
| 923 handleGetLineTableResponse(Map response) { | |
| 924 Map result = response["result"]; | |
| 925 var info = result["lines"]; | |
| 926 print("Line info table:\n$info"); | |
| 927 } | |
| 928 | |
| 929 | |
| 930 void handleGetIsolatesResponse(Map response) { | |
| 931 Map result = response["result"]; | |
| 932 List ids = result["isolateIds"]; | |
| 933 assert(ids != null); | |
| 934 print("List of isolates:"); | |
| 935 for (int id in ids) { | |
| 936 TargetIsolate isolate = targetIsolates[id]; | |
| 937 var state = (isolate != null) ? "running" : "<unknown isolate>"; | |
| 938 if (isolate != null && isolate.isPaused) { | |
| 939 var loc = formatLocation(isolate.pausedLocation); | |
| 940 state = "paused at $loc"; | |
| 941 } | |
| 942 var marker = " "; | |
| 943 if (currentIsolate != null && id == currentIsolate.id) { | |
| 944 marker = "*"; | |
| 945 } | |
| 946 print("$marker $id $state"); | |
| 947 } | |
| 948 } | |
| 949 | |
| 950 | |
| 951 void handleGetLibraryResponse(Map response) { | |
| 952 Map result = response["result"]; | |
| 953 List libs = result["libraries"]; | |
| 954 print("Loaded libraries:"); | |
| 955 print(libs); | |
| 956 for (int i = 0; i < libs.length; i++) { | |
| 957 print(" ${libs[i]["id"]} ${libs[i]["url"]}"); | |
| 958 } | |
| 959 } | |
| 960 | |
| 961 | |
| 962 void handleGetScriptsResponse(Map response) { | |
| 963 Map result = response["result"]; | |
| 964 List urls = result["urls"]; | |
| 965 print("Loaded scripts:"); | |
| 966 for (int i = 0; i < urls.length; i++) { | |
| 967 print(" $i ${urls[i]}"); | |
| 968 } | |
| 969 } | |
| 970 | |
| 971 | |
| 972 void handleEvalResponse(Map response) { | |
| 973 Map result = response["result"]; | |
| 974 print(remoteObject(result)); | |
| 975 } | |
| 976 | |
| 977 | |
| 978 void handleSetBpResponse(Map response) { | |
| 979 Map result = response["result"]; | |
| 980 var id = result["breakpointId"]; | |
| 981 assert(id != null); | |
| 982 print("Set BP $id"); | |
| 983 } | |
| 984 | |
| 985 | |
| 986 void handleGenericResponse(Map response) { | |
| 987 if (response["error"] != null) { | |
| 988 print("Error: ${response["error"]}"); | |
| 989 } | |
| 990 } | |
| 991 | |
| 992 void handleResumedResponse(Map response) { | |
| 993 if (response["error"] != null) { | |
| 994 print("Error: ${response["error"]}"); | |
| 995 return; | |
| 996 } | |
| 997 assert(currentIsolate != null); | |
| 998 currentIsolate.pausedLocation = null; | |
| 999 } | |
| 1000 | |
| 1001 | |
| 1002 void handleStackTraceResponse(Map response) { | |
| 1003 Map result = response["result"]; | |
| 1004 List callFrames = result["callFrames"]; | |
| 1005 assert(callFrames != null); | |
| 1006 printStackTrace(callFrames); | |
| 1007 } | |
| 1008 | |
| 1009 | |
| 1010 void printStackFrame(frame_num, Map frame) { | |
| 1011 var fname = frame["functionName"]; | |
| 1012 var loc = formatLocation(frame["location"]); | |
| 1013 print("#${_leftJustify(frame_num,2)} $fname at $loc"); | |
| 1014 List locals = frame["locals"]; | |
| 1015 for (int i = 0; i < locals.length; i++) { | |
| 1016 printNamedObject(locals[i]); | |
| 1017 } | |
| 1018 } | |
| 1019 | |
| 1020 | |
| 1021 void printStackTrace(List frames) { | |
| 1022 for (int i = 0; i < frames.length; i++) { | |
| 1023 printStackFrame(i, frames[i]); | |
| 1024 } | |
| 1025 } | |
| 1026 | |
| 1027 | |
| 1028 Map<int, int> parseLineNumberTable(List<List<int>> table) { | |
| 1029 Map tokenToLine = {}; | |
| 1030 for (var line in table) { | |
| 1031 // Each entry begins with a line number... | |
| 1032 var lineNumber = line[0]; | |
| 1033 for (var pos = 1; pos < line.length; pos += 2) { | |
| 1034 // ...and is followed by (token offset, col number) pairs. | |
| 1035 // We ignore the column numbers. | |
| 1036 var tokenOffset = line[pos]; | |
| 1037 tokenToLine[tokenOffset] = lineNumber; | |
| 1038 } | |
| 1039 } | |
| 1040 return tokenToLine; | |
| 1041 } | |
| 1042 | |
| 1043 | |
| 1044 Future<TargetScript> getTargetScript(Map location) { | |
| 1045 var isolate = targetIsolates[currentIsolate.id]; | |
| 1046 var url = location['url']; | |
| 1047 var script = isolate.scripts[url]; | |
| 1048 if (script != null) { | |
| 1049 return new Future.value(script); | |
| 1050 } | |
| 1051 script = new TargetScript(); | |
| 1052 | |
| 1053 // Ask the vm for the source and line number table. | |
| 1054 var sourceCmd = { | |
| 1055 "id": seqNum++, | |
| 1056 "command": "getScriptSource", | |
| 1057 "params": { "isolateId": currentIsolate.id, | |
| 1058 "libraryId": location['libraryId'], | |
| 1059 "url": url } }; | |
| 1060 | |
| 1061 var lineNumberCmd = { | |
| 1062 "id": seqNum++, | |
| 1063 "command": "getLineNumberTable", | |
| 1064 "params": { "isolateId": currentIsolate.id, | |
| 1065 "libraryId": location['libraryId'], | |
| 1066 "url": url } }; | |
| 1067 | |
| 1068 // Send the source command | |
| 1069 var sourceResponse = sendCmd(sourceCmd).then((response) { | |
| 1070 Map result = response["result"]; | |
| 1071 script.source = result['text']; | |
| 1072 // Line numbers are 1-based so add a dummy for line 0. | |
| 1073 script.lineToSource = ['']; | |
| 1074 script.lineToSource.addAll(script.source.split('\n')); | |
| 1075 }); | |
| 1076 | |
| 1077 // Send the line numbers command | |
| 1078 var lineNumberResponse = sendCmd(lineNumberCmd).then((response) { | |
| 1079 Map result = response["result"]; | |
| 1080 script.tokenToLine = parseLineNumberTable(result['lines']); | |
| 1081 }); | |
| 1082 | |
| 1083 return Future.wait([sourceResponse, lineNumberResponse]).then((_) { | |
| 1084 // When both commands complete, cache the result. | |
| 1085 isolate.scripts[url] = script; | |
| 1086 return script; | |
| 1087 }); | |
| 1088 } | |
| 1089 | |
| 1090 | |
| 1091 Future printLocation(String label, Map location) { | |
| 1092 // Figure out the line number. | |
| 1093 return getTargetScript(location).then((script) { | |
| 1094 var lineNumber = script.tokenToLine[location['tokenOffset']]; | |
| 1095 var text = script.lineToSource[lineNumber]; | |
| 1096 if (label != null) { | |
| 1097 var fileName = location['url'].split("/").last; | |
| 1098 print("$label \n" | |
| 1099 " at $fileName:$lineNumber"); | |
| 1100 } | |
| 1101 print("${_leftJustify(lineNumber, 8)}$text"); | |
| 1102 }); | |
| 1103 } | |
| 1104 | |
| 1105 | |
| 1106 Future handlePausedEvent(msg) { | |
| 1107 assert(msg["params"] != null); | |
| 1108 var reason = msg["params"]["reason"]; | |
| 1109 int isolateId = msg["params"]["isolateId"]; | |
| 1110 assert(isolateId != null); | |
| 1111 var isolate = targetIsolates[isolateId]; | |
| 1112 assert(isolate != null); | |
| 1113 assert(!isolate.isPaused); | |
| 1114 var location = msg["params"]["location"];; | |
| 1115 setCurrentIsolate(isolate); | |
| 1116 isolate.pausedLocation = (location == null) ? UnknownLocation : location; | |
| 1117 if (reason == "breakpoint") { | |
| 1118 assert(location != null); | |
| 1119 var bpId = (msg["params"]["breakpointId"]); | |
| 1120 var label = (bpId != null) ? "Breakpoint $bpId" : null; | |
| 1121 return printLocation(label, location); | |
| 1122 } else if (reason == "interrupted") { | |
| 1123 assert(location != null); | |
| 1124 return printLocation("Interrupted", location); | |
| 1125 } else { | |
| 1126 assert(reason == "exception"); | |
| 1127 var excObj = msg["params"]["exception"]; | |
| 1128 print("Isolate $isolateId paused on exception"); | |
| 1129 print(remoteObject(excObj)); | |
| 1130 return new Future.value(); | |
| 1131 } | |
| 1132 } | |
| 1133 | |
| 1134 void handleIsolateEvent(msg) { | |
| 1135 Map params = msg["params"]; | |
| 1136 assert(params != null); | |
| 1137 var isolateId = params["id"]; | |
| 1138 var reason = params["reason"]; | |
| 1139 if (reason == "created") { | |
| 1140 print("Isolate $isolateId has been created."); | |
| 1141 assert(targetIsolates[isolateId] == null); | |
| 1142 targetIsolates[isolateId] = new TargetIsolate(isolateId); | |
| 1143 } else { | |
| 1144 assert(reason == "shutdown"); | |
| 1145 var isolate = targetIsolates.remove(isolateId); | |
| 1146 assert(isolate != null); | |
| 1147 if (isolate == mainIsolate) { | |
| 1148 mainIsolate = null; | |
| 1149 print("Main isolate ${isolate.id} has terminated."); | |
| 1150 } else { | |
| 1151 print("Isolate ${isolate.id} has terminated."); | |
| 1152 } | |
| 1153 if (isolate == currentIsolate) { | |
| 1154 currentIsolate = mainIsolate; | |
| 1155 if (currentIsolate == null && !targetIsolates.isEmpty) { | |
| 1156 currentIsolate = targetIsolates.values.first; | |
| 1157 } | |
| 1158 if (currentIsolate != null) { | |
| 1159 print("Setting current isolate to ${currentIsolate.id}."); | |
| 1160 } else { | |
| 1161 print("All isolates have terminated."); | |
| 1162 } | |
| 1163 } | |
| 1164 } | |
| 1165 } | |
| 1166 | |
| 1167 void processVmMessage(String jsonString) { | |
| 1168 var msg = JSON.decode(jsonString); | |
| 1169 if (msg == null) { | |
| 1170 return; | |
| 1171 } | |
| 1172 var event = msg["event"]; | |
| 1173 if (event == "isolate") { | |
| 1174 cmdo.hide(); | |
| 1175 handleIsolateEvent(msg); | |
| 1176 cmdo.show(); | |
| 1177 return; | |
| 1178 } | |
| 1179 if (event == "paused") { | |
| 1180 cmdo.hide(); | |
| 1181 handlePausedEvent(msg).then((_) { | |
| 1182 cmdo.show(); | |
| 1183 }); | |
| 1184 return; | |
| 1185 } | |
| 1186 if (event == "breakpointResolved") { | |
| 1187 Map params = msg["params"]; | |
| 1188 assert(params != null); | |
| 1189 var isolateId = params["isolateId"]; | |
| 1190 var location = formatLocation(params["location"]); | |
| 1191 cmdo.hide(); | |
| 1192 print("Breakpoint ${params["breakpointId"]} resolved in isolate $isolateId" | |
| 1193 " at $location."); | |
| 1194 cmdo.show(); | |
| 1195 return; | |
| 1196 } | |
| 1197 if (msg["id"] != null) { | |
| 1198 var id = msg["id"]; | |
| 1199 if (outstandingCommands.containsKey(id)) { | |
| 1200 var completer = outstandingCommands.remove(id); | |
| 1201 if (msg["error"] != null) { | |
| 1202 print("VM says: ${msg["error"]}"); | |
| 1203 // TODO(turnidge): Rework how hide/show happens. For now we | |
| 1204 // show here explicitly. | |
| 1205 cmdo.show(); | |
| 1206 } else { | |
| 1207 completer.complete(msg); | |
| 1208 } | |
| 1209 } | |
| 1210 } | |
| 1211 } | |
| 1212 | |
| 1213 bool haveGarbageVmData() { | |
| 1214 if (vmData == null || vmData.length == 0) return false; | |
| 1215 var i = 0, char = " "; | |
| 1216 while (i < vmData.length) { | |
| 1217 char = vmData[i]; | |
| 1218 if (char != " " && char != "\n" && char != "\r" && char != "\t") break; | |
| 1219 i++; | |
| 1220 } | |
| 1221 if (i >= vmData.length) { | |
| 1222 return false; | |
| 1223 } else { | |
| 1224 return char != "{"; | |
| 1225 } | |
| 1226 } | |
| 1227 | |
| 1228 | |
| 1229 void processVmData(String data) { | |
| 1230 if (vmData == null || vmData.length == 0) { | |
| 1231 vmData = data; | |
| 1232 } else { | |
| 1233 vmData = vmData + data; | |
| 1234 } | |
| 1235 if (haveGarbageVmData()) { | |
| 1236 print("Error: have garbage data from VM: '$vmData'"); | |
| 1237 return; | |
| 1238 } | |
| 1239 int msg_len = jsonObjectLength(vmData); | |
| 1240 if (printMessages && msg_len == 0) { | |
| 1241 print("have partial or illegal json message" | |
| 1242 " of ${vmData.length} chars:\n'$vmData'"); | |
| 1243 return; | |
| 1244 } | |
| 1245 while (msg_len > 0 && msg_len <= vmData.length) { | |
| 1246 if (msg_len == vmData.length) { | |
| 1247 if (printMessages) { print("have one full message:\n$vmData"); } | |
| 1248 processVmMessage(vmData); | |
| 1249 vmData = null; | |
| 1250 return; | |
| 1251 } | |
| 1252 if (printMessages) { print("at least one message: '$vmData'"); } | |
| 1253 var msg = vmData.substring(0, msg_len); | |
| 1254 if (printMessages) { print("first message: $msg"); } | |
| 1255 vmData = vmData.substring(msg_len); | |
| 1256 if (haveGarbageVmData()) { | |
| 1257 print("Error: garbage data after previous message: '$vmData'"); | |
| 1258 print("Previous message was: '$msg'"); | |
| 1259 return; | |
| 1260 } | |
| 1261 processVmMessage(msg); | |
| 1262 msg_len = jsonObjectLength(vmData); | |
| 1263 } | |
| 1264 if (printMessages) { print("leftover vm data '$vmData'"); } | |
| 1265 } | |
| 1266 | |
| 1267 /** | |
| 1268 * Skip past a JSON object value. | |
| 1269 * The object value must start with '{' and continues to the | |
| 1270 * matching '}'. No attempt is made to otherwise validate the contents | |
| 1271 * as JSON. If it is invalid, a later [parseJson] will fail. | |
| 1272 */ | |
| 1273 int jsonObjectLength(String string) { | |
| 1274 int skipWhitespace(int index) { | |
| 1275 while (index < string.length) { | |
| 1276 String char = string[index]; | |
| 1277 if (char != " " && char != "\n" && char != "\r" && char != "\t") break; | |
| 1278 index++; | |
| 1279 } | |
| 1280 return index; | |
| 1281 } | |
| 1282 int skipString(int index) { | |
| 1283 assert(string[index - 1] == '"'); | |
| 1284 while (index < string.length) { | |
| 1285 String char = string[index]; | |
| 1286 if (char == '"') return index + 1; | |
| 1287 if (char == r'\') index++; | |
| 1288 if (index == string.length) return index; | |
| 1289 index++; | |
| 1290 } | |
| 1291 return index; | |
| 1292 } | |
| 1293 int index = 0; | |
| 1294 index = skipWhitespace(index); | |
| 1295 // Bail out if the first non-whitespace character isn't '{'. | |
| 1296 if (index == string.length || string[index] != '{') return 0; | |
| 1297 int nesting = 0; | |
| 1298 while (index < string.length) { | |
| 1299 String char = string[index++]; | |
| 1300 if (char == '{') { | |
| 1301 nesting++; | |
| 1302 } else if (char == '}') { | |
| 1303 nesting--; | |
| 1304 if (nesting == 0) return index; | |
| 1305 } else if (char == '"') { | |
| 1306 // Strings can contain braces. Skip their content. | |
| 1307 index = skipString(index); | |
| 1308 } | |
| 1309 } | |
| 1310 return 0; | |
| 1311 } | |
| 1312 | |
| 1313 List<String> debuggerCommandCompleter(List<String> commandParts) { | |
| 1314 List<String> completions = new List<String>(); | |
| 1315 | |
| 1316 // TODO(turnidge): Have a global command table and use it to for | |
| 1317 // help messages, command completion, and command dispatching. For now | |
| 1318 // we hardcode the list here. | |
| 1319 // | |
| 1320 // TODO(turnidge): Implement completion for arguments as well. | |
| 1321 List<String> oldCommands = ['bt', 'r', 's', 'so', 'si', 'sbp', 'rbp', | |
| 1322 'po', 'eval', 'pl', 'pc', 'll', 'plib', 'slib', | |
| 1323 'pg', 'ls', 'gs', 'tok', 'epi', 'li', 'i' ]; | |
| 1324 | |
| 1325 // Completion of first word in the command. | |
| 1326 if (commandParts.length == 1) { | |
| 1327 String prefix = commandParts.last; | |
| 1328 for (var command in oldCommands) { | |
| 1329 if (command.startsWith(prefix)) { | |
| 1330 completions.add(command); | |
| 1331 } | |
| 1332 } | |
| 1333 for (var command in commandList) { | |
| 1334 if (command.name.startsWith(prefix)) { | |
| 1335 completions.add(command.name); | |
| 1336 } | |
| 1337 } | |
| 1338 } | |
| 1339 | |
| 1340 return completions; | |
| 1341 } | |
| 1342 | |
| 1343 Future closeCommando() { | |
| 1344 var subscription = cmdSubscription; | |
| 1345 cmdSubscription = null; | |
| 1346 cmdo = null; | |
| 1347 | |
| 1348 var future = subscription.cancel(); | |
| 1349 if (future != null) { | |
| 1350 return future; | |
| 1351 } else { | |
| 1352 return new Future.value(); | |
| 1353 } | |
| 1354 } | |
| 1355 | |
| 1356 | |
| 1357 Future openVmSocket(int attempt) { | |
| 1358 return Socket.connect("127.0.0.1", debugPort).then( | |
| 1359 setupVmSocket, | |
| 1360 onError: (e) { | |
| 1361 // We were unable to connect to the debugger's port. Try again. | |
| 1362 retryOpenVmSocket(e, attempt); | |
| 1363 }); | |
| 1364 } | |
| 1365 | |
| 1366 | |
| 1367 void setupVmSocket(Socket s) { | |
| 1368 vmSock = s; | |
| 1369 vmSock.setOption(SocketOption.TCP_NODELAY, true); | |
| 1370 var stringStream = vmSock.transform(UTF8.decoder); | |
| 1371 outstandingCommands = new Map<int, Completer>(); | |
| 1372 vmSubscription = stringStream.listen( | |
| 1373 (String data) { | |
| 1374 processVmData(data); | |
| 1375 }, | |
| 1376 onDone: () { | |
| 1377 cmdo.hide(); | |
| 1378 if (verbose) { | |
| 1379 print("VM debugger connection closed"); | |
| 1380 } | |
| 1381 closeVmSocket().then((_) { | |
| 1382 cmdo.show(); | |
| 1383 }); | |
| 1384 }, | |
| 1385 onError: (err) { | |
| 1386 cmdo.hide(); | |
| 1387 // TODO(floitsch): do we want to print the stack trace? | |
| 1388 print("Error in debug connection: $err"); | |
| 1389 | |
| 1390 // TODO(turnidge): Kill the debugged process here? | |
| 1391 closeVmSocket().then((_) { | |
| 1392 cmdo.show(); | |
| 1393 }); | |
| 1394 }); | |
| 1395 } | |
| 1396 | |
| 1397 | |
| 1398 Future retryOpenVmSocket(error, int attempt) { | |
| 1399 var delay; | |
| 1400 if (attempt < 10) { | |
| 1401 delay = new Duration(milliseconds:10); | |
| 1402 } else if (attempt < 20) { | |
| 1403 delay = new Duration(seconds:1); | |
| 1404 } else { | |
| 1405 // Too many retries. Give up. | |
| 1406 // | |
| 1407 // TODO(turnidge): Kill the debugged process here? | |
| 1408 print('Timed out waiting for debugger to start.\nError: $e'); | |
| 1409 return closeVmSocket(); | |
| 1410 } | |
| 1411 // Wait and retry. | |
| 1412 return new Future.delayed(delay, () { | |
| 1413 openVmSocket(attempt + 1); | |
| 1414 }); | |
| 1415 } | |
| 1416 | |
| 1417 | |
| 1418 Future closeVmSocket() { | |
| 1419 if (vmSubscription == null) { | |
| 1420 // Already closed, nothing to do. | |
| 1421 assert(vmSock == null); | |
| 1422 return new Future.value(); | |
| 1423 } | |
| 1424 | |
| 1425 isDebugging = false; | |
| 1426 var subscription = vmSubscription; | |
| 1427 var sock = vmSock; | |
| 1428 | |
| 1429 // Wait for the socket to close and the subscription to be | |
| 1430 // cancelled. Perhaps overkill, but it means we know these will be | |
| 1431 // done. | |
| 1432 // | |
| 1433 // This is uglier than it needs to be since cancel can return null. | |
| 1434 var cleanupFutures = [sock.close()]; | |
| 1435 var future = subscription.cancel(); | |
| 1436 if (future != null) { | |
| 1437 cleanupFutures.add(future); | |
| 1438 } | |
| 1439 | |
| 1440 vmSubscription = null; | |
| 1441 vmSock = null; | |
| 1442 outstandingCommands = null; | |
| 1443 return Future.wait(cleanupFutures); | |
| 1444 } | |
| 1445 | |
| 1446 void debuggerError(self, parent, zone, error, StackTrace trace) { | |
| 1447 print('\n--------\nExiting due to unexpected error:\n' | |
| 1448 ' $error\n$trace\n'); | |
| 1449 debuggerQuit(); | |
| 1450 } | |
| 1451 | |
| 1452 Future debuggerQuit() { | |
| 1453 // Kill target process, if any. | |
| 1454 if (targetProcess != null) { | |
| 1455 if (!targetProcess.kill()) { | |
| 1456 print('Unable to kill process ${targetProcess.pid}'); | |
| 1457 } | |
| 1458 } | |
| 1459 | |
| 1460 // Restore terminal settings, close connections. | |
| 1461 return Future.wait([closeCommando(), closeVmSocket()]).then((_) { | |
| 1462 exit(0); | |
| 1463 | |
| 1464 // Unreachable. | |
| 1465 return new Future.value(); | |
| 1466 }); | |
| 1467 } | |
| 1468 | |
| 1469 | |
| 1470 void parseArgs(List<String> args) { | |
| 1471 int pos = 0; | |
| 1472 settings['vm'] = Platform.executable; | |
| 1473 while (pos < args.length && args[pos].startsWith('-')) { | |
| 1474 pos++; | |
| 1475 } | |
| 1476 if (pos < args.length) { | |
| 1477 settings['vmargs'] = args.getRange(0, pos).join(' '); | |
| 1478 settings['script'] = args[pos]; | |
| 1479 settings['args'] = args.getRange(pos + 1, args.length).join(' '); | |
| 1480 } | |
| 1481 } | |
| 1482 | |
| 1483 void main(List<String> args) { | |
| 1484 // Setup a zone which will exit the debugger cleanly on any uncaught | |
| 1485 // exception. | |
| 1486 var zone = Zone.ROOT.fork(specification:new ZoneSpecification( | |
| 1487 handleUncaughtError: debuggerError)); | |
| 1488 | |
| 1489 zone.run(() { | |
| 1490 parseArgs(args); | |
| 1491 cmdo = new Commando(completer: debuggerCommandCompleter); | |
| 1492 cmdSubscription = cmdo.commands.listen(processCommand, | |
| 1493 onError: processError, | |
| 1494 onDone: processDone); | |
| 1495 }); | |
| 1496 } | |
| OLD | NEW |