| 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 String gcsPath; | |
| 1128 | |
| 1129 Version version = parseVersion(fletchVersion); | |
| 1130 if (version.isEdgeVersion) { | |
| 1131 print("WARNING: For bleeding edge a fixed image is used."); | |
| 1132 // For edge versions download a well known version for now. | |
| 1133 var knownVersion = "0.3.0-edge.3de334803a15da98434e85f25f656119ec555b74"; | |
| 1134 gcsBucket = "fletch-temporary"; | |
| 1135 gcsPath = "channels/be/raw/$knownVersion/sdk"; | |
| 1136 } else if (version.isDevVersion) { | |
| 1137 // TODO(sgjesse): Change this to channels/dev/release at some point. | |
| 1138 gcsPath = "channels/dev/raw/$version/sdk"; | |
| 1139 } else { | |
| 1140 print("Stable version not supported. Got version $version."); | |
| 1141 } | |
| 1142 | |
| 1143 String osName; | |
| 1144 if (Platform.isLinux) { | |
| 1145 osName = "linux"; | |
| 1146 } else if (Platform.isMacOS) { | |
| 1147 osName = "mac"; | |
| 1148 } else { | |
| 1149 throwUnsupportedPlatform(); | |
| 1150 } | |
| 1151 String gccArmEmbedded = "gcc-arm-embedded-${osName}.zip"; | |
| 1152 Uri gccArmEmbeddedUrl = | |
| 1153 Uri.parse("$gcsRoot/$gcsBucket/$gcsPath/$gccArmEmbedded"); | |
| 1154 Directory tmpDir = Directory.systemTemp.createTempSync("fletch_download"); | |
| 1155 File tmpZip = new File(join(tmpDir.path, gccArmEmbedded)); | |
| 1156 | |
| 1157 OutputService outputService = | |
| 1158 new OutputService(commandSender.sendStdout, state.log); | |
| 1159 SDKServices service = new SDKServices(outputService); | |
| 1160 String toolName = "GCC ARM Embedded toolchain"; | |
| 1161 print("Downloading: $toolName"); | |
| 1162 state.log("Downloading $toolName from $gccArmEmbeddedUrl to $tmpZip"); | |
| 1163 await service.downloadWithProgress(gccArmEmbeddedUrl, tmpZip); | |
| 1164 print(""); // service.downloadWithProgress does not write newline when done. | |
| 1165 | |
| 1166 // In the SDK the tools directory is at the same level as the | |
| 1167 // internal (and bin) directory. | |
| 1168 Directory toolsDirectory = | |
| 1169 new Directory.fromUri(executable.resolve('../tools')); | |
| 1170 state.log("Decompression ${tmpZip.path} to ${toolsDirectory.path}"); | |
| 1171 await decompressFile(tmpZip, toolsDirectory); | |
| 1172 state.log("Deleting temporary directory ${tmpDir.path}"); | |
| 1173 await tmpDir.delete(recursive: true); | |
| 1174 | |
| 1175 print("Third party tools downloaded"); | |
| 1176 | |
| 1177 return 0; | |
| 1178 } | |
| 1179 | |
| 1180 Future<WorkerConnection> allocateWorker(IsolatePool pool) async { | |
| 1181 WorkerConnection workerConnection = | |
| 1182 new WorkerConnection(await pool.getIsolate(exitOnError: false)); | |
| 1183 await workerConnection.beginSession(); | |
| 1184 return workerConnection; | |
| 1185 } | |
| 1186 | |
| 1187 SharedTask combineTasks(SharedTask task1, SharedTask task2) { | |
| 1188 if (task1 == null) return task2; | |
| 1189 if (task2 == null) return task1; | |
| 1190 return new CombinedTask(task1, task2); | |
| 1191 } | |
| 1192 | |
| 1193 class CombinedTask extends SharedTask { | |
| 1194 // Keep this class simple, see note in superclass. | |
| 1195 | |
| 1196 final SharedTask task1; | |
| 1197 | |
| 1198 final SharedTask task2; | |
| 1199 | |
| 1200 const CombinedTask(this.task1, this.task2); | |
| 1201 | |
| 1202 Future<int> call( | |
| 1203 CommandSender commandSender, | |
| 1204 StreamIterator<ClientCommand> commandIterator) { | |
| 1205 return invokeCombinedTasks(commandSender, commandIterator, task1, task2); | |
| 1206 } | |
| 1207 } | |
| 1208 | |
| 1209 Future<int> invokeCombinedTasks( | |
| 1210 CommandSender commandSender, | |
| 1211 StreamIterator<ClientCommand> commandIterator, | |
| 1212 SharedTask task1, | |
| 1213 SharedTask task2) async { | |
| 1214 int result = await task1(commandSender, commandIterator); | |
| 1215 if (result != 0) return result; | |
| 1216 return task2(commandSender, commandIterator); | |
| 1217 } | |
| 1218 | |
| 1219 Future<String> getAgentVersion(InternetAddress host, int port) async { | |
| 1220 Socket socket; | |
| 1221 try { | |
| 1222 socket = await Socket.connect(host, port); | |
| 1223 handleSocketErrors(socket, "getAgentVersionSocket"); | |
| 1224 } on SocketException catch (e) { | |
| 1225 return 'Error: no agent: $e'; | |
| 1226 } | |
| 1227 try { | |
| 1228 AgentConnection connection = new AgentConnection(socket); | |
| 1229 return await connection.fletchVersion(); | |
| 1230 } finally { | |
| 1231 socket.close(); | |
| 1232 } | |
| 1233 } | |
| 1234 | |
| 1235 Future<List<InternetAddress>> discoverDevices( | |
| 1236 {bool prefixWithNumber: false}) async { | |
| 1237 const ipV4AddressLength = 'xxx.xxx.xxx.xxx'.length; | |
| 1238 print("Looking for Dartino capable devices (will search for 5 seconds)..."); | |
| 1239 MDnsClient client = new MDnsClient(); | |
| 1240 await client.start(); | |
| 1241 List<InternetAddress> result = <InternetAddress>[]; | |
| 1242 String name = '_dartino_agent._tcp.local'; | |
| 1243 await for (ResourceRecord ptr in client.lookup(RRType.PTR, name)) { | |
| 1244 String domain = ptr.domainName; | |
| 1245 await for (ResourceRecord srv in client.lookup(RRType.SRV, domain)) { | |
| 1246 String target = srv.target; | |
| 1247 await for (ResourceRecord a in client.lookup(RRType.A, target)) { | |
| 1248 InternetAddress address = a.address; | |
| 1249 if (!address.isLinkLocal) { | |
| 1250 result.add(address); | |
| 1251 String version = await getAgentVersion(address, AGENT_DEFAULT_PORT); | |
| 1252 String prefix = prefixWithNumber ? "${result.length}: " : ""; | |
| 1253 print("${prefix}Device at " | |
| 1254 "${address.address.padRight(ipV4AddressLength + 1)} " | |
| 1255 "$target ($version)"); | |
| 1256 } | |
| 1257 } | |
| 1258 } | |
| 1259 // TODO(karlklose): Verify that we got an A/IP4 result for the PTR result. | |
| 1260 // If not, maybe the cache was flushed before access and we need to query | |
| 1261 // for the SRV or A type again. | |
| 1262 } | |
| 1263 client.stop(); | |
| 1264 return result; | |
| 1265 } | |
| 1266 | |
| 1267 void showSessions() { | |
| 1268 Sessions.names.forEach(print); | |
| 1269 } | |
| 1270 | |
| 1271 Future<int> showSessionSettings() async { | |
| 1272 Settings settings = SessionState.current.settings; | |
| 1273 Uri source = settings.source; | |
| 1274 if (source != null) { | |
| 1275 // This should output `source.toFilePath()`, but we do it like this to be | |
| 1276 // consistent with the format of the [Settings.packages] value. | |
| 1277 print('Configured from $source}'); | |
| 1278 } | |
| 1279 settings.toJson().forEach((String key, value) { | |
| 1280 print('$key: $value'); | |
| 1281 }); | |
| 1282 return 0; | |
| 1283 } | |
| 1284 | |
| 1285 Address parseAddress(String address, {int defaultPort: 0}) { | |
| 1286 String host; | |
| 1287 int port; | |
| 1288 List<String> parts = address.split(":"); | |
| 1289 if (parts.length == 1) { | |
| 1290 host = InternetAddress.LOOPBACK_IP_V4.address; | |
| 1291 port = int.parse( | |
| 1292 parts[0], | |
| 1293 onError: (String source) { | |
| 1294 host = source; | |
| 1295 return defaultPort; | |
| 1296 }); | |
| 1297 } else { | |
| 1298 host = parts[0]; | |
| 1299 port = int.parse( | |
| 1300 parts[1], | |
| 1301 onError: (String source) { | |
| 1302 throwFatalError( | |
| 1303 DiagnosticKind.expectedAPortNumber, userInput: source); | |
| 1304 }); | |
| 1305 } | |
| 1306 return new Address(host, port); | |
| 1307 } | |
| 1308 | |
| 1309 class Address { | |
| 1310 final String host; | |
| 1311 final int port; | |
| 1312 | |
| 1313 const Address(this.host, this.port); | |
| 1314 | |
| 1315 String toString() => "Address($host, $port)"; | |
| 1316 | |
| 1317 String toJson() => "$host:$port"; | |
| 1318 | |
| 1319 bool operator ==(other) { | |
| 1320 if (other is! Address) return false; | |
| 1321 return other.host == host && other.port == port; | |
| 1322 } | |
| 1323 | |
| 1324 int get hashCode => host.hashCode ^ port.hashCode; | |
| 1325 } | |
| 1326 | |
| 1327 /// See ../verbs/documentation.dart for a definition of this format. | |
| 1328 Settings parseSettings(String jsonLikeData, Uri settingsUri) { | |
| 1329 String json = jsonLikeData.split("\n") | |
| 1330 .where((String line) => !line.trim().startsWith("//")).join("\n"); | |
| 1331 var userSettings; | |
| 1332 try { | |
| 1333 userSettings = JSON.decode(json); | |
| 1334 } on FormatException catch (e) { | |
| 1335 throwFatalError( | |
| 1336 DiagnosticKind.settingsNotJson, uri: settingsUri, message: e.message); | |
| 1337 } | |
| 1338 if (userSettings is! Map) { | |
| 1339 throwFatalError(DiagnosticKind.settingsNotAMap, uri: settingsUri); | |
| 1340 } | |
| 1341 Uri packages; | |
| 1342 final List<String> options = <String>[]; | |
| 1343 final Map<String, String> constants = <String, String>{}; | |
| 1344 Address deviceAddress; | |
| 1345 DeviceType deviceType; | |
| 1346 IncrementalMode incrementalMode = IncrementalMode.none; | |
| 1347 userSettings.forEach((String key, value) { | |
| 1348 switch (key) { | |
| 1349 case "packages": | |
| 1350 if (value != null) { | |
| 1351 if (value is! String) { | |
| 1352 throwFatalError( | |
| 1353 DiagnosticKind.settingsPackagesNotAString, uri: settingsUri, | |
| 1354 userInput: '$value'); | |
| 1355 } | |
| 1356 packages = settingsUri.resolve(value); | |
| 1357 } | |
| 1358 break; | |
| 1359 | |
| 1360 case "options": | |
| 1361 if (value != null) { | |
| 1362 if (value is! List) { | |
| 1363 throwFatalError( | |
| 1364 DiagnosticKind.settingsOptionsNotAList, uri: settingsUri, | |
| 1365 userInput: "$value"); | |
| 1366 } | |
| 1367 for (var option in value) { | |
| 1368 if (option is! String) { | |
| 1369 throwFatalError( | |
| 1370 DiagnosticKind.settingsOptionNotAString, uri: settingsUri, | |
| 1371 userInput: '$option'); | |
| 1372 } | |
| 1373 if (option.startsWith("-D")) { | |
| 1374 throwFatalError( | |
| 1375 DiagnosticKind.settingsCompileTimeConstantAsOption, | |
| 1376 uri: settingsUri, userInput: '$option'); | |
| 1377 } | |
| 1378 options.add(option); | |
| 1379 } | |
| 1380 } | |
| 1381 break; | |
| 1382 | |
| 1383 case "constants": | |
| 1384 if (value != null) { | |
| 1385 if (value is! Map) { | |
| 1386 throwFatalError( | |
| 1387 DiagnosticKind.settingsConstantsNotAMap, uri: settingsUri); | |
| 1388 } | |
| 1389 value.forEach((String key, value) { | |
| 1390 if (value == null) { | |
| 1391 // Ignore. | |
| 1392 } else if (value is bool || value is int || value is String) { | |
| 1393 constants[key] = '$value'; | |
| 1394 } else { | |
| 1395 throwFatalError( | |
| 1396 DiagnosticKind.settingsUnrecognizedConstantValue, | |
| 1397 uri: settingsUri, userInput: key, | |
| 1398 additionalUserInput: '$value'); | |
| 1399 } | |
| 1400 }); | |
| 1401 } | |
| 1402 break; | |
| 1403 | |
| 1404 case "device_address": | |
| 1405 if (value != null) { | |
| 1406 if (value is! String) { | |
| 1407 throwFatalError( | |
| 1408 DiagnosticKind.settingsDeviceAddressNotAString, | |
| 1409 uri: settingsUri, userInput: '$value'); | |
| 1410 } | |
| 1411 deviceAddress = | |
| 1412 parseAddress(value, defaultPort: AGENT_DEFAULT_PORT); | |
| 1413 } | |
| 1414 break; | |
| 1415 | |
| 1416 case "device_type": | |
| 1417 if (value != null) { | |
| 1418 if (value is! String) { | |
| 1419 throwFatalError( | |
| 1420 DiagnosticKind.settingsDeviceTypeNotAString, | |
| 1421 uri: settingsUri, userInput: '$value'); | |
| 1422 } | |
| 1423 deviceType = parseDeviceType(value); | |
| 1424 if (deviceType == null) { | |
| 1425 throwFatalError( | |
| 1426 DiagnosticKind.settingsDeviceTypeUnrecognized, | |
| 1427 uri: settingsUri, userInput: '$value'); | |
| 1428 } | |
| 1429 } | |
| 1430 break; | |
| 1431 | |
| 1432 case "incremental_mode": | |
| 1433 if (value != null) { | |
| 1434 if (value is! String) { | |
| 1435 throwFatalError( | |
| 1436 DiagnosticKind.settingsIncrementalModeNotAString, | |
| 1437 uri: settingsUri, userInput: '$value'); | |
| 1438 } | |
| 1439 incrementalMode = parseIncrementalMode(value); | |
| 1440 if (incrementalMode == null) { | |
| 1441 throwFatalError( | |
| 1442 DiagnosticKind.settingsIncrementalModeUnrecognized, | |
| 1443 uri: settingsUri, userInput: '$value'); | |
| 1444 } | |
| 1445 } | |
| 1446 break; | |
| 1447 | |
| 1448 default: | |
| 1449 throwFatalError( | |
| 1450 DiagnosticKind.settingsUnrecognizedKey, uri: settingsUri, | |
| 1451 userInput: key); | |
| 1452 break; | |
| 1453 } | |
| 1454 }); | |
| 1455 return new Settings.fromSource(settingsUri, | |
| 1456 packages, options, constants, deviceAddress, deviceType, incrementalMode); | |
| 1457 } | |
| 1458 | |
| 1459 class Settings { | |
| 1460 final Uri source; | |
| 1461 | |
| 1462 final Uri packages; | |
| 1463 | |
| 1464 final List<String> options; | |
| 1465 | |
| 1466 final Map<String, String> constants; | |
| 1467 | |
| 1468 final Address deviceAddress; | |
| 1469 | |
| 1470 final DeviceType deviceType; | |
| 1471 | |
| 1472 final IncrementalMode incrementalMode; | |
| 1473 | |
| 1474 const Settings( | |
| 1475 this.packages, | |
| 1476 this.options, | |
| 1477 this.constants, | |
| 1478 this.deviceAddress, | |
| 1479 this.deviceType, | |
| 1480 this.incrementalMode) : source = null; | |
| 1481 | |
| 1482 const Settings.fromSource( | |
| 1483 this.source, | |
| 1484 this.packages, | |
| 1485 this.options, | |
| 1486 this.constants, | |
| 1487 this.deviceAddress, | |
| 1488 this.deviceType, | |
| 1489 this.incrementalMode); | |
| 1490 | |
| 1491 const Settings.empty() | |
| 1492 : this(null, const <String>[], const <String, String>{}, null, null, | |
| 1493 IncrementalMode.none); | |
| 1494 | |
| 1495 Settings copyWith({ | |
| 1496 Uri packages, | |
| 1497 List<String> options, | |
| 1498 Map<String, String> constants, | |
| 1499 Address deviceAddress, | |
| 1500 DeviceType deviceType, | |
| 1501 IncrementalMode incrementalMode}) { | |
| 1502 | |
| 1503 if (packages == null) { | |
| 1504 packages = this.packages; | |
| 1505 } | |
| 1506 if (options == null) { | |
| 1507 options = this.options; | |
| 1508 } | |
| 1509 if (constants == null) { | |
| 1510 constants = this.constants; | |
| 1511 } | |
| 1512 if (deviceAddress == null) { | |
| 1513 deviceAddress = this.deviceAddress; | |
| 1514 } | |
| 1515 if (deviceType == null) { | |
| 1516 deviceType = this.deviceType; | |
| 1517 } | |
| 1518 if (incrementalMode == null) { | |
| 1519 incrementalMode = this.incrementalMode; | |
| 1520 } | |
| 1521 return new Settings( | |
| 1522 packages, | |
| 1523 options, | |
| 1524 constants, | |
| 1525 deviceAddress, | |
| 1526 deviceType, | |
| 1527 incrementalMode); | |
| 1528 } | |
| 1529 | |
| 1530 String toString() { | |
| 1531 return "Settings(" | |
| 1532 "packages: $packages, " | |
| 1533 "options: $options, " | |
| 1534 "constants: $constants, " | |
| 1535 "device_address: $deviceAddress, " | |
| 1536 "device_type: $deviceType, " | |
| 1537 "incremental_mode: $incrementalMode)"; | |
| 1538 } | |
| 1539 | |
| 1540 Map<String, dynamic> toJson() { | |
| 1541 Map<String, dynamic> result = <String, dynamic>{}; | |
| 1542 | |
| 1543 void addIfNotNull(String name, value) { | |
| 1544 if (value != null) { | |
| 1545 result[name] = value; | |
| 1546 } | |
| 1547 } | |
| 1548 | |
| 1549 addIfNotNull("packages", packages == null ? null : "$packages"); | |
| 1550 addIfNotNull("options", options); | |
| 1551 addIfNotNull("constants", constants); | |
| 1552 addIfNotNull("device_address", deviceAddress); | |
| 1553 addIfNotNull( | |
| 1554 "device_type", | |
| 1555 deviceType == null ? null : unParseDeviceType(deviceType)); | |
| 1556 addIfNotNull( | |
| 1557 "incremental_mode", | |
| 1558 incrementalMode == null | |
| 1559 ? null : unparseIncrementalMode(incrementalMode)); | |
| 1560 | |
| 1561 return result; | |
| 1562 } | |
| 1563 } | |
| OLD | NEW |