flutter_platform.dart 25.2 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

8
import 'package:meta/meta.dart';
9 10
import 'package:stream_channel/stream_channel.dart';

Ian Hickson's avatar
Ian Hickson committed
11 12 13
import 'package:test/src/backend/test_platform.dart'; // ignore: implementation_imports
import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation_imports
import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports
14

15
import '../base/common.dart';
16
import '../base/file_system.dart';
17
import '../base/io.dart';
18
import '../base/process_manager.dart';
19
import '../dart/package_map.dart';
20
import '../globals.dart';
21
import 'watcher.dart';
22

23 24
/// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method.
25
const Duration _kTestStartupTimeout = const Duration(seconds: 5);
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

/// The timeout we give the test process to start executing Dart code. When the
/// CPU is under severe load, this can take a while, but it's not indicative of
/// any problem with Flutter, so we give it a large timeout.
const Duration _kTestProcessTimeout = const Duration(minutes: 5);

/// Message logged by the test process to signal that its main method has begun
/// execution.
///
/// The test harness responds by starting the [_kTestStartupTimeout] countdown.
/// The CPU may be throttled, which can cause a long delay in between when the
/// process is spawned and when dart code execution begins; we don't want to
/// hold that against the test.
const String _kStartTimeoutTimerMessage = 'sky_shell test process has entered main method';

41 42
/// The address at which our WebSocket server resides and at which the sky_shell
/// processes will host the Observatory server.
43 44 45 46
final Map<InternetAddressType, InternetAddress> _kHosts = <InternetAddressType, InternetAddress>{
  InternetAddressType.IP_V4: InternetAddress.LOOPBACK_IP_V4,
  InternetAddressType.IP_V6: InternetAddress.LOOPBACK_IP_V6,
};
47

48 49
/// Configure the `test` package to work with Flutter.
///
50
/// On systems where each [_FlutterPlatform] is only used to run one test suite
51 52 53 54
/// (that is, one Dart file with a `*_test.dart` file name and a single `void
/// main()`), you can set an observatory port and a diagnostic port explicitly.
void installHook({
  @required String shellPath,
55 56
  TestWatcher watcher,
  bool enableObservatory: false,
57
  bool machine: false,
58
  bool startPaused: false,
59 60
  int observatoryPort,
  int diagnosticPort,
61
  InternetAddressType serverType: InternetAddressType.IP_V4,
62
}) {
63 64
  if (startPaused || observatoryPort != null || diagnosticPort != null)
    assert(enableObservatory);
65 66
  hack.registerPlatformPlugin(
    <TestPlatform>[TestPlatform.vm],
67
    () => new _FlutterPlatform(
68
      shellPath: shellPath,
69
      watcher: watcher,
70
      machine: machine,
71 72
      enableObservatory: enableObservatory,
      startPaused: startPaused,
73 74
      explicitObservatoryPort: observatoryPort,
      explicitDiagnosticPort: diagnosticPort,
75
      host: _kHosts[serverType],
76
    ),
77
  );
78 79
}

80
enum _InitialResult { crashed, timedOut, connected }
81
enum _TestResult { crashed, harnessBailed, testBailed }
82
typedef Future<Null> _Finalizer();
83

84
class _FlutterPlatform extends PlatformPlugin {
85
  _FlutterPlatform({
86
    @required this.shellPath,
87 88
    this.watcher,
    this.enableObservatory,
89
    this.machine,
90
    this.startPaused,
91 92
    this.explicitObservatoryPort,
    this.explicitDiagnosticPort,
93
    this.host,
94
  }) {
95 96
    assert(shellPath != null);
  }
97

98
  final String shellPath;
99 100
  final TestWatcher watcher;
  final bool enableObservatory;
101
  final bool machine;
102
  final bool startPaused;
103 104
  final int explicitObservatoryPort;
  final int explicitDiagnosticPort;
105
  final InternetAddress host;
106

107 108 109 110 111 112 113
  // Each time loadChannel() is called, we spin up a local WebSocket server,
  // then spin up the engine in a subprocess. We pass the engine a Dart file
  // that connects to our WebSocket server, then we proxy JSON messages from
  // the test harness to the engine and back again. If at any time the engine
  // crashes, we inject an error into that stream. When the process closes,
  // we clean everything up.

114 115
  int _testCount = 0;

116 117
  @override
  StreamChannel<dynamic> loadChannel(String testPath, TestPlatform platform) {
118 119
    // Fail if there will be a port conflict.
    if (explicitObservatoryPort != null || explicitDiagnosticPort != null) {
120
      if (_testCount > 0)
121
        throwToolExit('installHook() was called with an observatory port, a diagnostic port, both, or debugger mode enabled, but then more than one test suite was run.');
122
    }
123
    final int ourTestCount = _testCount;
124
    _testCount += 1;
125 126 127 128
    final StreamController<dynamic> localController = new StreamController<dynamic>();
    final StreamController<dynamic> remoteController = new StreamController<dynamic>();
    final Completer<Null> testCompleteCompleter = new Completer<Null>();
    final _FlutterPlatformStreamSinkWrapper<dynamic> remoteSink = new _FlutterPlatformStreamSinkWrapper<dynamic>(
129 130 131
      remoteController.sink,
      testCompleteCompleter.future,
    );
132
    final StreamChannel<dynamic> localChannel = new StreamChannel<dynamic>.withGuarantees(
133 134 135
      remoteController.stream,
      localController.sink,
    );
136
    final StreamChannel<dynamic> remoteChannel = new StreamChannel<dynamic>.withGuarantees(
137 138 139
      localController.stream,
      remoteSink,
    );
140
    testCompleteCompleter.complete(_startTest(testPath, localChannel, ourTestCount));
141
    return remoteChannel;
142
  }
143

144 145 146 147
  Future<Null> _startTest(
    String testPath,
    StreamChannel<dynamic> controller,
    int ourTestCount) async {
148
    printTrace('test $ourTestCount: starting test $testPath');
149

150 151
    dynamic outOfBandError; // error that we couldn't send to the harness that we need to send via our future

152 153 154 155
    final List<_Finalizer> finalizers = <_Finalizer>[];
    bool subprocessActive = false;
    bool controllerSinkClosed = false;
    try {
156 157
      // Callback can't throw since it's just setting a variable.
      controller.sink.done.whenComplete(() { controllerSinkClosed = true; }); // ignore: unawaited_futures
158 159

      // Prepare our WebSocket server to talk to the engine subproces.
160
      final HttpServer server = await HttpServer.bind(host, 0);
161 162 163
      finalizers.add(() async {
        printTrace('test $ourTestCount: shutting down test harness socket server');
        await server.close(force: true);
164
      });
165
      final Completer<WebSocket> webSocket = new Completer<WebSocket>();
166 167
      server.listen(
        (HttpRequest request) {
168 169
          if (!webSocket.isCompleted)
            webSocket.complete(WebSocketTransformer.upgrade(request));
170 171 172 173 174 175 176 177 178 179 180 181 182
        },
        onError: (dynamic error, dynamic stack) {
          // If you reach here, it's unlikely we're going to be able to really handle this well.
          printTrace('test $ourTestCount: test harness socket server experienced an unexpected error: $error');
          if (!controllerSinkClosed) {
            controller.sink.addError(error, stack);
            controller.sink.close();
          } else {
            printError('unexpected error from test harness socket server: $error');
          }
        },
        cancelOnError: true,
      );
183

184
      // Prepare a temporary directory to store the Dart file that will talk to us.
185
      final Directory temporaryDirectory = fs.systemTempDirectory.createTempSync('dart_test_listener');
186 187 188 189
      finalizers.add(() async {
        printTrace('test $ourTestCount: deleting temporary directory');
        temporaryDirectory.deleteSync(recursive: true);
      });
190

191
      // Prepare the Dart file that will talk to us and start the test.
192
      final File listenerFile = fs.file('${temporaryDirectory.path}/listener.dart');
193 194
      listenerFile.createSync();
      listenerFile.writeAsStringSync(_generateTestMain(
195
        testUrl: fs.path.toUri(fs.path.absolute(testPath)).toString(),
196
        encodedWebsocketUrl: Uri.encodeComponent(_getWebSocketUrl(server)),
197
      ));
198

199
      // Start the engine subprocess.
200
      printTrace('test $ourTestCount: starting shell process');
201
      final Process process = await _startProcess(
202 203 204
        shellPath,
        listenerFile.path,
        packages: PackageMap.globalPackagesPath,
205 206
        enableObservatory: enableObservatory,
        startPaused: startPaused,
207 208
        observatoryPort: explicitObservatoryPort,
        diagnosticPort: explicitDiagnosticPort,
209 210 211
      );
      subprocessActive = true;
      finalizers.add(() async {
212 213
        if (subprocessActive) {
          printTrace('test $ourTestCount: ensuring end-of-process for shell');
214
          process.kill();
215 216
          final int exitCode = await process.exitCode;
          subprocessActive = false;
217 218 219
          if (!controllerSinkClosed && exitCode != -15) { // ProcessSignal.SIGTERM
            // We expect SIGTERM (15) because we tried to terminate it.
            // It's negative because signals are returned as negative exit codes.
220
            final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'after tests finished'), testPath, shellPath);
221 222
            controller.sink.addError(message);
          }
223 224
        }
      });
225

226
      final Completer<Null> timeout = new Completer<Null>();
227

228
      // Pipe stdout and stderr from the subprocess to our printStatus console.
229
      // We also keep track of what observatory port the engine used, if any.
230
      Uri processObservatoryUri;
231

232 233
      _pipeStandardStreamsToConsole(
        process,
234 235
        reportObservatoryUri: (Uri detectedUri) {
          assert(processObservatoryUri == null);
236
          assert(explicitObservatoryPort == null ||
237
                 explicitObservatoryPort == detectedUri.port);
238
          if (startPaused && !machine) {
239 240
            printStatus('The test process has been started.');
            printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
241
            printStatus('  $detectedUri');
242 243
            printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
          } else {
244 245 246 247
            printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}');
          }
          if (watcher != null) {
            watcher.onStartedProcess(new ProcessEvent(ourTestCount, process, detectedUri));
248
          }
249
          processObservatoryUri = detectedUri;
250 251
        },
        startTimeoutTimer: () {
252
          new Future<_InitialResult>.delayed(_kTestStartupTimeout).then((_) => timeout.complete());
253 254
        },
      );
255

256 257 258 259 260
      // At this point, three things can happen next:
      // The engine could crash, in which case process.exitCode will complete.
      // The engine could connect to us, in which case webSocket.future will complete.
      // The local test harness could get bored of us.

261
      printTrace('test $ourTestCount: awaiting initial result for pid ${process.pid}');
262
      final _InitialResult initialResult = await Future.any(<Future<_InitialResult>>[
263 264 265 266
        process.exitCode.then<_InitialResult>((int exitCode) => _InitialResult.crashed),
        timeout.future.then<_InitialResult>((Null _) => _InitialResult.timedOut),
        new Future<_InitialResult>.delayed(_kTestProcessTimeout, () => _InitialResult.timedOut),
        webSocket.future.then<_InitialResult>((WebSocket webSocket) => _InitialResult.connected),
267 268 269 270
      ]);

      switch (initialResult) {
        case _InitialResult.crashed:
271
          printTrace('test $ourTestCount: process with pid ${process.pid} crashed before connecting to test harness');
272
          final int exitCode = await process.exitCode;
273
          subprocessActive = false;
274
          final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before connecting to test harness'), testPath, shellPath);
275
          controller.sink.addError(message);
276 277
          // Awaited for with 'sink.done' below.
          controller.sink.close(); // ignore: unawaited_futures
278
          printTrace('test $ourTestCount: waiting for controller sink to close');
279 280 281
          await controller.sink.done;
          break;
        case _InitialResult.timedOut:
282
          printTrace('test $ourTestCount: timed out waiting for process with pid ${process.pid} to connect to test harness');
283
          final String message = _getErrorMessage('Test never connected to test harness.', testPath, shellPath);
284
          controller.sink.addError(message);
285 286
          // Awaited for with 'sink.done' below.
          controller.sink.close(); // ignore: unawaited_futures
287
          printTrace('test $ourTestCount: waiting for controller sink to close');
288 289 290
          await controller.sink.done;
          break;
        case _InitialResult.connected:
291
          printTrace('test $ourTestCount: process with pid ${process.pid} connected to test harness');
292
          final WebSocket testSocket = await webSocket.future;
293

294 295
          final Completer<Null> harnessDone = new Completer<Null>();
          final StreamSubscription<dynamic> harnessToTest = controller.stream.listen(
296
            (dynamic event) { testSocket.add(JSON.encode(event)); },
297
            onDone: harnessDone.complete,
298 299 300 301 302 303 304 305 306 307 308
            onError: (dynamic error, dynamic stack) {
              // If you reach here, it's unlikely we're going to be able to really handle this well.
              printError('test harness controller stream experienced an unexpected error\ntest: $testPath\nerror: $error');
              if (!controllerSinkClosed) {
                controller.sink.addError(error, stack);
                controller.sink.close();
              } else {
                printError('unexpected error from test harness controller stream: $error');
              }
            },
            cancelOnError: true,
309 310
          );

311 312
          final Completer<Null> testDone = new Completer<Null>();
          final StreamSubscription<dynamic> testToHarness = testSocket.listen(
313 314 315
            (dynamic encodedEvent) {
              assert(encodedEvent is String); // we shouldn't ever get binary messages
              controller.sink.add(JSON.decode(encodedEvent));
316
            },
317
            onDone: testDone.complete,
318 319 320 321 322 323 324 325 326 327 328
            onError: (dynamic error, dynamic stack) {
              // If you reach here, it's unlikely we're going to be able to really handle this well.
              printError('test socket stream experienced an unexpected error\ntest: $testPath\nerror: $error');
              if (!controllerSinkClosed) {
                controller.sink.addError(error, stack);
                controller.sink.close();
              } else {
                printError('unexpected error from test socket stream: $error');
              }
            },
            cancelOnError: true,
329 330
          );

331
          printTrace('test $ourTestCount: awaiting test result for pid ${process.pid}');
332
          final _TestResult testResult = await Future.any(<Future<_TestResult>>[
333 334
            process.exitCode.then<_TestResult>((int exitCode) { return _TestResult.crashed; }),
            harnessDone.future.then<_TestResult>((Null _) { return _TestResult.harnessBailed; }),
335
            testDone.future.then<_TestResult>((Null _) { return _TestResult.testBailed; }),
336 337
          ]);

338 339 340 341
          await Future.wait(<Future<Null>>[
            harnessToTest.cancel(),
            testToHarness.cancel(),
          ]);
342 343 344

          switch (testResult) {
            case _TestResult.crashed:
345
              printTrace('test $ourTestCount: process with pid ${process.pid} crashed');
346
              final int exitCode = await process.exitCode;
347
              subprocessActive = false;
348
              final String message = _getErrorMessage(_getExitCodeMessage(exitCode, 'before test harness closed its WebSocket'), testPath, shellPath);
349
              controller.sink.addError(message);
350 351
              // Awaited for with 'sink.done' below.
              controller.sink.close(); // ignore: unawaited_futures
352
              printTrace('test $ourTestCount: waiting for controller sink to close');
353 354 355
              await controller.sink.done;
              break;
            case _TestResult.harnessBailed:
356 357 358 359
              printTrace('test $ourTestCount: process with pid ${process.pid} no longer needed by test harness');
              break;
            case _TestResult.testBailed:
              printTrace('test $ourTestCount: process with pid ${process.pid} no longer needs test harness');
360 361 362 363 364
              break;
          }
          break;
      }

365 366 367
      if (subprocessActive && watcher != null) {
        await watcher.onFinishedTests(
            new ProcessEvent(ourTestCount, process, processObservatoryUri));
368 369 370
      }
    } catch (error, stack) {
      printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
371
      if (!controllerSinkClosed) {
372
        controller.sink.addError(error, stack);
373
      } else {
374
        printError('unhandled error during test:\n$testPath\n$error');
375
        outOfBandError ??= error;
376 377
      }
    } finally {
378 379 380 381 382 383 384 385 386 387
      printTrace('test $ourTestCount: cleaning up...');
      for (_Finalizer finalizer in finalizers) {
        try {
          await finalizer();
        } catch (error, stack) {
          printTrace('test $ourTestCount: error while cleaning up; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
          if (!controllerSinkClosed) {
            controller.sink.addError(error, stack);
          } else {
            printError('unhandled error during finalization of test:\n$testPath\n$error');
388
            outOfBandError ??= error;
389 390 391
          }
        }
      }
392
      if (!controllerSinkClosed) {
393 394
        // Waiting below with await.
        controller.sink.close(); // ignore: unawaited_futures
395
        printTrace('test $ourTestCount: waiting for controller sink to close');
396 397 398 399 400
        await controller.sink.done;
      }
    }
    assert(!subprocessActive);
    assert(controllerSinkClosed);
401 402 403 404
    if (outOfBandError != null) {
      printTrace('test $ourTestCount: finished with out-of-band failure');
      throw outOfBandError;
    }
405 406
    printTrace('test $ourTestCount: finished');
    return null;
407 408
  }

409 410 411 412 413 414
  String _getWebSocketUrl(HttpServer server) {
    return host.type == InternetAddressType.IP_V4
        ? "ws://${host.address}:${server.port}"
        : "ws://[${host.address}]:${server.port}";
  }

415 416 417 418 419
  String _generateTestMain({
    String testUrl,
    String encodedWebsocketUrl,
  }) {
    return '''
420
import 'dart:convert';
421
import 'dart:io'; // ignore: dart_io_import
422

423 424 425
// We import this library first in order to trigger an import error for
// package:test (rather than package:stream_channel) when the developer forgets
// to add a dependency on package:test.
426
import 'package:test/src/runner/plugin/remote_platform_helpers.dart';
427 428

import 'package:stream_channel/stream_channel.dart';
429 430
import 'package:test/src/runner/vm/catch_isolate_errors.dart';

431
import '$testUrl' as test;
432 433

void main() {
434
  print('$_kStartTimeoutTimerMessage');
435
  String server = Uri.decodeComponent('$encodedWebsocketUrl');
436 437 438 439 440 441 442 443 444
  StreamChannel channel = serializeSuite(() {
    catchIsolateErrors();
    return test.main;
  });
  WebSocket.connect(server).then((WebSocket socket) {
    socket.map(JSON.decode).pipe(channel.sink);
    socket.addStream(channel.stream.map(JSON.encode));
  });
}
445 446 447 448 449 450 451 452 453 454
''';
  }

  File _cachedFontConfig;

  /// Returns a Fontconfig config file that limits font fallback to the
  /// artifact cache directory.
  File get _fontConfigFile {
    if (_cachedFontConfig != null)
      return _cachedFontConfig;
455

456
    final StringBuffer sb = new StringBuffer();
457 458 459 460 461
    sb.writeln('<fontconfig>');
    sb.writeln('  <dir>${cache.getCacheArtifacts().path}</dir>');
    sb.writeln('  <cachedir>/var/cache/fontconfig</cachedir>');
    sb.writeln('</fontconfig>');

462
    final Directory fontsDir = fs.systemTempDirectory.createTempSync('flutter_fonts');
463
    _cachedFontConfig = fs.file('${fontsDir.path}/fonts.conf');
464 465 466 467 468
    _cachedFontConfig.createSync();
    _cachedFontConfig.writeAsStringSync(sb.toString());
    return _cachedFontConfig;
  }

469 470 471 472 473
  Future<Process> _startProcess(
    String executable,
    String testPath, {
    String packages,
    bool enableObservatory: false,
474
    bool startPaused: false,
475 476 477
    int observatoryPort,
    int diagnosticPort,
  }) {
478
    assert(executable != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
479
    assert(!startPaused || enableObservatory);
480
    final List<String> command = <String>[executable];
481
    if (enableObservatory) {
482
      // Some systems drive the _FlutterPlatform class in an unusual way, where
483 484 485 486
      // only one test file is processed at a time, and the operating
      // environment hands out specific ports ahead of time in a cooperative
      // manner, where we're only allowed to open ports that were given to us in
      // advance like this. For those esoteric systems, we have this feature
487
      // whereby you can create _FlutterPlatform with a pair of ports.
488 489 490 491
      //
      // I mention this only so that you won't be tempted, as I was, to apply
      // the obvious simplification to this code and remove this entire feature.
      if (observatoryPort != null)
492
        command.add('--observatory-port=$observatoryPort');
493
      if (diagnosticPort != null)
494
        command.add('--diagnostic-port=$diagnosticPort');
495
      if (startPaused)
496
        command.add('--start-paused');
497
    } else {
498
      command.addAll(<String>['--disable-observatory', '--disable-diagnostic']);
499
    }
500 501
    if (host.type == InternetAddressType.IP_V6)
      command.add('--ipv6');
502
    command.addAll(<String>[
503 504 505
      '--enable-dart-profiling',
      '--non-interactive',
      '--enable-checked-mode',
506
      '--use-test-fonts',
507 508 509
      '--packages=$packages',
      testPath,
    ]);
510
    printTrace(command.join(' '));
511
    final Map<String, String> environment = <String, String>{
512 513 514
      'FLUTTER_TEST': 'true',
      'FONTCONFIG_FILE': _fontConfigFile.path,
    };
515
    return processManager.start(command, environment: environment);
516
  }
517

518 519 520
  void _pipeStandardStreamsToConsole(
    Process process, {
    void startTimeoutTimer(),
521
    void reportObservatoryUri(Uri uri),
522
  }) {
523 524
    final String observatoryString = 'Observatory listening on ';
    final String diagnosticServerString = 'Diagnostic server listening on ';
525

526 527 528 529
    for (Stream<List<int>> stream in
        <Stream<List<int>>>[process.stderr, process.stdout]) {
      stream.transform(UTF8.decoder)
        .transform(const LineSplitter())
530 531 532 533 534 535 536 537
        .listen(
          (String line) {
            if (line == _kStartTimeoutTimerMessage) {
              if (startTimeoutTimer != null)
                startTimeoutTimer();
            } else if (line.startsWith('error: Unable to read Dart source \'package:test/')) {
              printTrace('Shell: $line');
              printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n');
538
            } else if (line.startsWith(observatoryString)) {
539 540
              printTrace('Shell: $line');
              try {
541 542 543
                final Uri uri = Uri.parse(line.substring(observatoryString.length));
                if (reportObservatoryUri != null)
                  reportObservatoryUri(uri);
544 545 546
              } catch (error) {
                printError('Could not parse shell observatory port message: $error');
              }
547
            } else if (line.startsWith(diagnosticServerString)) {
548 549 550 551 552 553 554 555 556 557
              printTrace('Shell: $line');
            } else if (line != null) {
              printStatus('Shell: $line');
            }
          },
          onError: (dynamic error) {
            printError('shell console stream for process pid ${process.pid} experienced an unexpected error: $error');
          },
          cancelOnError: true,
        );
558
    }
559
  }
560

561 562 563
  String _getErrorMessage(String what, String testPath, String shellPath) {
    return '$what\nTest: $testPath\nShell: $shellPath\n\n';
  }
564

565 566
  String _getExitCodeMessage(int exitCode, String when) {
    switch (exitCode) {
567 568
      case 1:
        return 'Shell subprocess cleanly reported an error $when. Check the logs above for an error message.';
569 570 571 572 573 574 575 576
      case 0:
        return 'Shell subprocess ended cleanly $when. Did main() call exit()?';
      case -0x0f: // ProcessSignal.SIGTERM
        return 'Shell subprocess crashed with SIGTERM ($exitCode) $when.';
      case -0x0b: // ProcessSignal.SIGSEGV
        return 'Shell subprocess crashed with segmentation fault $when.';
      case -0x06: // ProcessSignal.SIGABRT
        return 'Shell subprocess crashed with SIGABRT ($exitCode) $when.';
577 578
      case -0x02: // ProcessSignal.SIGINT
        return 'Shell subprocess terminated by ^C (SIGINT, $exitCode) $when.';
579 580
      default:
        return 'Shell subprocess crashed with unexpected exit code $exitCode $when.';
581 582 583
    }
  }
}
584 585 586 587 588 589 590 591 592 593 594 595

class _FlutterPlatformStreamSinkWrapper<S> implements StreamSink<S> {
  _FlutterPlatformStreamSinkWrapper(this._parent, this._shellProcessClosed);
  final StreamSink<S> _parent;
  final Future<Null> _shellProcessClosed;

  @override
  Future<Null> get done => _done.future;
  final Completer<Null> _done = new Completer<Null>();

  @override
  Future<dynamic> close() {
596
   Future.wait<dynamic>(<Future<dynamic>>[
597 598
      _parent.close(),
      _shellProcessClosed,
599
    ]).then<Null>(
600 601 602
      (List<dynamic> value) {
        _done.complete();
      },
603
      onError: _done.completeError,
604 605 606 607 608 609 610 611 612 613 614
    );
    return done;
  }

  @override
  void add(S event) => _parent.add(event);
  @override
  void addError(dynamic errorEvent, [ StackTrace stackTrace ]) => _parent.addError(errorEvent, stackTrace);
  @override
  Future<dynamic> addStream(Stream<S> stream) => _parent.addStream(stream);
}