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 |