blob: 2c65f6d30f71e8ac4c2ec1ee5d8789a5c5db1ac0 [file] [log] [blame]
// 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';
import '../globals.dart';
import 'file_system.dart';
import 'io.dart';
import 'process_manager.dart';
typedef String StringConverter(String string);
/// A function that will be run before the VM exits.
typedef Future<dynamic> ShutdownHook();
// TODO(ianh): We have way too many ways to run subprocesses in this project.
/// The stage in which a [ShutdownHook] will be run. All shutdown hooks within
/// a given stage will be started in parallel and will be guaranteed to run to
/// completion before shutdown hooks in the next stage are started.
class ShutdownStage implements Comparable<ShutdownStage> {
const ShutdownStage._(this._priority);
/// The stage priority. Smaller values will be run before larger values.
final int _priority;
/// The stage before the invocation recording (if one exists) is serialized
/// to disk. Tasks performed during this stage *will* be recorded.
static const ShutdownStage STILL_RECORDING = const ShutdownStage._(1);
/// The stage during which the invocation recording (if one exists) will be
/// serialized to disk. Invocations performed after this stage will not be
/// recorded.
static const ShutdownStage SERIALIZE_RECORDING = const ShutdownStage._(2);
/// The stage during which a serialized recording will be refined (e.g.
/// cleansed for tests, zipped up for bug reporting purposes, etc.).
static const ShutdownStage POST_PROCESS_RECORDING = const ShutdownStage._(3);
/// The stage during which temporary files and directories will be deleted.
static const ShutdownStage CLEANUP = const ShutdownStage._(4);
@override
int compareTo(ShutdownStage other) => _priority.compareTo(other._priority);
}
Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{};
bool _shutdownHooksRunning = false;
/// Registers a [ShutdownHook] to be executed before the VM exits.
///
/// If [stage] is specified, the shutdown hook will be run during the specified
/// stage. By default, the shutdown hook will be run during the
/// [ShutdownStage.CLEANUP] stage.
void addShutdownHook(
ShutdownHook shutdownHook, [
ShutdownStage stage = ShutdownStage.CLEANUP,
]) {
assert(!_shutdownHooksRunning);
_shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
}
/// Runs all registered shutdown hooks and returns a future that completes when
/// all such hooks have finished.
///
/// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown
/// hooks within a given stage will be started in parallel and will be
/// guaranteed to run to completion before shutdown hooks in the next stage are
/// started.
Future<Null> runShutdownHooks() async {
_shutdownHooksRunning = true;
try {
for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
final List<ShutdownHook> hooks = _shutdownHooks.remove(stage);
final List<Future<dynamic>> futures = <Future<dynamic>>[];
for (ShutdownHook shutdownHook in hooks)
futures.add(shutdownHook());
await Future.wait<dynamic>(futures);
}
} finally {
_shutdownHooksRunning = false;
}
assert(_shutdownHooks.isEmpty);
}
Map<String, String> _environment(bool allowReentrantFlutter, [Map<String, String> environment]) {
if (allowReentrantFlutter) {
if (environment == null)
environment = <String, String>{ 'FLUTTER_ALREADY_LOCKED': 'true' };
else
environment['FLUTTER_ALREADY_LOCKED'] = 'true';
}
return environment;
}
/// This runs the command in the background from the specified working
/// directory. Completes when the process has been started.
Future<Process> runCommand(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false,
Map<String, String> environment
}) {
_traceCommand(cmd, workingDirectory: workingDirectory);
return processManager.start(
cmd,
workingDirectory: workingDirectory,
environment: _environment(allowReentrantFlutter, environment),
);
}
/// This runs the command and streams stdout/stderr from the child process to
/// this process' stdout/stderr. Completes with the process's exit code.
Future<int> runCommandAndStreamOutput(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false,
String prefix: '',
bool trace: false,
RegExp filter,
StringConverter mapFunction,
Map<String, String> environment
}) async {
final Process process = await runCommand(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment
);
final StreamSubscription<String> stdoutSubscription = process.stdout
.transform(UTF8.decoder)
.transform(const LineSplitter())
.where((String line) => filter == null ? true : filter.hasMatch(line))
.listen((String line) {
if (mapFunction != null)
line = mapFunction(line);
if (line != null) {
final String message = '$prefix$line';
if (trace)
printTrace(message);
else
printStatus(message);
}
});
final StreamSubscription<String> stderrSubscription = process.stderr
.transform(UTF8.decoder)
.transform(const LineSplitter())
.where((String line) => filter == null ? true : filter.hasMatch(line))
.listen((String line) {
if (mapFunction != null)
line = mapFunction(line);
if (line != null)
printError('$prefix$line');
});
// Wait for stdout to be fully processed
// because process.exitCode may complete first causing flaky tests.
await Future.wait(<Future<Null>>[
stdoutSubscription.asFuture<Null>(),
stderrSubscription.asFuture<Null>(),
]);
stdoutSubscription.cancel();
stderrSubscription.cancel();
return await process.exitCode;
}
Future<Null> runAndKill(List<String> cmd, Duration timeout) {
final Future<Process> proc = runDetached(cmd);
return new Future<Null>.delayed(timeout, () async {
printTrace('Intentionally killing ${cmd[0]}');
processManager.killPid((await proc).pid);
});
}
Future<Process> runDetached(List<String> cmd) {
_traceCommand(cmd);
final Future<Process> proc = processManager.start(
cmd,
mode: ProcessStartMode.DETACHED,
);
return proc;
}
Future<RunResult> runAsync(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false,
Map<String, String> environment
}) async {
_traceCommand(cmd, workingDirectory: workingDirectory);
final ProcessResult results = await processManager.run(
cmd,
workingDirectory: workingDirectory,
environment: _environment(allowReentrantFlutter, environment),
);
final RunResult runResults = new RunResult(results);
printTrace(runResults.toString());
return runResults;
}
Future<RunResult> runCheckedAsync(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false,
Map<String, String> environment
}) async {
final RunResult result = await runAsync(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment
);
if (result.exitCode != 0)
throw 'Exit code ${result.exitCode} from: ${cmd.join(' ')}';
return result;
}
bool exitsHappy(List<String> cli) {
_traceCommand(cli);
try {
return processManager.runSync(cli).exitCode == 0;
} catch (error) {
return false;
}
}
Future<bool> exitsHappyAsync(List<String> cli) async {
_traceCommand(cli);
try {
return (await processManager.run(cli)).exitCode == 0;
} catch (error) {
return false;
}
}
/// Run cmd and return stdout.
///
/// Throws an error if cmd exits with a non-zero value.
String runCheckedSync(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false,
bool hideStdout: false,
Map<String, String> environment,
}) {
return _runWithLoggingSync(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
hideStdout: hideStdout,
checked: true,
noisyErrors: true,
environment: environment,
);
}
/// Run cmd and return stdout.
String runSync(List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter: false
}) {
return _runWithLoggingSync(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter
);
}
void _traceCommand(List<String> args, { String workingDirectory }) {
final String argsText = args.join(' ');
if (workingDirectory == null)
printTrace(argsText);
else
printTrace("[$workingDirectory${fs.path.separator}] $argsText");
}
String _runWithLoggingSync(List<String> cmd, {
bool checked: false,
bool noisyErrors: false,
bool throwStandardErrorOnError: false,
String workingDirectory,
bool allowReentrantFlutter: false,
bool hideStdout: false,
Map<String, String> environment,
}) {
_traceCommand(cmd, workingDirectory: workingDirectory);
final ProcessResult results = processManager.runSync(
cmd,
workingDirectory: workingDirectory,
environment: _environment(allowReentrantFlutter, environment),
);
printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}');
if (results.stdout.isNotEmpty && !hideStdout) {
if (results.exitCode != 0 && noisyErrors)
printStatus(results.stdout.trim());
else
printTrace(results.stdout.trim());
}
if (results.exitCode != 0) {
if (results.stderr.isNotEmpty) {
if (noisyErrors)
printError(results.stderr.trim());
else
printTrace(results.stderr.trim());
}
if (throwStandardErrorOnError)
throw results.stderr.trim();
if (checked)
throw 'Exit code ${results.exitCode} from: ${cmd.join(' ')}';
}
return results.stdout.trim();
}
class ProcessExit implements Exception {
ProcessExit(this.exitCode, {this.immediate: false});
final bool immediate;
final int exitCode;
String get message => 'ProcessExit: $exitCode';
@override
String toString() => message;
}
class RunResult {
RunResult(this.processResult);
final ProcessResult processResult;
int get exitCode => processResult.exitCode;
String get stdout => processResult.stdout;
String get stderr => processResult.stderr;
@override
String toString() {
final StringBuffer out = new StringBuffer();
if (processResult.stdout.isNotEmpty)
out.writeln(processResult.stdout);
if (processResult.stderr.isNotEmpty)
out.writeln(processResult.stderr);
return out.toString().trimRight();
}
}