| 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 /// Provides a [Command] interface for interacting with a Fletch driver session. | |
| 6 /// | |
| 7 /// Normally, this is used by test.dart, but is also has a [main] method that | |
| 8 /// makes it possible to run a test outside test.dart. | |
| 9 library test.fletch_session_command; | |
| 10 | |
| 11 import 'dart:async' show | |
| 12 Completer, | |
| 13 EventSink, | |
| 14 Future, | |
| 15 Stream, | |
| 16 StreamController, | |
| 17 StreamTransformer, | |
| 18 Timer; | |
| 19 | |
| 20 import 'dart:collection' show | |
| 21 Queue; | |
| 22 | |
| 23 import 'dart:convert' show | |
| 24 UTF8, | |
| 25 LineSplitter; | |
| 26 | |
| 27 import 'dart:io' show | |
| 28 BytesBuilder, | |
| 29 Platform, | |
| 30 Process, | |
| 31 ProcessSignal; | |
| 32 | |
| 33 import 'test_runner.dart' show | |
| 34 Command, | |
| 35 CommandOutputImpl; | |
| 36 | |
| 37 import 'decode_exit_code.dart' show | |
| 38 DecodeExitCode; | |
| 39 | |
| 40 import '../../../pkg/fletchc/lib/src/hub/exit_codes.dart' show | |
| 41 COMPILER_EXITCODE_CONNECTION_ERROR, | |
| 42 COMPILER_EXITCODE_CRASH, | |
| 43 DART_VM_EXITCODE_COMPILE_TIME_ERROR, | |
| 44 DART_VM_EXITCODE_UNCAUGHT_EXCEPTION; | |
| 45 | |
| 46 import '../../../pkg/fletchc/lib/fletch_vm.dart' show | |
| 47 FletchVm; | |
| 48 | |
| 49 const String settingsFileNameFlag = "test.fletch_settings_file_name"; | |
| 50 const String settingsFileName = | |
| 51 const String.fromEnvironment(settingsFileNameFlag); | |
| 52 | |
| 53 /// Default timeout value (in seconds) used for running commands that are | |
| 54 /// assumed to complete fast. | |
| 55 // TODO(ahe): Lower this to 5 seconds. | |
| 56 const int defaultTimeout = 20; | |
| 57 | |
| 58 final Queue<FletchSessionMirror> sessions = new Queue<FletchSessionMirror>(); | |
| 59 | |
| 60 int sessionCount = 0; | |
| 61 | |
| 62 /// Return an available [FletchSessionMirror] or construct a new. | |
| 63 FletchSessionMirror getAvailableSession() { | |
| 64 if (sessions.isEmpty) { | |
| 65 return new FletchSessionMirror(sessionCount++); | |
| 66 } else { | |
| 67 return sessions.removeFirst(); | |
| 68 } | |
| 69 } | |
| 70 | |
| 71 void returnSession(FletchSessionMirror session) { | |
| 72 sessions.addLast(session); | |
| 73 } | |
| 74 | |
| 75 String explainExitCode(int code) { | |
| 76 String exit_message; | |
| 77 if (code == null) { | |
| 78 exit_message = "no exit code"; | |
| 79 } else if (code == 0) { | |
| 80 exit_message = "(success exit code)"; | |
| 81 } else if (code > 0) { | |
| 82 switch (code) { | |
| 83 case COMPILER_EXITCODE_CONNECTION_ERROR: | |
| 84 exit_message = "(connection error)"; | |
| 85 break; | |
| 86 case COMPILER_EXITCODE_CRASH: | |
| 87 exit_message = "(compiler crash)"; | |
| 88 break; | |
| 89 case DART_VM_EXITCODE_COMPILE_TIME_ERROR: | |
| 90 exit_message = "(compile-time error)"; | |
| 91 break; | |
| 92 case DART_VM_EXITCODE_UNCAUGHT_EXCEPTION: | |
| 93 exit_message = "(uncaught exception)"; | |
| 94 break; | |
| 95 default: | |
| 96 exit_message = "(error exit code)"; | |
| 97 break; | |
| 98 } | |
| 99 } else { | |
| 100 exit_message = "(signal ${-code})"; | |
| 101 if (code == -15 || code == -9) { | |
| 102 exit_message += " (killed by external signal - timeout?)"; | |
| 103 } else if (code == -7 || code == -11 || code == -4) { | |
| 104 // SIGBUS, SIGSEGV, SIGILL | |
| 105 exit_message += " (internal error)"; | |
| 106 } else if (code == -2) { | |
| 107 exit_message += " (control-C)"; | |
| 108 } else { | |
| 109 exit_message += " (see man 7 signal)"; | |
| 110 } | |
| 111 } | |
| 112 return exit_message; | |
| 113 } | |
| 114 | |
| 115 class FletchSessionCommand implements Command { | |
| 116 final String executable; | |
| 117 final String script; | |
| 118 final List<String> arguments; | |
| 119 final Map<String, String> environmentOverrides; | |
| 120 final String snapshotFileName; | |
| 121 final String settingsFileName; | |
| 122 | |
| 123 FletchSessionCommand( | |
| 124 this.executable, | |
| 125 this.script, | |
| 126 this.arguments, | |
| 127 this.environmentOverrides, | |
| 128 {this.snapshotFileName, | |
| 129 this.settingsFileName: ".fletch-settings"}); | |
| 130 | |
| 131 String get displayName => "fletch_session"; | |
| 132 | |
| 133 int get maxNumRetries => 0; | |
| 134 | |
| 135 String get reproductionCommand { | |
| 136 var dartVm = Uri.parse(executable).resolve('dart'); | |
| 137 String fletchPath = Uri.parse(executable).resolve('fletch-vm').toString(); | |
| 138 String versionFlag = '-Dfletch.version=`$fletchPath --version`'; | |
| 139 String settingsFileFlag = "-D$settingsFileNameFlag=$settingsFileName"; | |
| 140 | |
| 141 return """ | |
| 142 | |
| 143 | |
| 144 | |
| 145 There are three ways to reproduce this error: | |
| 146 | |
| 147 1. Run the test exactly as in this test framework. This is the hardest to | |
| 148 debug using gdb: | |
| 149 | |
| 150 ${Platform.executable} -c $settingsFileFlag \\ | |
| 151 $versionFlag \\ | |
| 152 tools/testing/dart/fletch_session_command.dart $executable \\ | |
| 153 ${arguments.join(' ')} | |
| 154 | |
| 155 | |
| 156 2. Run the helper program `tests/fletchc/run.dart` under `gdb` using | |
| 157 `set follow-fork-mode child`. This can be confusing, but makes it | |
| 158 easy to run a reproduction command in a loop: | |
| 159 | |
| 160 gdb -ex 'set follow-fork-mode child' -ex run --args \\ | |
| 161 $dartVm $settingsFileFlag \\ | |
| 162 $versionFlag \\ | |
| 163 -c tests/fletchc/run.dart $script | |
| 164 | |
| 165 3. Run the `fletch-vm` in gdb and attach to it via the helper program. This | |
| 166 is the easiest way to debug using both gdb and lldb. You need to start two | |
| 167 processes, each in their own terminal window: | |
| 168 | |
| 169 gdb -ex run --args $executable-vm --port=54321 | |
| 170 | |
| 171 $dartVm $settingsFileFlag \\ | |
| 172 $versionFlag \\ | |
| 173 -c -DattachToVm=54321 tests/fletchc/run.dart $script | |
| 174 | |
| 175 | |
| 176 """; | |
| 177 } | |
| 178 | |
| 179 Future<FletchTestCommandOutput> run( | |
| 180 int timeout, | |
| 181 bool verbose, | |
| 182 {bool superVerbose: false}) async { | |
| 183 if (arguments.length > 1) { | |
| 184 String options = arguments | |
| 185 .where((String argument) => argument != script) | |
| 186 .join(' '); | |
| 187 // TODO(ahe): Passing options to the incremental compiler isn't | |
| 188 // trivial. We don't want to reset the compiler each time an option | |
| 189 // changes. For example, when changing the package root, the compiler | |
| 190 // should refresh all package files to see if they have changed. | |
| 191 return compilerFail("Compiler options not implemented: $options"); | |
| 192 } | |
| 193 | |
| 194 FletchSessionHelper fletch = | |
| 195 new FletchSessionHelper( | |
| 196 getAvailableSession(), executable, environmentOverrides, | |
| 197 verbose, superVerbose); | |
| 198 | |
| 199 fletch.sessionMirror.printLoggedCommands(fletch.stdout, executable); | |
| 200 | |
| 201 Stopwatch sw = new Stopwatch()..start(); | |
| 202 int exitCode; | |
| 203 bool endedSession = false; | |
| 204 try { | |
| 205 Future vmTerminationFuture; | |
| 206 try { | |
| 207 await fletch.createSession(settingsFileName); | |
| 208 | |
| 209 // Now that the session is created, start a Fletch VM. | |
| 210 String vmSocketAddress = await fletch.spawnVm(); | |
| 211 // Timeout of the VM is implemented by shutting down the Fletch VM | |
| 212 // after [timeout] seconds. This ensures that compilation+runtime never | |
| 213 // exceed [timeout] seconds (plus whatever time is spent in setting up | |
| 214 // the session above). | |
| 215 vmTerminationFuture = fletch.shutdownVm(timeout); | |
| 216 await fletch.runInSession(["attach", "tcp_socket", vmSocketAddress]); | |
| 217 if (snapshotFileName != null) { | |
| 218 exitCode = await fletch.runInSession( | |
| 219 ["export", script, 'to', 'file', snapshotFileName], | |
| 220 checkExitCode: false, timeout: timeout); | |
| 221 } else { | |
| 222 exitCode = await fletch.runInSession(["compile", script], | |
| 223 checkExitCode: false, timeout: timeout); | |
| 224 fletch.stderr.writeln("Compilation took: ${sw.elapsed}"); | |
| 225 if (exitCode == 0) { | |
| 226 exitCode = await fletch.runInSession( | |
| 227 ["run", "--terminate-debugger"], | |
| 228 checkExitCode: false, timeout: timeout); | |
| 229 } | |
| 230 } | |
| 231 } finally { | |
| 232 if (exitCode == COMPILER_EXITCODE_CRASH) { | |
| 233 // If the compiler crashes, chances are that it didn't close the | |
| 234 // connection to the Fletch VM. So we kill it. | |
| 235 fletch.killVmProcess(ProcessSignal.SIGTERM); | |
| 236 } | |
| 237 int vmExitCode = await vmTerminationFuture; | |
| 238 fletch.stdout.writeln("Fletch VM exitcode is $vmExitCode " | |
| 239 "${explainExitCode(vmExitCode)}\n" | |
| 240 "Exit code reported by ${fletch.executable} is $exitCode " | |
| 241 "${explainExitCode(exitCode)}\n"); | |
| 242 if (exitCode == COMPILER_EXITCODE_CONNECTION_ERROR) { | |
| 243 fletch.stderr.writeln("Connection error from compiler"); | |
| 244 exitCode = vmExitCode; | |
| 245 } else if (exitCode != vmExitCode) { | |
| 246 if (!fletch.killedVmProcess || vmExitCode == null || | |
| 247 vmExitCode >= 0) { | |
| 248 throw new UnexpectedExitCode( | |
| 249 vmExitCode, "${fletch.executable}-vm", <String>[]); | |
| 250 } | |
| 251 } | |
| 252 } | |
| 253 } on UnexpectedExitCode catch (error) { | |
| 254 fletch.stderr.writeln("$error"); | |
| 255 exitCode = combineExitCodes(exitCode, error.exitCode); | |
| 256 try { | |
| 257 if (!endedSession) { | |
| 258 // TODO(ahe): Only end if there's a crash. | |
| 259 endedSession = true; | |
| 260 await fletch.run(["x-end", "session", fletch.sessionName]); | |
| 261 } | |
| 262 } on UnexpectedExitCode catch (error) { | |
| 263 fletch.stderr.writeln("$error"); | |
| 264 // TODO(ahe): Error ignored, long term we should be able to guarantee | |
| 265 // that shutting down a session never leads to an error. | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 if (exitCode == null) { | |
| 270 exitCode = COMPILER_EXITCODE_CRASH; | |
| 271 fletch.stdout.writeln( | |
| 272 '**test.py** could not determine a good exitcode, using $exitCode.'); | |
| 273 } | |
| 274 | |
| 275 if (endedSession) { | |
| 276 returnSession(new FletchSessionMirror(fletch.sessionMirror.id)); | |
| 277 } else { | |
| 278 returnSession(fletch.sessionMirror); | |
| 279 } | |
| 280 | |
| 281 return new FletchTestCommandOutput( | |
| 282 this, exitCode, fletch.hasTimedOut, | |
| 283 fletch.combinedStdout, fletch.combinedStderr, sw.elapsed, -1); | |
| 284 } | |
| 285 | |
| 286 FletchTestCommandOutput compilerFail(String message) { | |
| 287 return new FletchTestCommandOutput( | |
| 288 this, DART_VM_EXITCODE_COMPILE_TIME_ERROR, false, <int>[], | |
| 289 UTF8.encode(message), const Duration(seconds: 0), -1); | |
| 290 } | |
| 291 | |
| 292 String toString() => reproductionCommand; | |
| 293 | |
| 294 set displayName(_) => throw "not supported"; | |
| 295 | |
| 296 get commandLine => throw "not supported"; | |
| 297 set commandLine(_) => throw "not supported"; | |
| 298 | |
| 299 get outputIsUpToDate => throw "not supported"; | |
| 300 } | |
| 301 | |
| 302 /// [compiler] is assumed to be coming from `fletch` in which case | |
| 303 /// [COMPILER_EXITCODE_CRASH], [DART_VM_EXITCODE_COMPILE_TIME_ERROR], and | |
| 304 /// [DART_VM_EXITCODE_UNCAUGHT_EXCEPTION] all represent a compiler crash. | |
| 305 /// | |
| 306 /// [runtime] is assumed to be coming from `fletch-vm` in which case which case | |
| 307 /// [DART_VM_EXITCODE_COMPILE_TIME_ERROR], and | |
| 308 /// [DART_VM_EXITCODE_UNCAUGHT_EXCEPTION] is just the result of running a test | |
| 309 /// that has an error (not a crash). | |
| 310 int combineExitCodes(int compiler, int runtime) { | |
| 311 if (compiler == null) return runtime; | |
| 312 | |
| 313 if (runtime == null) return compiler; | |
| 314 | |
| 315 switch (compiler) { | |
| 316 case COMPILER_EXITCODE_CRASH: | |
| 317 case DART_VM_EXITCODE_COMPILE_TIME_ERROR: | |
| 318 case DART_VM_EXITCODE_UNCAUGHT_EXCEPTION: | |
| 319 // If the compiler exits with any of those values above, it crashed. It | |
| 320 // should never crash. | |
| 321 return COMPILER_EXITCODE_CRASH; | |
| 322 | |
| 323 default: | |
| 324 break; | |
| 325 } | |
| 326 | |
| 327 if (compiler < 0) { | |
| 328 // Normally, this would be a timeout. However, it can also signify that the | |
| 329 // Dart VM crashed, with, for example, SIGABRT or SIGSEGV. | |
| 330 return compiler; | |
| 331 } | |
| 332 | |
| 333 return runtime; | |
| 334 } | |
| 335 | |
| 336 class UnexpectedExitCode extends Error { | |
| 337 final int exitCode; | |
| 338 final String executable; | |
| 339 final List<String> arguments; | |
| 340 | |
| 341 UnexpectedExitCode(this.exitCode, this.executable, this.arguments); | |
| 342 | |
| 343 String toString() { | |
| 344 return "Non-zero exit code ($exitCode) from: " | |
| 345 "$executable ${arguments.join(' ')}"; | |
| 346 } | |
| 347 } | |
| 348 | |
| 349 class FletchTestCommandOutput extends CommandOutputImpl with DecodeExitCode { | |
| 350 FletchTestCommandOutput( | |
| 351 Command command, | |
| 352 int exitCode, | |
| 353 bool timedOut, | |
| 354 List<int> stdout, | |
| 355 List<int> stderr, | |
| 356 Duration time, | |
| 357 int pid) | |
| 358 : super(command, exitCode, timedOut, stdout, stderr, time, false, pid); | |
| 359 } | |
| 360 | |
| 361 Stream<List<int>> addPrefixWhenNotEmpty( | |
| 362 Stream<List<int>> input, | |
| 363 String prefix) async* { | |
| 364 bool isFirst = true; | |
| 365 await for (List<int> bytes in input) { | |
| 366 if (isFirst) { | |
| 367 yield UTF8.encode("$prefix\n"); | |
| 368 isFirst = false; | |
| 369 } | |
| 370 yield bytes; | |
| 371 } | |
| 372 } | |
| 373 | |
| 374 class BytesOutputSink implements Sink<List<int>> { | |
| 375 final BytesBuilder bytesBuilder = new BytesBuilder(); | |
| 376 | |
| 377 final Sink<List<int>> verboseSink; | |
| 378 | |
| 379 factory BytesOutputSink(bool isVerbose) { | |
| 380 StreamController<List<int>> verboseController = | |
| 381 new StreamController<List<int>>(); | |
| 382 Stream<List<int>> verboseStream = verboseController.stream; | |
| 383 if (isVerbose) { | |
| 384 verboseStream.transform(UTF8.decoder).transform(new LineSplitter()) | |
| 385 .listen(print); | |
| 386 } else { | |
| 387 verboseStream.listen(null); | |
| 388 } | |
| 389 return new BytesOutputSink.internal(verboseController); | |
| 390 } | |
| 391 | |
| 392 BytesOutputSink.internal(this.verboseSink); | |
| 393 | |
| 394 void add(List<int> data) { | |
| 395 verboseSink.add(data); | |
| 396 bytesBuilder.add(data); | |
| 397 } | |
| 398 | |
| 399 void writeln(String text) { | |
| 400 writeText("$text\n"); | |
| 401 } | |
| 402 | |
| 403 void writeText(String text) { | |
| 404 add(UTF8.encode(text)); | |
| 405 } | |
| 406 | |
| 407 void close() { | |
| 408 verboseSink.close(); | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 class FletchSessionHelper { | |
| 413 final String executable; | |
| 414 | |
| 415 final FletchSessionMirror sessionMirror; | |
| 416 | |
| 417 final String sessionName; | |
| 418 | |
| 419 final Map<String, String> environmentOverrides; | |
| 420 | |
| 421 final bool isVerbose; | |
| 422 | |
| 423 final BytesOutputSink stdout; | |
| 424 | |
| 425 final BytesOutputSink stderr; | |
| 426 | |
| 427 final BytesOutputSink vmStdout; | |
| 428 | |
| 429 final BytesOutputSink vmStderr; | |
| 430 | |
| 431 Process vmProcess; | |
| 432 | |
| 433 Future<int> vmExitCodeFuture; | |
| 434 | |
| 435 bool killedVmProcess = false; | |
| 436 | |
| 437 bool hasTimedOut = false; | |
| 438 | |
| 439 FletchSessionHelper( | |
| 440 FletchSessionMirror sessionMirror, | |
| 441 this.executable, | |
| 442 this.environmentOverrides, | |
| 443 this.isVerbose, | |
| 444 bool superVerbose) | |
| 445 : sessionMirror = sessionMirror, | |
| 446 sessionName = sessionMirror.makeSessionName(), | |
| 447 stdout = new BytesOutputSink(superVerbose), | |
| 448 stderr = new BytesOutputSink(superVerbose), | |
| 449 vmStdout = new BytesOutputSink(superVerbose), | |
| 450 vmStderr = new BytesOutputSink(superVerbose); | |
| 451 | |
| 452 List<int> get combinedStdout { | |
| 453 stdout.close(); | |
| 454 vmStdout.close(); | |
| 455 BytesBuilder combined = new BytesBuilder() | |
| 456 ..add(stdout.bytesBuilder.takeBytes()) | |
| 457 ..add(vmStdout.bytesBuilder.takeBytes()); | |
| 458 return combined.takeBytes(); | |
| 459 } | |
| 460 | |
| 461 List<int> get combinedStderr { | |
| 462 stderr.close(); | |
| 463 vmStderr.close(); | |
| 464 BytesBuilder combined = new BytesBuilder() | |
| 465 ..add(stderr.bytesBuilder.takeBytes()) | |
| 466 ..add(vmStderr.bytesBuilder.takeBytes()); | |
| 467 return combined.takeBytes(); | |
| 468 } | |
| 469 | |
| 470 /// Run [executable] with arguments and wait for it to exit. This method | |
| 471 /// uses [exitCodeWithTimeout] to ensure the process exits within [timeout] | |
| 472 /// seconds. | |
| 473 /// | |
| 474 /// If the process times out, UnexpectedExitCode is thrown. | |
| 475 /// | |
| 476 /// If [checkExitCode] is true (the default), UnexpectedExitCode is thrown | |
| 477 /// unless the process' exit code is 0. | |
| 478 Future<int> run( | |
| 479 List<String> arguments, | |
| 480 {bool checkExitCode: true, | |
| 481 int timeout: defaultTimeout}) async { | |
| 482 sessionMirror.logCommand(arguments); | |
| 483 Process process = await Process.start( | |
| 484 "$executable", arguments, environment: environmentOverrides); | |
| 485 String commandDescription = "$executable ${arguments.join(' ')}"; | |
| 486 if (isVerbose) { | |
| 487 print("Running $commandDescription"); | |
| 488 } | |
| 489 String commandDescriptionForLog = "\$ $commandDescription"; | |
| 490 stdout.writeln(commandDescriptionForLog); | |
| 491 Future stdoutFuture = process.stdout.listen(stdout.add).asFuture(); | |
| 492 Future stderrFuture = | |
| 493 addPrefixWhenNotEmpty(process.stderr, commandDescriptionForLog) | |
| 494 .listen(stderr.add) | |
| 495 .asFuture(); | |
| 496 await process.stdin.close(); | |
| 497 | |
| 498 // Can't reuse [hasTimedOut] as we don't want to throw when calling 'x-end' | |
| 499 // in the finally block above. | |
| 500 bool thisCommandTimedout = false; | |
| 501 | |
| 502 int exitCode = await exitCodeWithTimeout(process, timeout, () { | |
| 503 stdout.writeln( | |
| 504 "\n=> Reached command timeout (sent SIGTERM to fletch-vm)"); | |
| 505 thisCommandTimedout = true; | |
| 506 hasTimedOut = true; | |
| 507 if (vmProcess != null) { | |
| 508 killedVmProcess = vmProcess.kill(ProcessSignal.SIGTERM); | |
| 509 } | |
| 510 }); | |
| 511 await stdoutFuture; | |
| 512 await stderrFuture; | |
| 513 | |
| 514 stdout.writeln("\n => $exitCode ${explainExitCode(exitCode)}\n"); | |
| 515 if (checkExitCode && (thisCommandTimedout || exitCode != 0)) { | |
| 516 throw new UnexpectedExitCode(exitCode, executable, arguments); | |
| 517 } | |
| 518 return exitCode; | |
| 519 } | |
| 520 | |
| 521 /// Same as [run], except that the arguments "in session $sessionName" are | |
| 522 /// implied. | |
| 523 Future<int> runInSession( | |
| 524 List<String> arguments, | |
| 525 {bool checkExitCode: true, | |
| 526 int timeout: defaultTimeout}) { | |
| 527 return run( | |
| 528 []..addAll(arguments)..addAll(["in", "session", sessionName]), | |
| 529 checkExitCode: checkExitCode, timeout: timeout); | |
| 530 } | |
| 531 | |
| 532 Future<int> createSession(String settingsFileName) async { | |
| 533 if (sessionMirror.isCreated) { | |
| 534 return 0; | |
| 535 } else { | |
| 536 sessionMirror.isCreated = true; | |
| 537 return await run( | |
| 538 ["create", "session", sessionName, "with", settingsFileName]); | |
| 539 } | |
| 540 } | |
| 541 | |
| 542 Future<String> spawnVm() async { | |
| 543 FletchVm fletchVm = await FletchVm.start( | |
| 544 "$executable-vm", environment: environmentOverrides); | |
| 545 vmProcess = fletchVm.process; | |
| 546 String commandDescription = "$executable-vm"; | |
| 547 if (isVerbose) { | |
| 548 print("Running $commandDescription"); | |
| 549 } | |
| 550 String commandDescriptionForLog = "\$ $commandDescription"; | |
| 551 vmStdout.writeln(commandDescriptionForLog); | |
| 552 stdout.writeln('$commandDescriptionForLog &'); | |
| 553 | |
| 554 Future stdoutFuture = | |
| 555 fletchVm.stdoutLines.listen(vmStdout.writeln).asFuture(); | |
| 556 bool isFirstStderrLine = true; | |
| 557 Future stderrFuture = | |
| 558 fletchVm.stderrLines.listen( | |
| 559 (String line) { | |
| 560 if (isFirstStderrLine) { | |
| 561 vmStdout.writeln(commandDescriptionForLog); | |
| 562 isFirstStderrLine = false; | |
| 563 } | |
| 564 vmStdout.writeln(line); | |
| 565 }) | |
| 566 .asFuture(); | |
| 567 | |
| 568 vmExitCodeFuture = fletchVm.exitCode.then((int exitCode) async { | |
| 569 if (isVerbose) print("Exiting Fletch VM with exit code $exitCode."); | |
| 570 await stdoutFuture; | |
| 571 if (isVerbose) print("Stdout of Fletch VM process closed."); | |
| 572 await stderrFuture; | |
| 573 if (isVerbose) print("Stderr of Fletch VM process closed."); | |
| 574 return exitCode; | |
| 575 }); | |
| 576 | |
| 577 return "${fletchVm.host}:${fletchVm.port}"; | |
| 578 } | |
| 579 | |
| 580 /// Returns a future that completes when the fletch VM exits using | |
| 581 /// [exitCodeWithTimeout] to ensure termination within [timeout] seconds. | |
| 582 Future<int> shutdownVm(int timeout) async { | |
| 583 await exitCodeWithTimeout(vmProcess, timeout, () { | |
| 584 stdout.writeln( | |
| 585 "\n**fletch-vm** Reached total timeout (sent SIGTERM to fletch-vm)"); | |
| 586 killedVmProcess = true; | |
| 587 hasTimedOut = true; | |
| 588 }); | |
| 589 return vmExitCodeFuture; | |
| 590 } | |
| 591 | |
| 592 void killVmProcess(ProcessSignal signal) { | |
| 593 if (vmProcess == null) return; | |
| 594 killedVmProcess = vmProcess.kill(ProcessSignal.SIGTERM); | |
| 595 } | |
| 596 } | |
| 597 | |
| 598 /// Helper method for implementing timing out while waiting for [process] to | |
| 599 /// exit. [timeout] is in seconds. If the process times out, it will be killed | |
| 600 /// using SIGTERM and onTimeout will be called. | |
| 601 /// | |
| 602 /// After SIGTERM, the process has 5 seconds to exit or it will be killed with | |
| 603 /// SIGKILL. | |
| 604 /// | |
| 605 /// Note: We treat SIGKILL as a crash, not a timeout. The process is supposed | |
| 606 /// to exit quickly and gracefully after receiving SIGTERM. See | |
| 607 /// [DecodeExitCode] in `decode_exit_code.dart`. | |
| 608 Future<int> exitCodeWithTimeout( | |
| 609 Process process, | |
| 610 int timeout, | |
| 611 void onTimeout()) async { | |
| 612 if (process == null) return 0; | |
| 613 | |
| 614 bool done = false; | |
| 615 Timer timer; | |
| 616 | |
| 617 void secondTimeout() { | |
| 618 if (done) return; | |
| 619 process.kill(ProcessSignal.SIGKILL); | |
| 620 } | |
| 621 | |
| 622 void firstTimeout() { | |
| 623 if (done) return; | |
| 624 if (process.kill(ProcessSignal.SIGTERM)) { | |
| 625 timer = new Timer(const Duration(seconds: 5), secondTimeout); | |
| 626 onTimeout(); | |
| 627 } | |
| 628 } | |
| 629 | |
| 630 timer = new Timer(new Duration(seconds: timeout), firstTimeout); | |
| 631 | |
| 632 int exitCode = await process.exitCode; | |
| 633 done = true; | |
| 634 timer.cancel(); | |
| 635 return exitCode; | |
| 636 } | |
| 637 | |
| 638 /// Represents a session in the persistent Fletch client process. | |
| 639 class FletchSessionMirror { | |
| 640 static const int RINGBUFFER_SIZE = 15; | |
| 641 | |
| 642 final int id; | |
| 643 | |
| 644 final Queue<List<String>> internalLoggedCommands = new Queue<List<String>>(); | |
| 645 | |
| 646 bool isCreated = false; | |
| 647 | |
| 648 FletchSessionMirror(this.id); | |
| 649 | |
| 650 void logCommand(List<String> command) { | |
| 651 internalLoggedCommands.add(command); | |
| 652 if (internalLoggedCommands.length >= RINGBUFFER_SIZE) { | |
| 653 internalLoggedCommands.removeFirst(); | |
| 654 } | |
| 655 } | |
| 656 | |
| 657 void printLoggedCommands(BytesOutputSink sink, String executable) { | |
| 658 sink.writeln("Previous commands in this session:"); | |
| 659 for (List<String> command in internalLoggedCommands) { | |
| 660 sink.writeText(executable); | |
| 661 for (String argument in command) { | |
| 662 sink.writeText(" "); | |
| 663 sink.writeText(argument); | |
| 664 } | |
| 665 sink.writeln(""); | |
| 666 } | |
| 667 sink.writeln(""); | |
| 668 } | |
| 669 | |
| 670 String makeSessionName() => '$id'; | |
| 671 } | |
| 672 | |
| 673 Future<Null> main(List<String> arguments) async { | |
| 674 // Setting [sessionCount] to the current time in milliseconds ensures that it | |
| 675 // is highly unlikely that reproduction commands conflicts with an existing | |
| 676 // session in a persistent process that wasn't killed. | |
| 677 sessionCount = new DateTime.now().millisecondsSinceEpoch; | |
| 678 String executable = arguments.first; | |
| 679 String script = arguments[1]; | |
| 680 arguments = arguments.skip(2).toList(); | |
| 681 Map<String, String> environmentOverrides = <String, String>{}; | |
| 682 FletchSessionCommand command = new FletchSessionCommand( | |
| 683 executable, script, arguments, environmentOverrides, | |
| 684 settingsFileName: settingsFileName); | |
| 685 FletchTestCommandOutput output = | |
| 686 await command.run(0, true, superVerbose: true); | |
| 687 print("Test outcome: ${output.decodeExitCode()}"); | |
| 688 } | |
| OLD | NEW |