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 library fletchc.worker.developer; | |
6 | |
7 import 'dart:async' show | |
8 Future, | |
9 Stream, | |
10 StreamController, | |
11 Timer; | |
12 | |
13 import 'dart:convert' show | |
14 JSON, | |
15 JsonEncoder, | |
16 UTF8; | |
17 | |
18 import 'dart:io' show | |
19 Directory, | |
20 File, | |
21 FileSystemEntity, | |
22 InternetAddress, | |
23 Platform, | |
24 Process, | |
25 Socket, | |
26 SocketException; | |
27 | |
28 import 'package:sdk_library_metadata/libraries.dart' show | |
29 Category; | |
30 | |
31 import 'package:sdk_services/sdk_services.dart' show | |
32 OutputService, | |
33 SDKServices; | |
34 | |
35 import 'package:fletch_agent/agent_connection.dart' show | |
36 AgentConnection, | |
37 AgentException, | |
38 VmData; | |
39 | |
40 import 'package:fletch_agent/messages.dart' show | |
41 AGENT_DEFAULT_PORT, | |
42 MessageDecodeException; | |
43 | |
44 import 'package:mdns/mdns.dart' show | |
45 MDnsClient, | |
46 ResourceRecord, | |
47 RRType; | |
48 | |
49 import 'package:path/path.dart' show | |
50 join; | |
51 | |
52 import '../../vm_commands.dart' show | |
53 VmCommandCode, | |
54 ConnectionError, | |
55 Debugging, | |
56 HandShakeResult, | |
57 ProcessBacktrace, | |
58 ProcessBacktraceRequest, | |
59 ProcessRun, | |
60 ProcessSpawnForMain, | |
61 SessionEnd, | |
62 WriteSnapshotResult; | |
63 | |
64 import '../../program_info.dart' show | |
65 Configuration, | |
66 ProgramInfo, | |
67 ProgramInfoBinary, | |
68 ProgramInfoJson, | |
69 buildProgramInfo; | |
70 | |
71 import '../hub/session_manager.dart' show | |
72 FletchVm, | |
73 SessionState, | |
74 Sessions; | |
75 | |
76 import '../hub/client_commands.dart' show | |
77 ClientCommandCode, | |
78 handleSocketErrors; | |
79 | |
80 import '../verbs/infrastructure.dart' show | |
81 ClientCommand, | |
82 CommandSender, | |
83 DiagnosticKind, | |
84 FletchCompiler, | |
85 FletchDelta, | |
86 IncrementalCompiler, | |
87 WorkerConnection, | |
88 IsolatePool, | |
89 Session, | |
90 SharedTask, | |
91 StreamIterator, | |
92 throwFatalError; | |
93 | |
94 import '../../incremental/fletchc_incremental.dart' show | |
95 IncrementalCompilationFailed, | |
96 IncrementalMode, | |
97 parseIncrementalMode, | |
98 unparseIncrementalMode; | |
99 | |
100 export '../../incremental/fletchc_incremental.dart' show | |
101 IncrementalMode; | |
102 | |
103 import '../../fletch_compiler.dart' show fletchDeviceType; | |
104 | |
105 import '../hub/exit_codes.dart' as exit_codes; | |
106 | |
107 import '../../fletch_system.dart' show | |
108 FletchFunction, | |
109 FletchSystem; | |
110 | |
111 import '../../bytecodes.dart' show | |
112 Bytecode, | |
113 MethodEnd; | |
114 | |
115 import '../diagnostic.dart' show | |
116 throwInternalError; | |
117 | |
118 import '../guess_configuration.dart' show | |
119 executable, | |
120 fletchVersion, | |
121 guessFletchVm; | |
122 | |
123 import '../device_type.dart' show | |
124 DeviceType, | |
125 parseDeviceType, | |
126 unParseDeviceType; | |
127 | |
128 export '../device_type.dart' show | |
129 DeviceType; | |
130 | |
131 import '../please_report_crash.dart' show | |
132 pleaseReportCrash; | |
133 | |
134 import '../../debug_state.dart' as debug show | |
135 RemoteObject, | |
136 BackTrace; | |
137 | |
138 typedef Future<Null> ClientEventHandler(Session session); | |
139 | |
140 Uri configFileUri; | |
141 | |
142 Future<Socket> connect( | |
143 String host, | |
144 int port, | |
145 DiagnosticKind kind, | |
146 String socketDescription, | |
147 SessionState state) async { | |
148 // We are using .catchError rather than try/catch because we have seen | |
149 // incorrect stack traces using the latter. | |
150 Socket socket = await Socket.connect(host, port).catchError( | |
151 (SocketException error) { | |
152 String message = error.message; | |
153 if (error.osError != null) { | |
154 message = error.osError.message; | |
155 } | |
156 throwFatalError(kind, address: '$host:$port', message: message); | |
157 }, test: (e) => e is SocketException); | |
158 handleSocketErrors(socket, socketDescription, log: (String info) { | |
159 state.log("Connected to TCP $socketDescription $info"); | |
160 }); | |
161 return socket; | |
162 } | |
163 | |
164 Future<AgentConnection> connectToAgent(SessionState state) async { | |
165 // TODO(wibling): need to make sure the agent is running. | |
166 assert(state.settings.deviceAddress != null); | |
167 String host = state.settings.deviceAddress.host; | |
168 int agentPort = state.settings.deviceAddress.port; | |
169 Socket socket = await connect( | |
170 host, agentPort, DiagnosticKind.socketAgentConnectError, | |
171 "agentSocket", state); | |
172 return new AgentConnection(socket); | |
173 } | |
174 | |
175 /// Return the result of a function in the context of an open [AgentConnection]. | |
176 /// | |
177 /// The result is a [Future] of this value. | |
178 /// This function handles [AgentException] and [MessageDecodeException]. | |
179 Future withAgentConnection( | |
180 SessionState state, | |
181 Future f(AgentConnection connection)) async { | |
182 AgentConnection connection = await connectToAgent(state); | |
183 try { | |
184 return await f(connection); | |
185 } on AgentException catch (error) { | |
186 throwFatalError( | |
187 DiagnosticKind.socketAgentReplyError, | |
188 address: '${connection.socket.remoteAddress.host}:' | |
189 '${connection.socket.remotePort}', | |
190 message: error.message); | |
191 } on MessageDecodeException catch (error) { | |
192 throwFatalError( | |
193 DiagnosticKind.socketAgentReplyError, | |
194 address: '${connection.socket.remoteAddress.host}:' | |
195 '${connection.socket.remotePort}', | |
196 message: error.message); | |
197 } finally { | |
198 disconnectFromAgent(connection); | |
199 } | |
200 } | |
201 | |
202 void disconnectFromAgent(AgentConnection connection) { | |
203 assert(connection.socket != null); | |
204 connection.socket.close(); | |
205 } | |
206 | |
207 Future<Null> checkAgentVersion(Uri base, SessionState state) async { | |
208 String deviceFletchVersion = await withAgentConnection(state, | |
209 (connection) => connection.fletchVersion()); | |
210 Uri packageFile = await lookForAgentPackage(base, version: fletchVersion); | |
211 String fixit; | |
212 if (packageFile != null) { | |
213 fixit = "Try running\n" | |
214 " 'fletch x-upgrade agent in session ${state.name}'."; | |
215 } else { | |
216 fixit = "Try downloading a matching SDK and running\n" | |
217 " 'fletch x-upgrade agent in session ${state.name}'\n" | |
218 "from the SDK's root directory."; | |
219 } | |
220 | |
221 if (fletchVersion != deviceFletchVersion) { | |
222 throwFatalError(DiagnosticKind.agentVersionMismatch, | |
223 userInput: fletchVersion, | |
224 additionalUserInput: deviceFletchVersion, | |
225 fixit: fixit); | |
226 } | |
227 } | |
228 | |
229 Future<Null> startAndAttachViaAgent(Uri base, SessionState state) async { | |
230 // TODO(wibling): integrate with the FletchVm class, e.g. have a | |
231 // AgentFletchVm and LocalFletchVm that both share the same interface | |
232 // where the former is interacting with the agent. | |
233 await checkAgentVersion(base, state); | |
234 VmData vmData = await withAgentConnection(state, | |
235 (connection) => connection.startVm()); | |
236 state.fletchAgentVmId = vmData.id; | |
237 String host = state.settings.deviceAddress.host; | |
238 await attachToVm(host, vmData.port, state); | |
239 await state.session.disableVMStandardOutput(); | |
240 } | |
241 | |
242 Future<Null> startAndAttachDirectly(SessionState state, Uri base) async { | |
243 String fletchVmPath = state.compilerHelper.fletchVm.toFilePath(); | |
244 state.fletchVm = await FletchVm.start(fletchVmPath, workingDirectory: base); | |
245 await attachToVm(state.fletchVm.host, state.fletchVm.port, state); | |
246 await state.session.disableVMStandardOutput(); | |
247 } | |
248 | |
249 Future<Null> attachToVm(String host, int port, SessionState state) async { | |
250 Socket socket = await connect( | |
251 host, port, DiagnosticKind.socketVmConnectError, "vmSocket", state); | |
252 | |
253 Session session = new Session(socket, state.compiler, state.stdoutSink, | |
254 state.stderrSink, null); | |
255 | |
256 // Perform handshake with VM which validates that VM and compiler | |
257 // have the same versions. | |
258 HandShakeResult handShakeResult = await session.handShake(fletchVersion); | |
259 if (handShakeResult == null) { | |
260 throwFatalError(DiagnosticKind.handShakeFailed, address: '$host:$port'); | |
261 } | |
262 if (!handShakeResult.success) { | |
263 throwFatalError(DiagnosticKind.versionMismatch, | |
264 address: '$host:$port', | |
265 userInput: fletchVersion, | |
266 additionalUserInput: handShakeResult.version); | |
267 } | |
268 | |
269 // Enable debugging to be able to communicate with VM when there | |
270 // are errors. | |
271 await session.runCommand(const Debugging()); | |
272 | |
273 state.session = session; | |
274 } | |
275 | |
276 Future<int> compile( | |
277 Uri script, | |
278 SessionState state, | |
279 Uri base, | |
280 {bool analyzeOnly: false, | |
281 bool fatalIncrementalFailures: false}) async { | |
282 IncrementalCompiler compiler = state.compiler; | |
283 if (!compiler.isProductionModeEnabled) { | |
284 state.resetCompiler(); | |
285 } | |
286 Uri firstScript = state.script; | |
287 List<FletchDelta> previousResults = state.compilationResults; | |
288 | |
289 FletchDelta newResult; | |
290 try { | |
291 if (analyzeOnly) { | |
292 state.resetCompiler(); | |
293 state.log("Analyzing '$script'"); | |
294 return await compiler.analyze(script, base); | |
295 } else if (previousResults.isEmpty) { | |
296 state.script = script; | |
297 await compiler.compile(script, base); | |
298 newResult = compiler.computeInitialDelta(); | |
299 } else { | |
300 try { | |
301 state.log("Compiling difference from $firstScript to $script"); | |
302 newResult = await compiler.compileUpdates( | |
303 previousResults.last.system, <Uri, Uri>{firstScript: script}, | |
304 logTime: state.log, logVerbose: state.log); | |
305 } on IncrementalCompilationFailed catch (error) { | |
306 state.log(error); | |
307 state.resetCompiler(); | |
308 if (fatalIncrementalFailures) { | |
309 print(error); | |
310 state.log( | |
311 "Aborting compilation due to --fatal-incremental-failures..."); | |
312 return exit_codes.INCREMENTAL_COMPILER_FAILED; | |
313 } | |
314 state.log("Attempting full compile..."); | |
315 state.script = script; | |
316 await compiler.compile(script, base); | |
317 newResult = compiler.computeInitialDelta(); | |
318 } | |
319 } | |
320 } catch (error, stackTrace) { | |
321 pleaseReportCrash(error, stackTrace); | |
322 return exit_codes.COMPILER_EXITCODE_CRASH; | |
323 } | |
324 if (newResult == null) { | |
325 return exit_codes.DART_VM_EXITCODE_COMPILE_TIME_ERROR; | |
326 } | |
327 | |
328 state.addCompilationResult(newResult); | |
329 | |
330 state.log("Compiled '$script' to ${newResult.commands.length} commands"); | |
331 | |
332 return 0; | |
333 } | |
334 | |
335 Future<Settings> readSettings(Uri uri) async { | |
336 if (await new File.fromUri(uri).exists()) { | |
337 String jsonLikeData = await new File.fromUri(uri).readAsString(); | |
338 return parseSettings(jsonLikeData, uri); | |
339 } else { | |
340 return null; | |
341 } | |
342 } | |
343 | |
344 Future<Uri> findFile(Uri cwd, String fileName) async { | |
345 Uri uri = cwd.resolve(fileName); | |
346 while (true) { | |
347 if (await new File.fromUri(uri).exists()) return uri; | |
348 if (uri.pathSegments.length <= 1) return null; | |
349 uri = uri.resolve('../$fileName'); | |
350 } | |
351 } | |
352 | |
353 Future<Settings> createSettings( | |
354 String sessionName, | |
355 Uri uri, | |
356 Uri cwd, | |
357 Uri configFileUri, | |
358 CommandSender commandSender, | |
359 StreamIterator<ClientCommand> commandIterator) async { | |
360 bool userProvidedSettings = uri != null; | |
361 if (!userProvidedSettings) { | |
362 // Try to find a $sessionName.fletch-settings file starting from the current | |
363 // working directory and walking up its parent directories. | |
364 uri = await findFile(cwd, '$sessionName.fletch-settings'); | |
365 | |
366 // If no $sessionName.fletch-settings file is found, try to find the | |
367 // settings template file (in the SDK or git repo) by looking for a | |
368 // .fletch-settings file starting from the fletch executable's directory | |
369 // and walking up its parent directory chain. | |
370 if (uri == null) { | |
371 uri = await findFile(executable, '.fletch-settings'); | |
372 if (uri != null) print('Using template settings file $uri'); | |
373 } | |
374 } | |
375 | |
376 Settings settings = new Settings.empty(); | |
377 if (uri != null) { | |
378 String jsonLikeData = await new File.fromUri(uri).readAsString(); | |
379 settings = parseSettings(jsonLikeData, uri); | |
380 } | |
381 if (userProvidedSettings) return settings; | |
382 | |
383 // TODO(wibling): get rid of below special handling of the sessions 'remote' | |
384 // and 'local' and come up with a fletch project concept that can contain | |
385 // these settings. | |
386 Uri packagesUri; | |
387 Address address; | |
388 switch (sessionName) { | |
389 case "remote": | |
390 uri = configFileUri.resolve("remote.fletch-settings"); | |
391 Settings remoteSettings = await readSettings(uri); | |
392 if (remoteSettings != null) return remoteSettings; | |
393 packagesUri = executable.resolve("fletch-sdk.packages"); | |
394 address = await readAddressFromUser(commandSender, commandIterator); | |
395 if (address == null) { | |
396 // Assume user aborted data entry. | |
397 return settings; | |
398 } | |
399 break; | |
400 | |
401 case "local": | |
402 uri = configFileUri.resolve("local.fletch-settings"); | |
403 Settings localSettings = await readSettings(uri); | |
404 if (localSettings != null) return localSettings; | |
405 // TODO(ahe): Use mock packages here. | |
406 packagesUri = executable.resolve("fletch-sdk.packages"); | |
407 break; | |
408 | |
409 default: | |
410 return settings; | |
411 } | |
412 | |
413 if (!await new File.fromUri(packagesUri).exists()) { | |
414 packagesUri = null; | |
415 } | |
416 settings = settings.copyWith(packages: packagesUri, deviceAddress: address); | |
417 print("Created settings file '$uri'"); | |
418 await new File.fromUri(uri).writeAsString( | |
419 "${const JsonEncoder.withIndent(' ').convert(settings)}\n"); | |
420 return settings; | |
421 } | |
422 | |
423 Future<Address> readAddressFromUser( | |
424 CommandSender commandSender, | |
425 StreamIterator<ClientCommand> commandIterator) async { | |
426 String message = "Please enter IP address of remote device " | |
427 "(press Enter to search for devices):"; | |
428 commandSender.sendStdout(message); | |
429 // The list of devices found by running discovery. | |
430 List<InternetAddress> devices = <InternetAddress>[]; | |
431 while (await commandIterator.moveNext()) { | |
432 ClientCommand command = commandIterator.current; | |
433 switch (command.code) { | |
434 case ClientCommandCode.Stdin: | |
435 if (command.data.length == 0) { | |
436 // TODO(ahe): It may be safe to return null here, but we need to | |
437 // check how this interacts with the debugger's InputHandler. | |
438 throwInternalError("Unexpected end of input"); | |
439 } | |
440 // TODO(ahe): This assumes that the user's input arrives as one | |
441 // message. It is relatively safe to assume this for a normal terminal | |
442 // session because we use canonical input processing (Unix line | |
443 // buffering), but it doesn't work in general. So we should fix that. | |
444 String line = UTF8.decode(command.data).trim(); | |
445 if (line.isEmpty && devices.isEmpty) { | |
446 commandSender.sendStdout("\n"); | |
447 // [discoverDevices] will print out the list of device with their | |
448 // IP address, hostname, and agent version. | |
449 devices = await discoverDevices(prefixWithNumber: true); | |
450 if (devices.isEmpty) { | |
451 commandSender.sendStdout( | |
452 "Couldn't find Fletch capable devices\n"); | |
453 commandSender.sendStdout(message); | |
454 } else { | |
455 if (devices.length == 1) { | |
456 commandSender.sendStdout("\n"); | |
457 commandSender.sendStdout("Press Enter to use this device"); | |
458 } else { | |
459 commandSender.sendStdout("\n"); | |
460 commandSender.sendStdout( | |
461 "Found ${devices.length} Fletch capable devices\n"); | |
462 commandSender.sendStdout( | |
463 "Please enter the number or the IP address of " | |
464 "the remote device you would like to use " | |
465 "(press Enter to use the first device): "); | |
466 } | |
467 } | |
468 } else { | |
469 bool checkedIndex = false; | |
470 if (devices.length > 0) { | |
471 if (line.isEmpty) { | |
472 return new Address(devices[0].address, AGENT_DEFAULT_PORT); | |
473 } | |
474 try { | |
475 checkedIndex = true; | |
476 int index = int.parse(line); | |
477 if (1 <= index && index <= devices.length) { | |
478 return new Address(devices[index - 1].address, | |
479 AGENT_DEFAULT_PORT); | |
480 } else { | |
481 commandSender.sendStdout("Invalid device index $line\n\n"); | |
482 commandSender.sendStdout(message); | |
483 } | |
484 } on FormatException { | |
485 // Ignore FormatException and fall through to parse as IP address. | |
486 } | |
487 } | |
488 if (!checkedIndex) { | |
489 return parseAddress(line, defaultPort: AGENT_DEFAULT_PORT); | |
490 } | |
491 } | |
492 break; | |
493 | |
494 default: | |
495 throwInternalError("Unexpected ${command.code}"); | |
496 return null; | |
497 } | |
498 } | |
499 return null; | |
500 } | |
501 | |
502 SessionState createSessionState( | |
503 String name, | |
504 Settings settings, | |
505 {Uri libraryRoot, | |
506 Uri fletchVm, | |
507 Uri nativesJson}) { | |
508 if (settings == null) { | |
509 settings = const Settings.empty(); | |
510 } | |
511 List<String> compilerOptions = const bool.fromEnvironment("fletchc-verbose") | |
512 ? <String>['--verbose'] : <String>[]; | |
513 compilerOptions.addAll(settings.options); | |
514 Uri packageConfig = settings.packages; | |
515 if (packageConfig == null) { | |
516 packageConfig = executable.resolve("fletch-sdk.packages"); | |
517 } | |
518 | |
519 DeviceType deviceType = settings.deviceType ?? | |
520 parseDeviceType(fletchDeviceType); | |
521 | |
522 String platform = (deviceType == DeviceType.embedded) | |
523 ? "fletch_embedded.platform" | |
524 : "fletch_mobile.platform"; | |
525 | |
526 FletchCompiler compilerHelper = new FletchCompiler( | |
527 options: compilerOptions, | |
528 packageConfig: packageConfig, | |
529 environment: settings.constants, | |
530 platform: platform, | |
531 libraryRoot: libraryRoot, | |
532 fletchVm: fletchVm, | |
533 nativesJson: nativesJson); | |
534 | |
535 return new SessionState( | |
536 name, compilerHelper, | |
537 compilerHelper.newIncrementalCompiler(settings.incrementalMode), | |
538 settings); | |
539 } | |
540 | |
541 Future runWithDebugger( | |
542 List<String> commands, | |
543 Session session, | |
544 SessionState state) async { | |
545 | |
546 // Method used to generate the debugger commands if none are specified. | |
547 Stream<String> inputGenerator() async* { | |
548 yield 't verbose'; | |
549 yield 'b main'; | |
550 yield 'r'; | |
551 while (!session.terminated) { | |
552 yield 's'; | |
553 } | |
554 } | |
555 | |
556 return commands.isEmpty ? | |
557 session.debug(inputGenerator(), Uri.base, state, echo: true) : | |
558 session.debug( | |
559 new Stream<String>.fromIterable(commands), Uri.base, state, | |
560 echo: true); | |
561 } | |
562 | |
563 Future<int> run( | |
564 SessionState state, | |
565 {List<String> testDebuggerCommands, | |
566 bool terminateDebugger: true}) async { | |
567 List<FletchDelta> compilationResults = state.compilationResults; | |
568 Session session = state.session; | |
569 | |
570 for (FletchDelta delta in compilationResults) { | |
571 await session.applyDelta(delta); | |
572 } | |
573 | |
574 if (testDebuggerCommands != null) { | |
575 await runWithDebugger(testDebuggerCommands, session, state); | |
576 return 0; | |
577 } | |
578 | |
579 session.silent = true; | |
580 | |
581 await session.enableDebugger(); | |
582 await session.spawnProcess(); | |
583 var command = await session.debugRun(); | |
584 | |
585 int exitCode = exit_codes.COMPILER_EXITCODE_CRASH; | |
586 if (command == null) { | |
587 await session.kill(); | |
588 await session.shutdown(); | |
589 throwInternalError("No command received from Fletch VM"); | |
590 } | |
591 | |
592 Future printException() async { | |
593 if (!session.loaded) { | |
594 print('### process not loaded, cannot print uncaught exception'); | |
595 return; | |
596 } | |
597 debug.RemoteObject exception = await session.uncaughtException(); | |
598 if (exception != null) { | |
599 print(session.exceptionToString(exception)); | |
600 } | |
601 } | |
602 | |
603 Future printTrace() async { | |
604 if (!session.loaded) { | |
605 print("### process not loaded, cannot print stacktrace and code"); | |
606 return; | |
607 } | |
608 debug.BackTrace stackTrace = await session.backTrace(); | |
609 if (stackTrace != null) { | |
610 print(stackTrace.format()); | |
611 print(stackTrace.list(state)); | |
612 } | |
613 } | |
614 | |
615 try { | |
616 switch (command.code) { | |
617 case VmCommandCode.UncaughtException: | |
618 state.log("Uncaught error"); | |
619 exitCode = exit_codes.DART_VM_EXITCODE_UNCAUGHT_EXCEPTION; | |
620 await printException(); | |
621 await printTrace(); | |
622 // TODO(ahe): Need to continue to unwind stack. | |
623 break; | |
624 case VmCommandCode.ProcessCompileTimeError: | |
625 state.log("Compile-time error"); | |
626 exitCode = exit_codes.DART_VM_EXITCODE_COMPILE_TIME_ERROR; | |
627 await printTrace(); | |
628 // TODO(ahe): Continue to unwind stack? | |
629 break; | |
630 | |
631 case VmCommandCode.ProcessTerminated: | |
632 exitCode = 0; | |
633 break; | |
634 | |
635 case VmCommandCode.ConnectionError: | |
636 state.log("Error on connection to Fletch VM: ${command.error}"); | |
637 exitCode = exit_codes.COMPILER_EXITCODE_CONNECTION_ERROR; | |
638 break; | |
639 | |
640 default: | |
641 throwInternalError("Unexpected result from Fletch VM: '$command'"); | |
642 break; | |
643 } | |
644 } finally { | |
645 if (terminateDebugger) { | |
646 await state.terminateSession(); | |
647 } else { | |
648 // If the session terminated due to a ConnectionError or the program | |
649 // finished don't reuse the state's session. | |
650 if (session.terminated) { | |
651 state.session = null; | |
652 } | |
653 session.silent = false; | |
654 } | |
655 }; | |
656 | |
657 return exitCode; | |
658 } | |
659 | |
660 Future<int> export(SessionState state, | |
661 Uri snapshot, | |
662 {bool binaryProgramInfo: false}) async { | |
663 List<FletchDelta> compilationResults = state.compilationResults; | |
664 Session session = state.session; | |
665 state.session = null; | |
666 | |
667 for (FletchDelta delta in compilationResults) { | |
668 await session.applyDelta(delta); | |
669 } | |
670 | |
671 var result = await session.writeSnapshot(snapshot.toFilePath()); | |
672 if (result is WriteSnapshotResult) { | |
673 WriteSnapshotResult snapshotResult = result; | |
674 | |
675 await session.shutdown(); | |
676 | |
677 ProgramInfo info = | |
678 buildProgramInfo(compilationResults.last.system, snapshotResult); | |
679 | |
680 File jsonFile = new File('${snapshot.toFilePath()}.info.json'); | |
681 await jsonFile.writeAsString(ProgramInfoJson.encode(info)); | |
682 | |
683 if (binaryProgramInfo) { | |
684 File binFile = new File('${snapshot.toFilePath()}.info.bin'); | |
685 await binFile.writeAsBytes(ProgramInfoBinary.encode(info)); | |
686 } | |
687 | |
688 return 0; | |
689 } else { | |
690 assert(result is ConnectionError); | |
691 print("There was a connection error while writing the snapshot."); | |
692 return exit_codes.COMPILER_EXITCODE_CONNECTION_ERROR; | |
693 } | |
694 } | |
695 | |
696 Future<int> compileAndAttachToVmThen( | |
697 CommandSender commandSender, | |
698 StreamIterator<ClientCommand> commandIterator, | |
699 SessionState state, | |
700 Uri script, | |
701 Uri base, | |
702 bool waitForVmExit, | |
703 Future<int> action(), | |
704 {ClientEventHandler eventHandler}) async { | |
705 bool startedVmDirectly = false; | |
706 List<FletchDelta> compilationResults = state.compilationResults; | |
707 if (compilationResults.isEmpty || script != null) { | |
708 if (script == null) { | |
709 throwFatalError(DiagnosticKind.noFileTarget); | |
710 } | |
711 int exitCode = await compile(script, state, base); | |
712 if (exitCode != 0) return exitCode; | |
713 compilationResults = state.compilationResults; | |
714 assert(compilationResults != null); | |
715 } | |
716 | |
717 Session session = state.session; | |
718 if (session != null && session.loaded) { | |
719 // We cannot reuse a session that has already been loaded. Loading | |
720 // currently implies that some of the code has been run. | |
721 if (state.explicitAttach) { | |
722 // If the user explicitly called 'fletch attach' we cannot | |
723 // create a new VM session since we don't know if the vm is | |
724 // running locally or remotely and if running remotely there | |
725 // is no guarantee there is an agent to start a new vm. | |
726 // | |
727 // The UserSession is invalid in its current state as the | |
728 // vm session (aka. session in the code here) has already | |
729 // been loaded and run some code. | |
730 throwFatalError(DiagnosticKind.sessionInvalidState, | |
731 sessionName: state.name); | |
732 } | |
733 state.log('Cannot reuse existing VM session, creating new.'); | |
734 await state.terminateSession(); | |
735 session = null; | |
736 } | |
737 if (session == null) { | |
738 if (state.settings.deviceAddress != null) { | |
739 await startAndAttachViaAgent(base, state); | |
740 // TODO(wibling): read stdout from agent. | |
741 } else { | |
742 startedVmDirectly = true; | |
743 await startAndAttachDirectly(state, base); | |
744 state.fletchVm.stdoutLines.listen((String line) { | |
745 commandSender.sendStdout("$line\n"); | |
746 }); | |
747 state.fletchVm.stderrLines.listen((String line) { | |
748 commandSender.sendStderr("$line\n"); | |
749 }); | |
750 } | |
751 session = state.session; | |
752 assert(session != null); | |
753 } | |
754 | |
755 eventHandler ??= defaultClientEventHandler(state, commandIterator); | |
756 setupClientInOut(state, commandSender, eventHandler); | |
757 | |
758 int exitCode = exit_codes.COMPILER_EXITCODE_CRASH; | |
759 try { | |
760 exitCode = await action(); | |
761 } catch (error, trace) { | |
762 print(error); | |
763 if (trace != null) { | |
764 print(trace); | |
765 } | |
766 } finally { | |
767 if (waitForVmExit && startedVmDirectly) { | |
768 exitCode = await state.fletchVm.exitCode; | |
769 } | |
770 state.detachCommandSender(); | |
771 } | |
772 return exitCode; | |
773 } | |
774 | |
775 void setupClientInOut( | |
776 SessionState state, | |
777 CommandSender commandSender, | |
778 ClientEventHandler eventHandler) { | |
779 // Forward output going into the state's outputSink using the passed in | |
780 // commandSender. This typically forwards output to the hub (main isolate) | |
781 // which forwards it on to stdout of the Fletch C++ client. | |
782 state.attachCommandSender(commandSender); | |
783 | |
784 // Start event handling for input passed from the Fletch C++ client. | |
785 eventHandler(state.session); | |
786 | |
787 // Let the hub (main isolate) know that event handling has been started. | |
788 commandSender.sendEventLoopStarted(); | |
789 } | |
790 | |
791 /// Return a default client event handler bound to the current session's | |
792 /// commandIterator and state. | |
793 /// This handler only takes care of signals coming from the client. | |
794 ClientEventHandler defaultClientEventHandler( | |
795 SessionState state, | |
796 StreamIterator<ClientCommand> commandIterator) { | |
797 return (Session session) async { | |
798 while (await commandIterator.moveNext()) { | |
799 ClientCommand command = commandIterator.current; | |
800 switch (command.code) { | |
801 case ClientCommandCode.Signal: | |
802 int signalNumber = command.data; | |
803 handleSignal(state, signalNumber); | |
804 break; | |
805 default: | |
806 state.log("Unhandled command from client: $command"); | |
807 } | |
808 } | |
809 }; | |
810 } | |
811 | |
812 void handleSignal(SessionState state, int signalNumber) { | |
813 state.log("Received signal $signalNumber"); | |
814 if (!state.hasRemoteVm && state.fletchVm == null) { | |
815 // This can happen if a user has attached to a vm using the "attach" verb | |
816 // in which case we don't forward the signal to the vm. | |
817 // TODO(wibling): Determine how to interpret the signal for the persistent | |
818 // process. | |
819 state.log('Signal $signalNumber ignored. VM was manually attached.'); | |
820 print('Signal $signalNumber ignored. VM was manually attached.'); | |
821 return; | |
822 } | |
823 if (state.hasRemoteVm) { | |
824 signalAgentVm(state, signalNumber); | |
825 } else { | |
826 assert(state.fletchVm.process != null); | |
827 int vmPid = state.fletchVm.process.pid; | |
828 Process.runSync("kill", ["-$signalNumber", "$vmPid"]); | |
829 } | |
830 } | |
831 | |
832 Future signalAgentVm(SessionState state, int signalNumber) async { | |
833 await withAgentConnection(state, (connection) { | |
834 return connection.signalVm(state.fletchAgentVmId, signalNumber); | |
835 }); | |
836 } | |
837 | |
838 String extractVersion(Uri uri) { | |
839 List<String> nameParts = uri.pathSegments.last.split('_'); | |
840 if (nameParts.length != 3 || nameParts[0] != 'fletch-agent') { | |
841 throwFatalError(DiagnosticKind.upgradeInvalidPackageName); | |
842 } | |
843 String version = nameParts[1]; | |
844 // create_debian_packages.py adds a '-1' after the hash in the package name. | |
845 if (version.endsWith('-1')) { | |
846 version = version.substring(0, version.length - 2); | |
847 } | |
848 return version; | |
849 } | |
850 | |
851 /// Try to locate an Fletch agent package file assuming the normal SDK layout | |
852 /// with SDK base directory [base]. | |
853 /// | |
854 /// If the parameter [version] is passed, the Uri is only returned, if | |
855 /// the version matches. | |
856 Future<Uri> lookForAgentPackage(Uri base, {String version}) async { | |
857 String platform = "raspberry-pi2"; | |
858 Uri platformUri = base.resolve("platforms/$platform"); | |
859 Directory platformDir = new Directory.fromUri(platformUri); | |
860 | |
861 // Try to locate the agent package in the SDK for the selected platform. | |
862 Uri sdkAgentPackage; | |
863 if (await platformDir.exists()) { | |
864 for (FileSystemEntity entry in platformDir.listSync()) { | |
865 Uri uri = entry.uri; | |
866 String name = uri.pathSegments.last; | |
867 if (name.startsWith('fletch-agent') && | |
868 name.endsWith('.deb') && | |
869 (version == null || extractVersion(uri) == version)) { | |
870 return uri; | |
871 } | |
872 } | |
873 } | |
874 return null; | |
875 } | |
876 | |
877 Future<Uri> readPackagePathFromUser( | |
878 Uri base, | |
879 CommandSender commandSender, | |
880 StreamIterator<ClientCommand> commandIterator) async { | |
881 Uri sdkAgentPackage = await lookForAgentPackage(base); | |
882 if (sdkAgentPackage != null) { | |
883 String path = sdkAgentPackage.toFilePath(); | |
884 commandSender.sendStdout("Found SDK package: $path\n"); | |
885 commandSender.sendStdout("Press Enter to use this package to upgrade " | |
886 "or enter the path to another package file:\n"); | |
887 } else { | |
888 commandSender.sendStdout("Please enter the path to the package file " | |
889 "you want to use:\n"); | |
890 } | |
891 | |
892 while (await commandIterator.moveNext()) { | |
893 ClientCommand command = commandIterator.current; | |
894 switch (command.code) { | |
895 case ClientCommandCode.Stdin: | |
896 if (command.data.length == 0) { | |
897 throwInternalError("Unexpected end of input"); | |
898 } | |
899 // TODO(karlklose): This assumes that the user's input arrives as one | |
900 // message. It is relatively safe to assume this for a normal terminal | |
901 // session because we use canonical input processing (Unix line | |
902 // buffering), but it doesn't work in general. So we should fix that. | |
903 String line = UTF8.decode(command.data).trim(); | |
904 if (line.isEmpty) { | |
905 return sdkAgentPackage; | |
906 } else { | |
907 return base.resolve(line); | |
908 } | |
909 break; | |
910 | |
911 default: | |
912 throwInternalError("Unexpected ${command.code}"); | |
913 return null; | |
914 } | |
915 } | |
916 return null; | |
917 } | |
918 | |
919 class Version { | |
920 final List<int> version; | |
921 final String label; | |
922 | |
923 Version(this.version, this.label) { | |
924 if (version.length != 3) { | |
925 throw new ArgumentError("version must have three parts"); | |
926 } | |
927 } | |
928 | |
929 /// Returns `true` if this version's digits are greater in lexicographical | |
930 /// order. | |
931 /// | |
932 /// We use a function instead of [operator >] because [label] is not used | |
933 /// in the comparison, but it is used in [operator ==]. | |
934 bool isGreaterThan(Version other) { | |
935 for (int part = 0; part < 3; ++part) { | |
936 if (version[part] < other.version[part]) { | |
937 return false; | |
938 } | |
939 if (version[part] > other.version[part]) { | |
940 return true; | |
941 } | |
942 } | |
943 return false; | |
944 } | |
945 | |
946 bool operator ==(other) { | |
947 return other is Version && | |
948 version[0] == other.version[0] && | |
949 version[1] == other.version[1] && | |
950 version[2] == other.version[2] && | |
951 label == other.label; | |
952 } | |
953 | |
954 int get hashCode { | |
955 return 3 * version[0] + | |
956 5 * version[1] + | |
957 7 * version[2] + | |
958 13 * label.hashCode; | |
959 } | |
960 | |
961 /// Check if this version is a bleeding edge version. | |
962 bool get isEdgeVersion => label == null ? false : label.startsWith('edge.'); | |
963 | |
964 /// Check if this version is a dev version. | |
965 bool get isDevVersion => label == null ? false : label.startsWith('dev.'); | |
966 | |
967 String toString() { | |
968 String labelPart = label == null ? '' : '-$label'; | |
969 return '${version[0]}.${version[1]}.${version[2]}$labelPart'; | |
970 } | |
971 } | |
972 | |
973 Version parseVersion(String text) { | |
974 List<String> labelParts = text.split('-'); | |
975 if (labelParts.length > 2) { | |
976 throw new ArgumentError('Not a version: $text.'); | |
977 } | |
978 List<String> digitParts = labelParts[0].split('.'); | |
979 if (digitParts.length != 3) { | |
980 throw new ArgumentError('Not a version: $text.'); | |
981 } | |
982 List<int> digits = digitParts.map(int.parse).toList(); | |
983 return new Version(digits, labelParts.length == 2 ? labelParts[1] : null); | |
984 } | |
985 | |
986 Future<int> upgradeAgent( | |
987 CommandSender commandSender, | |
988 StreamIterator<ClientCommand> commandIterator, | |
989 SessionState state, | |
990 Uri base, | |
991 Uri packageUri) async { | |
992 if (state.settings.deviceAddress == null) { | |
993 throwFatalError(DiagnosticKind.noAgentFound); | |
994 } | |
995 | |
996 while (packageUri == null) { | |
997 packageUri = | |
998 await readPackagePathFromUser(base, commandSender, commandIterator); | |
999 } | |
1000 | |
1001 if (!await new File.fromUri(packageUri).exists()) { | |
1002 print('File not found: $packageUri'); | |
1003 return 1; | |
1004 } | |
1005 | |
1006 Version version = parseVersion(extractVersion(packageUri)); | |
1007 | |
1008 Version existingVersion = parseVersion( | |
1009 await withAgentConnection(state, | |
1010 (connection) => connection.fletchVersion())); | |
1011 | |
1012 if (existingVersion == version) { | |
1013 print('Target device is already at $version'); | |
1014 return 0; | |
1015 } | |
1016 | |
1017 print("Attempting to upgrade device from " | |
1018 "$existingVersion to $version"); | |
1019 | |
1020 if (existingVersion.isGreaterThan(version)) { | |
1021 commandSender.sendStdout("The existing version is greater than the " | |
1022 "version you want to use to upgrade.\n" | |
1023 "Please confirm this operation by typing 'yes' " | |
1024 "(press Enter to abort): "); | |
1025 Confirm: while (await commandIterator.moveNext()) { | |
1026 ClientCommand command = commandIterator.current; | |
1027 switch (command.code) { | |
1028 case ClientCommandCode.Stdin: | |
1029 if (command.data.length == 0) { | |
1030 throwInternalError("Unexpected end of input"); | |
1031 } | |
1032 String line = UTF8.decode(command.data).trim(); | |
1033 if (line.isEmpty) { | |
1034 commandSender.sendStdout("Upgrade aborted\n"); | |
1035 return 0; | |
1036 } else if (line.trim().toLowerCase() == "yes") { | |
1037 break Confirm; | |
1038 } | |
1039 break; | |
1040 | |
1041 default: | |
1042 throwInternalError("Unexpected ${command.code}"); | |
1043 return null; | |
1044 } | |
1045 } | |
1046 } | |
1047 | |
1048 List<int> data = await new File.fromUri(packageUri).readAsBytes(); | |
1049 print("Sending package to fletch agent"); | |
1050 await withAgentConnection(state, | |
1051 (connection) => connection.upgradeAgent(version.toString(), data)); | |
1052 print("Transfer complete, waiting for the Fletch agent to restart. " | |
1053 "This can take a few seconds."); | |
1054 | |
1055 Version newVersion; | |
1056 int remainingTries = 20; | |
1057 // Wait for the agent to come back online to verify the version. | |
1058 while (--remainingTries > 0) { | |
1059 await new Future.delayed(const Duration(seconds: 1)); | |
1060 try { | |
1061 // TODO(karlklose): this functionality should be shared with connect. | |
1062 Socket socket = await Socket.connect( | |
1063 state.settings.deviceAddress.host, | |
1064 state.settings.deviceAddress.port); | |
1065 handleSocketErrors(socket, "pollAgentVersion", log: (String info) { | |
1066 state.log("Connected to TCP waitForAgentUpgrade $info"); | |
1067 }); | |
1068 AgentConnection connection = new AgentConnection(socket); | |
1069 newVersion = parseVersion(await connection.fletchVersion()); | |
1070 disconnectFromAgent(connection); | |
1071 if (newVersion != existingVersion) { | |
1072 break; | |
1073 } | |
1074 } on SocketException catch (e) { | |
1075 // Ignore this error and keep waiting. | |
1076 } | |
1077 } | |
1078 | |
1079 if (newVersion == existingVersion) { | |
1080 print("Failed to upgrade: the device is still at the old version."); | |
1081 print("Try running x-upgrade again. " | |
1082 "If the upgrade fails again, try rebooting the device."); | |
1083 return 1; | |
1084 } else if (newVersion == null) { | |
1085 print("Could not connect to Fletch agent after upgrade."); | |
1086 print("Try running 'fletch show devices' later to see if it has been" | |
1087 " restarted. If the device does not show up, try rebooting it."); | |
1088 return 1; | |
1089 } else { | |
1090 print("Upgrade successful."); | |
1091 } | |
1092 | |
1093 return 0; | |
1094 } | |
1095 | |
1096 Future<int> downloadTools( | |
1097 CommandSender commandSender, | |
1098 StreamIterator<ClientCommand> commandIterator, | |
1099 SessionState state) async { | |
1100 | |
1101 void throwUnsupportedPlatform() { | |
1102 throwFatalError( | |
1103 DiagnosticKind.unsupportedPlatform, | |
1104 message: Platform.operatingSystem); | |
1105 } | |
1106 | |
1107 Future decompressFile(File zipFile, Directory destination) async { | |
1108 var result; | |
1109 if (Platform.isLinux) { | |
1110 result = await Process.run( | |
1111 "unzip", ["-o", zipFile.path, "-d", destination.path]); | |
1112 } else if (Platform.isMacOS) { | |
1113 result = await Process.run( | |
1114 "ditto", ["-x", "-k", zipFile.path, destination.path]); | |
1115 } else { | |
1116 throwUnsupportedPlatform(); | |
1117 } | |
1118 if (result.exitCode != 0) { | |
1119 throwInternalError( | |
1120 "Failed to decompress ${zipFile.path} to ${destination.path}, " | |
1121 "error = ${result.exitCode}"); | |
1122 } | |
1123 } | |
1124 | |
1125 const String gcsRoot = "https://storage.googleapis.com"; | |
1126 String gcsBucket = "fletch-archive"; | |
1127 | |
1128 Future downloadTool(String gcsPath, String zipFile, String toolName) async { | |
1129 Uri url = Uri.parse("$gcsRoot/$gcsBucket/$gcsPath/$zipFile"); | |
1130 Directory tmpDir = Directory.systemTemp.createTempSync("fletch_download"); | |
1131 File tmpZip = new File(join(tmpDir.path, zipFile)); | |
1132 | |
1133 OutputService outputService = | |
1134 new OutputService(commandSender.sendStdout, state.log); | |
1135 SDKServices service = new SDKServices(outputService); | |
1136 print("Downloading: $toolName"); | |
1137 state.log("Downloading $toolName from $url to $tmpZip"); | |
1138 await service.downloadWithProgress(url, tmpZip); | |
1139 print(""); // service.downloadWithProgress does not write newline when done. | |
1140 | |
1141 // In the SDK, the tools directory is at the same level as the | |
1142 // internal (and bin) directory. | |
1143 Directory toolsDirectory = | |
1144 new Directory.fromUri(executable.resolve('../tools')); | |
1145 state.log("Decompressing ${tmpZip.path} to ${toolsDirectory.path}"); | |
1146 await decompressFile(tmpZip, toolsDirectory); | |
1147 state.log("Deleting temporary directory ${tmpDir.path}"); | |
1148 await tmpDir.delete(recursive: true); | |
1149 } | |
1150 | |
1151 String gcsPath; | |
1152 | |
1153 Version version = parseVersion(fletchVersion); | |
1154 if (version.isEdgeVersion) { | |
1155 print("WARNING: For bleeding edge a fixed image is used."); | |
1156 // For edge versions download use a well known version for now. | |
1157 var knownVersion = "0.3.0-edge.3c85dbafe006eb2ce16545aaf3df1352fa7a4500"; | |
1158 gcsBucket = "fletch-temporary"; | |
1159 gcsPath = "channels/be/raw/$knownVersion/sdk"; | |
1160 } else if (version.isDevVersion) { | |
1161 // TODO(sgjesse): Change this to channels/dev/release at some point. | |
1162 gcsPath = "channels/dev/raw/$version/sdk"; | |
1163 } else { | |
1164 print("Stable version not supported. Got version $version."); | |
1165 } | |
1166 | |
1167 String osName; | |
1168 if (Platform.isLinux) { | |
1169 osName = "linux"; | |
1170 } else if (Platform.isMacOS) { | |
1171 osName = "mac"; | |
1172 } else { | |
1173 throwUnsupportedPlatform(); | |
1174 } | |
1175 | |
1176 String gccArmEmbedded = "gcc-arm-embedded-${osName}.zip"; | |
1177 await downloadTool(gcsPath, gccArmEmbedded, "GCC ARM Embedded toolchain"); | |
1178 String openocd = "openocd-${osName}.zip"; | |
1179 await downloadTool(gcsPath, openocd, "Open On-Chip Debugger (OpenOCD)"); | |
1180 | |
1181 print("Third party tools downloaded"); | |
1182 | |
1183 return 0; | |
1184 } | |
1185 | |
1186 Future<WorkerConnection> allocateWorker(IsolatePool pool) async { | |
1187 WorkerConnection workerConnection = | |
1188 new WorkerConnection(await pool.getIsolate(exitOnError: false)); | |
1189 await workerConnection.beginSession(); | |
1190 return workerConnection; | |
1191 } | |
1192 | |
1193 SharedTask combineTasks(SharedTask task1, SharedTask task2) { | |
1194 if (task1 == null) return task2; | |
1195 if (task2 == null) return task1; | |
1196 return new CombinedTask(task1, task2); | |
1197 } | |
1198 | |
1199 class CombinedTask extends SharedTask { | |
1200 // Keep this class simple, see note in superclass. | |
1201 | |
1202 final SharedTask task1; | |
1203 | |
1204 final SharedTask task2; | |
1205 | |
1206 const CombinedTask(this.task1, this.task2); | |
1207 | |
1208 Future<int> call( | |
1209 CommandSender commandSender, | |
1210 StreamIterator<ClientCommand> commandIterator) { | |
1211 return invokeCombinedTasks(commandSender, commandIterator, task1, task2); | |
1212 } | |
1213 } | |
1214 | |
1215 Future<int> invokeCombinedTasks( | |
1216 CommandSender commandSender, | |
1217 StreamIterator<ClientCommand> commandIterator, | |
1218 SharedTask task1, | |
1219 SharedTask task2) async { | |
1220 int result = await task1(commandSender, commandIterator); | |
1221 if (result != 0) return result; | |
1222 return task2(commandSender, commandIterator); | |
1223 } | |
1224 | |
1225 Future<String> getAgentVersion(InternetAddress host, int port) async { | |
1226 Socket socket; | |
1227 try { | |
1228 socket = await Socket.connect(host, port); | |
1229 handleSocketErrors(socket, "getAgentVersionSocket"); | |
1230 } on SocketException catch (e) { | |
1231 return 'Error: no agent: $e'; | |
1232 } | |
1233 try { | |
1234 AgentConnection connection = new AgentConnection(socket); | |
1235 return await connection.fletchVersion(); | |
1236 } finally { | |
1237 socket.close(); | |
1238 } | |
1239 } | |
1240 | |
1241 Future<List<InternetAddress>> discoverDevices( | |
1242 {bool prefixWithNumber: false}) async { | |
1243 const ipV4AddressLength = 'xxx.xxx.xxx.xxx'.length; | |
1244 print("Looking for Dartino capable devices (will search for 5 seconds)..."); | |
1245 MDnsClient client = new MDnsClient(); | |
1246 await client.start(); | |
1247 List<InternetAddress> result = <InternetAddress>[]; | |
1248 String name = '_dartino_agent._tcp.local'; | |
1249 await for (ResourceRecord ptr in client.lookup(RRType.PTR, name)) { | |
1250 String domain = ptr.domainName; | |
1251 await for (ResourceRecord srv in client.lookup(RRType.SRV, domain)) { | |
1252 String target = srv.target; | |
1253 await for (ResourceRecord a in client.lookup(RRType.A, target)) { | |
1254 InternetAddress address = a.address; | |
1255 if (!address.isLinkLocal) { | |
1256 result.add(address); | |
1257 String version = await getAgentVersion(address, AGENT_DEFAULT_PORT); | |
1258 String prefix = prefixWithNumber ? "${result.length}: " : ""; | |
1259 print("${prefix}Device at " | |
1260 "${address.address.padRight(ipV4AddressLength + 1)} " | |
1261 "$target ($version)"); | |
1262 } | |
1263 } | |
1264 } | |
1265 // TODO(karlklose): Verify that we got an A/IP4 result for the PTR result. | |
1266 // If not, maybe the cache was flushed before access and we need to query | |
1267 // for the SRV or A type again. | |
1268 } | |
1269 client.stop(); | |
1270 return result; | |
1271 } | |
1272 | |
1273 void showSessions() { | |
1274 Sessions.names.forEach(print); | |
1275 } | |
1276 | |
1277 Future<int> showSessionSettings() async { | |
1278 Settings settings = SessionState.current.settings; | |
1279 Uri source = settings.source; | |
1280 if (source != null) { | |
1281 // This should output `source.toFilePath()`, but we do it like this to be | |
1282 // consistent with the format of the [Settings.packages] value. | |
1283 print('Configured from $source}'); | |
1284 } | |
1285 settings.toJson().forEach((String key, value) { | |
1286 print('$key: $value'); | |
1287 }); | |
1288 return 0; | |
1289 } | |
1290 | |
1291 Address parseAddress(String address, {int defaultPort: 0}) { | |
1292 String host; | |
1293 int port; | |
1294 List<String> parts = address.split(":"); | |
1295 if (parts.length == 1) { | |
1296 host = InternetAddress.LOOPBACK_IP_V4.address; | |
1297 port = int.parse( | |
1298 parts[0], | |
1299 onError: (String source) { | |
1300 host = source; | |
1301 return defaultPort; | |
1302 }); | |
1303 } else { | |
1304 host = parts[0]; | |
1305 port = int.parse( | |
1306 parts[1], | |
1307 onError: (String source) { | |
1308 throwFatalError( | |
1309 DiagnosticKind.expectedAPortNumber, userInput: source); | |
1310 }); | |
1311 } | |
1312 return new Address(host, port); | |
1313 } | |
1314 | |
1315 class Address { | |
1316 final String host; | |
1317 final int port; | |
1318 | |
1319 const Address(this.host, this.port); | |
1320 | |
1321 String toString() => "Address($host, $port)"; | |
1322 | |
1323 String toJson() => "$host:$port"; | |
1324 | |
1325 bool operator ==(other) { | |
1326 if (other is! Address) return false; | |
1327 return other.host == host && other.port == port; | |
1328 } | |
1329 | |
1330 int get hashCode => host.hashCode ^ port.hashCode; | |
1331 } | |
1332 | |
1333 /// See ../verbs/documentation.dart for a definition of this format. | |
1334 Settings parseSettings(String jsonLikeData, Uri settingsUri) { | |
1335 String json = jsonLikeData.split("\n") | |
1336 .where((String line) => !line.trim().startsWith("//")).join("\n"); | |
1337 var userSettings; | |
1338 try { | |
1339 userSettings = JSON.decode(json); | |
1340 } on FormatException catch (e) { | |
1341 throwFatalError( | |
1342 DiagnosticKind.settingsNotJson, uri: settingsUri, message: e.message); | |
1343 } | |
1344 if (userSettings is! Map) { | |
1345 throwFatalError(DiagnosticKind.settingsNotAMap, uri: settingsUri); | |
1346 } | |
1347 Uri packages; | |
1348 final List<String> options = <String>[]; | |
1349 final Map<String, String> constants = <String, String>{}; | |
1350 Address deviceAddress; | |
1351 DeviceType deviceType; | |
1352 IncrementalMode incrementalMode = IncrementalMode.none; | |
1353 userSettings.forEach((String key, value) { | |
1354 switch (key) { | |
1355 case "packages": | |
1356 if (value != null) { | |
1357 if (value is! String) { | |
1358 throwFatalError( | |
1359 DiagnosticKind.settingsPackagesNotAString, uri: settingsUri, | |
1360 userInput: '$value'); | |
1361 } | |
1362 packages = settingsUri.resolve(value); | |
1363 } | |
1364 break; | |
1365 | |
1366 case "options": | |
1367 if (value != null) { | |
1368 if (value is! List) { | |
1369 throwFatalError( | |
1370 DiagnosticKind.settingsOptionsNotAList, uri: settingsUri, | |
1371 userInput: "$value"); | |
1372 } | |
1373 for (var option in value) { | |
1374 if (option is! String) { | |
1375 throwFatalError( | |
1376 DiagnosticKind.settingsOptionNotAString, uri: settingsUri, | |
1377 userInput: '$option'); | |
1378 } | |
1379 if (option.startsWith("-D")) { | |
1380 throwFatalError( | |
1381 DiagnosticKind.settingsCompileTimeConstantAsOption, | |
1382 uri: settingsUri, userInput: '$option'); | |
1383 } | |
1384 options.add(option); | |
1385 } | |
1386 } | |
1387 break; | |
1388 | |
1389 case "constants": | |
1390 if (value != null) { | |
1391 if (value is! Map) { | |
1392 throwFatalError( | |
1393 DiagnosticKind.settingsConstantsNotAMap, uri: settingsUri); | |
1394 } | |
1395 value.forEach((String key, value) { | |
1396 if (value == null) { | |
1397 // Ignore. | |
1398 } else if (value is bool || value is int || value is String) { | |
1399 constants[key] = '$value'; | |
1400 } else { | |
1401 throwFatalError( | |
1402 DiagnosticKind.settingsUnrecognizedConstantValue, | |
1403 uri: settingsUri, userInput: key, | |
1404 additionalUserInput: '$value'); | |
1405 } | |
1406 }); | |
1407 } | |
1408 break; | |
1409 | |
1410 case "device_address": | |
1411 if (value != null) { | |
1412 if (value is! String) { | |
1413 throwFatalError( | |
1414 DiagnosticKind.settingsDeviceAddressNotAString, | |
1415 uri: settingsUri, userInput: '$value'); | |
1416 } | |
1417 deviceAddress = | |
1418 parseAddress(value, defaultPort: AGENT_DEFAULT_PORT); | |
1419 } | |
1420 break; | |
1421 | |
1422 case "device_type": | |
1423 if (value != null) { | |
1424 if (value is! String) { | |
1425 throwFatalError( | |
1426 DiagnosticKind.settingsDeviceTypeNotAString, | |
1427 uri: settingsUri, userInput: '$value'); | |
1428 } | |
1429 deviceType = parseDeviceType(value); | |
1430 if (deviceType == null) { | |
1431 throwFatalError( | |
1432 DiagnosticKind.settingsDeviceTypeUnrecognized, | |
1433 uri: settingsUri, userInput: '$value'); | |
1434 } | |
1435 } | |
1436 break; | |
1437 | |
1438 case "incremental_mode": | |
1439 if (value != null) { | |
1440 if (value is! String) { | |
1441 throwFatalError( | |
1442 DiagnosticKind.settingsIncrementalModeNotAString, | |
1443 uri: settingsUri, userInput: '$value'); | |
1444 } | |
1445 incrementalMode = parseIncrementalMode(value); | |
1446 if (incrementalMode == null) { | |
1447 throwFatalError( | |
1448 DiagnosticKind.settingsIncrementalModeUnrecognized, | |
1449 uri: settingsUri, userInput: '$value'); | |
1450 } | |
1451 } | |
1452 break; | |
1453 | |
1454 default: | |
1455 throwFatalError( | |
1456 DiagnosticKind.settingsUnrecognizedKey, uri: settingsUri, | |
1457 userInput: key); | |
1458 break; | |
1459 } | |
1460 }); | |
1461 return new Settings.fromSource(settingsUri, | |
1462 packages, options, constants, deviceAddress, deviceType, incrementalMode); | |
1463 } | |
1464 | |
1465 class Settings { | |
1466 final Uri source; | |
1467 | |
1468 final Uri packages; | |
1469 | |
1470 final List<String> options; | |
1471 | |
1472 final Map<String, String> constants; | |
1473 | |
1474 final Address deviceAddress; | |
1475 | |
1476 final DeviceType deviceType; | |
1477 | |
1478 final IncrementalMode incrementalMode; | |
1479 | |
1480 const Settings( | |
1481 this.packages, | |
1482 this.options, | |
1483 this.constants, | |
1484 this.deviceAddress, | |
1485 this.deviceType, | |
1486 this.incrementalMode) : source = null; | |
1487 | |
1488 const Settings.fromSource( | |
1489 this.source, | |
1490 this.packages, | |
1491 this.options, | |
1492 this.constants, | |
1493 this.deviceAddress, | |
1494 this.deviceType, | |
1495 this.incrementalMode); | |
1496 | |
1497 const Settings.empty() | |
1498 : this(null, const <String>[], const <String, String>{}, null, null, | |
1499 IncrementalMode.none); | |
1500 | |
1501 Settings copyWith({ | |
1502 Uri packages, | |
1503 List<String> options, | |
1504 Map<String, String> constants, | |
1505 Address deviceAddress, | |
1506 DeviceType deviceType, | |
1507 IncrementalMode incrementalMode}) { | |
1508 | |
1509 if (packages == null) { | |
1510 packages = this.packages; | |
1511 } | |
1512 if (options == null) { | |
1513 options = this.options; | |
1514 } | |
1515 if (constants == null) { | |
1516 constants = this.constants; | |
1517 } | |
1518 if (deviceAddress == null) { | |
1519 deviceAddress = this.deviceAddress; | |
1520 } | |
1521 if (deviceType == null) { | |
1522 deviceType = this.deviceType; | |
1523 } | |
1524 if (incrementalMode == null) { | |
1525 incrementalMode = this.incrementalMode; | |
1526 } | |
1527 return new Settings( | |
1528 packages, | |
1529 options, | |
1530 constants, | |
1531 deviceAddress, | |
1532 deviceType, | |
1533 incrementalMode); | |
1534 } | |
1535 | |
1536 String toString() { | |
1537 return "Settings(" | |
1538 "packages: $packages, " | |
1539 "options: $options, " | |
1540 "constants: $constants, " | |
1541 "device_address: $deviceAddress, " | |
1542 "device_type: $deviceType, " | |
1543 "incremental_mode: $incrementalMode)"; | |
1544 } | |
1545 | |
1546 Map<String, dynamic> toJson() { | |
1547 Map<String, dynamic> result = <String, dynamic>{}; | |
1548 | |
1549 void addIfNotNull(String name, value) { | |
1550 if (value != null) { | |
1551 result[name] = value; | |
1552 } | |
1553 } | |
1554 | |
1555 addIfNotNull("packages", packages == null ? null : "$packages"); | |
1556 addIfNotNull("options", options); | |
1557 addIfNotNull("constants", constants); | |
1558 addIfNotNull("device_address", deviceAddress); | |
1559 addIfNotNull( | |
1560 "device_type", | |
1561 deviceType == null ? null : unParseDeviceType(deviceType)); | |
1562 addIfNotNull( | |
1563 "incremental_mode", | |
1564 incrementalMode == null | |
1565 ? null : unparseIncrementalMode(incrementalMode)); | |
1566 | |
1567 return result; | |
1568 } | |
1569 } | |
OLD | NEW |