| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2015, the Dartino 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.md file. | |
| 4 | |
| 5 part of fletch.vm_session; | |
| 6 | |
| 7 const String BANNER = """ | |
| 8 Starting session. Type 'help' for a list of commands. | |
| 9 """; | |
| 10 | |
| 11 const String HELP = """ | |
| 12 Commands: | |
| 13 'help' show list of commands | |
| 14 'r'/'run' start program | |
| 15 'b [method name] [bytecode index]' set breakpoint | |
| 16 'bf <file> [line] [column]' set breakpoint | |
| 17 'bf <file> [line] [pattern]' set breakpoint on first occurrence of | |
| 18 the string pattern on the indicated line | |
| 19 'd <breakpoint id>' delete breakpoint | |
| 20 'lb' list breakpoints | |
| 21 's' step until next expression, | |
| 22 enters method invocations | |
| 23 'n' step until next expression, | |
| 24 does not enter method invocations | |
| 25 'fibers', 'lf' list all process fibers | |
| 26 'finish' finish current method (step out) | |
| 27 'restart' restart the selected frame | |
| 28 'sb' step bytecode, enters method invocations | |
| 29 'nb' step over bytecode, does not enter metho
d invocations | |
| 30 'c' continue execution | |
| 31 'bt' backtrace | |
| 32 'f <n>' select frame | |
| 33 'l' list source for frame | |
| 34 'p <name>' print the value of local variable | |
| 35 'p *<name>' print the structure of local variable | |
| 36 'p' print the values of all locals | |
| 37 'processes', 'lp' list all processes | |
| 38 'disasm' disassemble code for frame | |
| 39 't <flag>' toggle one of the flags: | |
| 40 - 'internal' : show internal frames | |
| 41 'q'/'quit' quit the session | |
| 42 """; | |
| 43 | |
| 44 class InputHandler { | |
| 45 final Session session; | |
| 46 final Stream<String> stream; | |
| 47 final bool echo; | |
| 48 final Uri base; | |
| 49 | |
| 50 String previousLine = ''; | |
| 51 | |
| 52 int processPagingCount = 10; | |
| 53 int processPagingCurrent = 0; | |
| 54 | |
| 55 InputHandler(this.session, this.stream, this.echo, this.base); | |
| 56 | |
| 57 void printPrompt() => session.writeStdout('> '); | |
| 58 | |
| 59 writeStdout(String s) => session.writeStdout(s); | |
| 60 | |
| 61 writeStdoutLine(String s) => session.writeStdout("$s\n"); | |
| 62 | |
| 63 Future handleLine(StreamIterator stream, SessionState state) async { | |
| 64 String line = stream.current; | |
| 65 if (line.isEmpty) line = previousLine; | |
| 66 if (line.isEmpty) { | |
| 67 printPrompt(); | |
| 68 return; | |
| 69 } | |
| 70 if (echo) writeStdoutLine(line); | |
| 71 List<String> commandComponents = | |
| 72 line.split(' ').where((s) => s.isNotEmpty).toList(); | |
| 73 String command = commandComponents[0]; | |
| 74 switch (command) { | |
| 75 case 'help': | |
| 76 writeStdoutLine(HELP); | |
| 77 break; | |
| 78 case 'b': | |
| 79 var method = | |
| 80 (commandComponents.length > 1) ? commandComponents[1] : 'main'; | |
| 81 var bci = | |
| 82 (commandComponents.length > 2) ? commandComponents[2] : '0'; | |
| 83 bci = int.parse(bci, onError: (_) => null); | |
| 84 if (bci == null) { | |
| 85 writeStdoutLine('### invalid bytecode index: $bci'); | |
| 86 break; | |
| 87 } | |
| 88 List<Breakpoint> breakpoints = | |
| 89 await session.setBreakpoint(methodName: method, bytecodeIndex: bci); | |
| 90 if (breakpoints != null) { | |
| 91 for (Breakpoint breakpoint in breakpoints) { | |
| 92 writeStdoutLine("breakpoint set: $breakpoint"); | |
| 93 } | |
| 94 } else { | |
| 95 writeStdoutLine( | |
| 96 "### failed to set breakpoint at method: $method index: $bci"); | |
| 97 } | |
| 98 break; | |
| 99 case 'bf': | |
| 100 var file = | |
| 101 (commandComponents.length > 1) ? commandComponents[1] : ''; | |
| 102 var line = | |
| 103 (commandComponents.length > 2) ? commandComponents[2] : '1'; | |
| 104 var columnOrPattern = | |
| 105 (commandComponents.length > 3) ? commandComponents[3] : '1'; | |
| 106 | |
| 107 List<Uri> files = <Uri>[]; | |
| 108 | |
| 109 if (await new File.fromUri(base.resolve(file)).exists()) { | |
| 110 // If the supplied file resolved directly to a file use it. | |
| 111 files.add(base.resolve(file)); | |
| 112 } else { | |
| 113 // Otherwise search for possible matches. | |
| 114 List<Uri> matches = session.findSourceFiles(file).toList()..sort( | |
| 115 (a, b) => a.toString().compareTo(b.toString())); | |
| 116 Iterable<int> selection = await select( | |
| 117 stream, | |
| 118 "Multiple matches for file pattern $file", | |
| 119 matches.map((uri) => | |
| 120 uri.toString().replaceFirst(base.toString(), ''))); | |
| 121 for (int selected in selection) { | |
| 122 files.add(matches.elementAt(selected)); | |
| 123 } | |
| 124 } | |
| 125 | |
| 126 if (files.isEmpty) { | |
| 127 writeStdoutLine('### no matching file found for: $file'); | |
| 128 break; | |
| 129 } | |
| 130 | |
| 131 line = int.parse(line, onError: (_) => null); | |
| 132 if (line == null || line < 1) { | |
| 133 writeStdoutLine('### invalid line number: $line'); | |
| 134 break; | |
| 135 } | |
| 136 | |
| 137 List<Breakpoint> breakpoints = <Breakpoint>[]; | |
| 138 int columnNumber = int.parse(columnOrPattern, onError: (_) => null); | |
| 139 if (columnNumber == null) { | |
| 140 for (Uri fileUri in files) { | |
| 141 Breakpoint breakpoint = await session.setFileBreakpointFromPattern( | |
| 142 fileUri, line, columnOrPattern); | |
| 143 if (breakpoint == null) { | |
| 144 writeStdoutLine( | |
| 145 '### failed to set breakpoint for pattern $columnOrPattern ' + | |
| 146 'on $fileUri:$line'); | |
| 147 } else { | |
| 148 breakpoints.add(breakpoint); | |
| 149 } | |
| 150 } | |
| 151 } else if (columnNumber < 1) { | |
| 152 writeStdoutLine('### invalid column number: $columnOrPattern'); | |
| 153 break; | |
| 154 } else { | |
| 155 for (Uri fileUri in files) { | |
| 156 Breakpoint breakpoint = | |
| 157 await session.setFileBreakpoint(fileUri, line, columnNumber); | |
| 158 if (breakpoint == null) { | |
| 159 writeStdoutLine( | |
| 160 '### failed to set breakpoint ' + | |
| 161 'on $fileUri:$line:$columnNumber'); | |
| 162 } else { | |
| 163 breakpoints.add(breakpoint); | |
| 164 } | |
| 165 } | |
| 166 } | |
| 167 if (breakpoints.isNotEmpty) { | |
| 168 for (Breakpoint breakpoint in breakpoints) { | |
| 169 writeStdoutLine("breakpoint set: $breakpoint"); | |
| 170 } | |
| 171 } else { | |
| 172 writeStdoutLine( | |
| 173 "### failed to set any breakpoints"); | |
| 174 } | |
| 175 break; | |
| 176 case 'bt': | |
| 177 if (!checkLoaded('cannot print backtrace')) { | |
| 178 break; | |
| 179 } | |
| 180 BackTrace backtrace = await session.backTrace(); | |
| 181 if (backtrace == null) { | |
| 182 writeStdoutLine('### failed to get backtrace for current program'); | |
| 183 } else { | |
| 184 writeStdout(backtrace.format()); | |
| 185 } | |
| 186 break; | |
| 187 case 'f': | |
| 188 var frame = | |
| 189 (commandComponents.length > 1) ? commandComponents[1] : "-1"; | |
| 190 frame = int.parse(frame, onError: (_) => null); | |
| 191 if (frame == null || !session.selectFrame(frame)) { | |
| 192 writeStdoutLine('### invalid frame number: $frame'); | |
| 193 } | |
| 194 break; | |
| 195 case 'l': | |
| 196 if (!checkLoaded('nothing to list')) { | |
| 197 break; | |
| 198 } | |
| 199 BackTrace trace = await session.backTrace(); | |
| 200 String listing = trace != null ? trace.list(state) : null; | |
| 201 if (listing != null) { | |
| 202 writeStdoutLine(listing); | |
| 203 } else { | |
| 204 writeStdoutLine("### failed listing source"); | |
| 205 } | |
| 206 break; | |
| 207 case 'disasm': | |
| 208 if (checkLoaded('cannot show bytecodes')) { | |
| 209 BackTrace backtrace = await session.backTrace(); | |
| 210 String disassembly = backtrace != null ? backtrace.disasm() : null; | |
| 211 if (disassembly != null) { | |
| 212 writeStdout(disassembly); | |
| 213 } else { | |
| 214 writeStdoutLine( | |
| 215 "### could not disassemble source for current frame"); | |
| 216 } | |
| 217 } | |
| 218 break; | |
| 219 case 'c': | |
| 220 if (checkRunning('cannot continue')) { | |
| 221 await handleProcessStopResponse(await session.cont(), state); | |
| 222 } | |
| 223 break; | |
| 224 case 'd': | |
| 225 var id = (commandComponents.length > 1) ? commandComponents[1] : null; | |
| 226 id = int.parse(id, onError: (_) => null); | |
| 227 if (id == null) { | |
| 228 writeStdoutLine('### invalid breakpoint number: $id'); | |
| 229 break; | |
| 230 } | |
| 231 Breakpoint breakpoint = await session.deleteBreakpoint(id); | |
| 232 if (breakpoint == null) { | |
| 233 writeStdoutLine("### invalid breakpoint id: $id"); | |
| 234 break; | |
| 235 } | |
| 236 writeStdoutLine("### deleted breakpoint: $breakpoint"); | |
| 237 break; | |
| 238 case 'processes': | |
| 239 case 'lp': | |
| 240 if (checkRunning('cannot list processes')) { | |
| 241 // Reset current paging point if not continuing from an 'lp' command. | |
| 242 if (previousLine != 'lp' && previousLine != 'processes') { | |
| 243 processPagingCurrent = 0; | |
| 244 } | |
| 245 | |
| 246 List<int> processes = await session.processes(); | |
| 247 processes.sort(); | |
| 248 | |
| 249 int count = processes.length; | |
| 250 int start = processPagingCurrent; | |
| 251 int end; | |
| 252 if (start + processPagingCount < count) { | |
| 253 processPagingCurrent += processPagingCount; | |
| 254 end = processPagingCurrent; | |
| 255 } else { | |
| 256 processPagingCurrent = 0; | |
| 257 end = count; | |
| 258 } | |
| 259 | |
| 260 if (processPagingCount < count) { | |
| 261 writeStdout("displaying range [$start;${end-1}] "); | |
| 262 writeStdoutLine("of $count processes"); | |
| 263 } | |
| 264 for (int i = start; i < end; ++i) { | |
| 265 int processId = processes[i]; | |
| 266 BackTrace stack = await session.processStack(processId); | |
| 267 writeStdoutLine('\nprocess ${processId}'); | |
| 268 writeStdout(stack.format()); | |
| 269 } | |
| 270 writeStdoutLine(''); | |
| 271 } | |
| 272 break; | |
| 273 case 'fibers': | |
| 274 case 'lf': | |
| 275 if (checkRunning('cannot show fibers')) { | |
| 276 List<BackTrace> traces = await session.fibers(); | |
| 277 for (int fiber = 0; fiber < traces.length; ++fiber) { | |
| 278 writeStdoutLine('\nfiber $fiber'); | |
| 279 writeStdout(traces[fiber].format()); | |
| 280 } | |
| 281 writeStdoutLine(''); | |
| 282 } | |
| 283 break; | |
| 284 case 'finish': | |
| 285 if (checkRunning('cannot finish method')) { | |
| 286 await handleProcessStopResponse(await session.stepOut(), state); | |
| 287 } | |
| 288 break; | |
| 289 case 'restart': | |
| 290 if (!checkLoaded('cannot restart')) { | |
| 291 break; | |
| 292 } | |
| 293 BackTrace trace = await session.backTrace(); | |
| 294 if (trace == null) { | |
| 295 writeStdoutLine("### cannot restart when nothing is executing"); | |
| 296 break; | |
| 297 } | |
| 298 if (trace.length <= 1) { | |
| 299 writeStdoutLine("### cannot restart entry frame"); | |
| 300 break; | |
| 301 } | |
| 302 await handleProcessStopResponse(await session.restart(), state); | |
| 303 break; | |
| 304 case 'lb': | |
| 305 List<Breakpoint> breakpoints = session.breakpoints(); | |
| 306 if (breakpoints == null || breakpoints.isEmpty) { | |
| 307 writeStdoutLine('### no breakpoints'); | |
| 308 } else { | |
| 309 writeStdoutLine("### breakpoints:"); | |
| 310 for (var bp in breakpoints) { | |
| 311 writeStdoutLine('$bp'); | |
| 312 } | |
| 313 } | |
| 314 break; | |
| 315 case 'p': | |
| 316 if (!checkLoaded('nothing to print')) { | |
| 317 break; | |
| 318 } | |
| 319 if (commandComponents.length <= 1) { | |
| 320 List<RemoteObject> variables = await session.processAllVariables(); | |
| 321 if (variables.isEmpty) { | |
| 322 writeStdoutLine('### No variables in scope'); | |
| 323 } else { | |
| 324 for (RemoteObject variable in variables) { | |
| 325 writeStdoutLine(session.remoteObjectToString(variable)); | |
| 326 } | |
| 327 } | |
| 328 break; | |
| 329 } | |
| 330 String variableName = commandComponents[1]; | |
| 331 RemoteObject variable; | |
| 332 if (variableName.startsWith('*')) { | |
| 333 variableName = variableName.substring(1); | |
| 334 variable = await session.processVariableStructure(variableName); | |
| 335 } else { | |
| 336 variable = await session.processVariable(variableName); | |
| 337 } | |
| 338 if (variable == null) { | |
| 339 writeStdoutLine('### no such variable: $variableName'); | |
| 340 } else { | |
| 341 writeStdoutLine(session.remoteObjectToString(variable)); | |
| 342 } | |
| 343 break; | |
| 344 case 'q': | |
| 345 case 'quit': | |
| 346 await session.terminateSession(); | |
| 347 break; | |
| 348 case 'r': | |
| 349 case 'run': | |
| 350 if (checkNotLoaded("use 'restart' to run again")) { | |
| 351 await handleProcessStopResponse(await session.debugRun(), state); | |
| 352 } | |
| 353 break; | |
| 354 case 's': | |
| 355 if (checkRunning('cannot step to next expression')) { | |
| 356 await handleProcessStopResponse(await session.step(), state); | |
| 357 } | |
| 358 break; | |
| 359 case 'n': | |
| 360 if (checkRunning('cannot go to next expression')) { | |
| 361 await handleProcessStopResponse(await session.stepOver(), state); | |
| 362 } | |
| 363 break; | |
| 364 case 'sb': | |
| 365 if (checkRunning('cannot step bytecode')) { | |
| 366 await handleProcessStopResponse(await session.stepBytecode(), state); | |
| 367 } | |
| 368 break; | |
| 369 case 'nb': | |
| 370 if (checkRunning('cannot step over bytecode')) { | |
| 371 await handleProcessStopResponse( | |
| 372 await session.stepOverBytecode(), state); | |
| 373 } | |
| 374 break; | |
| 375 case 't': | |
| 376 String toggle; | |
| 377 if (commandComponents.length > 1) { | |
| 378 toggle = commandComponents[1]; | |
| 379 } | |
| 380 switch (toggle) { | |
| 381 case 'internal': | |
| 382 bool internalVisible = session.toggleInternal(); | |
| 383 writeStdoutLine( | |
| 384 '### internal frame visibility set to: $internalVisible'); | |
| 385 break; | |
| 386 case 'verbose': | |
| 387 bool verbose = session.toggleVerbose(); | |
| 388 writeStdoutLine('### verbose printing set to: $verbose'); | |
| 389 break; | |
| 390 default: | |
| 391 writeStdoutLine('### invalid flag $toggle'); | |
| 392 break; | |
| 393 } | |
| 394 break; | |
| 395 default: | |
| 396 writeStdoutLine('### unknown command: $command'); | |
| 397 break; | |
| 398 } | |
| 399 previousLine = line; | |
| 400 if (!session.terminated) printPrompt(); | |
| 401 } | |
| 402 | |
| 403 // This method is used to deal with the stopped process command responses | |
| 404 // that can be returned when sending the Fletch VM a command request. | |
| 405 Future handleProcessStopResponse( | |
| 406 VmCommand response, | |
| 407 SessionState state) async { | |
| 408 String output = await session.processStopResponseToString(response, state); | |
| 409 if (output != null && output.isNotEmpty) { | |
| 410 writeStdout(output); | |
| 411 } | |
| 412 } | |
| 413 | |
| 414 bool checkLoaded([String postfix]) { | |
| 415 if (!session.loaded) { | |
| 416 String prefix = '### process not loaded'; | |
| 417 writeStdoutLine(postfix != null ? '$prefix, $postfix' : prefix); | |
| 418 } | |
| 419 return session.loaded; | |
| 420 } | |
| 421 | |
| 422 bool checkNotLoaded([String postfix]) { | |
| 423 if (session.loaded) { | |
| 424 String prefix = '### process already loaded'; | |
| 425 writeStdoutLine(postfix != null ? '$prefix, $postfix' : prefix); | |
| 426 } | |
| 427 return !session.loaded; | |
| 428 } | |
| 429 | |
| 430 bool checkRunning([String postfix]) { | |
| 431 if (!session.running) { | |
| 432 String prefix = '### process not running'; | |
| 433 writeStdoutLine(postfix != null ? '$prefix, $postfix' : prefix); | |
| 434 } | |
| 435 return session.running; | |
| 436 } | |
| 437 | |
| 438 bool checkNotRunning([String postfix]) { | |
| 439 if (session.running) { | |
| 440 String prefix = '### process already running'; | |
| 441 writeStdoutLine(postfix != null ? '$prefix, $postfix' : prefix); | |
| 442 } | |
| 443 return !session.running; | |
| 444 } | |
| 445 | |
| 446 Future<int> run(SessionState state) async { | |
| 447 writeStdoutLine(BANNER); | |
| 448 printPrompt(); | |
| 449 StreamIterator streamIterator = new StreamIterator(stream); | |
| 450 while (await streamIterator.moveNext()) { | |
| 451 try { | |
| 452 await handleLine(streamIterator, state); | |
| 453 } catch (e, s) { | |
| 454 Future cancel = streamIterator.cancel()?.catchError((_) {}); | |
| 455 if (!session.terminated) { | |
| 456 await session.terminateSession().catchError((_) {}); | |
| 457 } | |
| 458 await cancel; | |
| 459 return new Future.error(e, s); | |
| 460 } | |
| 461 if (session.terminated) { | |
| 462 await streamIterator.cancel(); | |
| 463 } | |
| 464 } | |
| 465 if (!session.terminated) await session.terminateSession(); | |
| 466 return 0; | |
| 467 } | |
| 468 | |
| 469 // Prompt the user to select among a set of choices. | |
| 470 // Returns a set of indexes that are the chosen indexes from the input set. | |
| 471 // If the size of choices is less then two, then the result is that the full | |
| 472 // input set is selected without prompting the user. Otherwise the user is | |
| 473 // interactively prompted to choose a selection. | |
| 474 Future<Iterable<int>> select( | |
| 475 StreamIterator stream, | |
| 476 String message, | |
| 477 Iterable<String> choices) async { | |
| 478 int length = choices.length; | |
| 479 if (length == 0) return <int>[]; | |
| 480 if (length == 1) return <int>[0]; | |
| 481 writeStdout("$message. "); | |
| 482 writeStdoutLine("Please select from the following choices:"); | |
| 483 int i = 1; | |
| 484 int pad = 2 + "$length".length; | |
| 485 for (String choice in choices) { | |
| 486 writeStdoutLine("${i++}".padLeft(pad) + ": $choice"); | |
| 487 } | |
| 488 writeStdoutLine('a'.padLeft(pad) + ": all of the above"); | |
| 489 writeStdoutLine('n'.padLeft(pad) + ": none of the above"); | |
| 490 while (true) { | |
| 491 printPrompt(); | |
| 492 bool hasNext = await stream.moveNext(); | |
| 493 if (!hasNext) { | |
| 494 writeStdoutLine("### failed to read choice input"); | |
| 495 return <int>[]; | |
| 496 } | |
| 497 String line = stream.current; | |
| 498 if (echo) writeStdoutLine(line); | |
| 499 if (line == 'n') { | |
| 500 return <int>[]; | |
| 501 } | |
| 502 if (line == 'a') { | |
| 503 return new List<int>.generate(length, (i) => i); | |
| 504 } | |
| 505 int choice = int.parse(line, onError: (_) => 0); | |
| 506 if (choice > 0 && choice <= length) { | |
| 507 return <int>[choice - 1]; | |
| 508 } | |
| 509 writeStdoutLine("Invalid choice: $choice"); | |
| 510 writeStdoutLine("Please select a number between 1 and $length, " + | |
| 511 "'a' for all, or 'n' for none."); | |
| 512 } | |
| 513 } | |
| 514 } | |
| OLD | NEW |