blob: 104dafc51c89331029425d17c32629d0a26b4e2c [file] [log] [blame]
// Copyright 2013 The Flutter 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';
import 'dart:io';
import 'package:stack_trace/stack_trace.dart';
import 'package:typed_data/typed_buffers.dart';
class BrowserProcess {
/// Creates a new browser.
///
/// Clients pass in [startBrowser], which asynchronously returns the browser
/// process. Any errors in [startBrowser] (even those raised asynchronously
/// after it returns) are piped to [onExit] and will cause the browser to be
/// killed.
BrowserProcess(Future<Process> Function() startBrowser) {
// Don't return a Future here because there's no need for the caller to wait
// for the process to actually start. They should just wait for the HTTP
// request instead.
runZonedGuarded(() async {
final Process process = await startBrowser();
_processCompleter.complete(process);
final Uint8Buffer output = Uint8Buffer();
void drainOutput(Stream<List<int>> stream) {
try {
_ioSubscriptions
.add(stream.listen(output.addAll, cancelOnError: true));
} on StateError catch (_) {}
}
// If we don't drain the stdout and stderr the process can hang.
drainOutput(process.stdout);
drainOutput(process.stderr);
final int exitCode = await process.exitCode;
// This hack dodges an otherwise intractable race condition. When the user
// presses Control-C, the signal is sent to the browser and the test
// runner at the same time. It's possible for the browser to exit before
// the [Browser.close] is called, which would trigger the error below.
//
// A negative exit code signals that the process exited due to a signal.
// However, it's possible that this signal didn't come from the user's
// Control-C, in which case we do want to throw the error. The only way to
// resolve the ambiguity is to wait a brief amount of time and see if this
// browser is actually closed.
if (!_closed && exitCode < 0) {
await Future<void>.delayed(const Duration(milliseconds: 200));
}
if (!_closed && exitCode != 0) {
final String outputString = utf8.decode(output);
String message = 'Browser process failed with exit code $exitCode.';
if (outputString.isNotEmpty) {
message += '\nStandard output:\n$outputString';
}
throw Exception(message);
}
_onExitCompleter.complete();
}, (dynamic error, StackTrace? stackTrace) {
// Ignore any errors after the browser has been closed.
if (_closed) {
return;
}
// Make sure the process dies even if the error wasn't fatal.
_process.then((Process process) => process.kill());
stackTrace ??= Trace.current();
if (_onExitCompleter.isCompleted) {
return;
}
_onExitCompleter.completeError(
Exception('Failed to run browser process: $error.'),
stackTrace,
);
});
}
/// The underlying process.
///
/// This will fire once the process has started successfully.
Future<Process> get _process => _processCompleter.future;
final Completer<Process> _processCompleter = Completer<Process>();
/// Whether [close] has been called.
bool _closed = false;
/// A future that completes when the browser exits.
///
/// If there's a problem starting or running the browser, this will complete
/// with an error.
Future<void> get onExit => _onExitCompleter.future;
final Completer<void> _onExitCompleter = Completer<void>();
/// Standard IO streams for the underlying browser process.
final List<StreamSubscription<void>> _ioSubscriptions = <StreamSubscription<void>>[];
/// Kills the browser process.
///
/// Returns the same [Future] as [onExit], except that it won't emit
/// exceptions.
Future<void> close() async {
_closed = true;
// If we don't manually close the stream the test runner can hang.
// For example this happens with Chrome Headless.
// See SDK issue: https://github.com/dart-lang/sdk/issues/31264
for (final StreamSubscription<void> stream in _ioSubscriptions) {
unawaited(stream.cancel());
}
(await _process).kill();
// Swallow exceptions. The user should explicitly use [onExit] for these.
return onExit.catchError((dynamic _) {});
}
}