import 'dart:async';
import '../convert.dart';
import '../globals.dart';
import 'common.dart';
import 'file_system.dart';
import 'io.dart';
import 'process_manager.dart';
import 'utils.dart';
typedef StringConverter = String Function(String string);
typedef ShutdownHook = Future<dynamic> Function();
class ShutdownStage implements Comparable<ShutdownStage> {
const ShutdownStage._(this.priority);
final int priority;
static const ShutdownStage STILL_RECORDING = ShutdownStage._(1);
static const ShutdownStage SERIALIZE_RECORDING = ShutdownStage._(2);
static const ShutdownStage POST_PROCESS_RECORDING = ShutdownStage._(3);
static const ShutdownStage CLEANUP = ShutdownStage._(4);
@override
int compareTo(ShutdownStage other) => priority.compareTo(other.priority);
}
Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{};
bool _shutdownHooksRunning = false;
void addShutdownHook(
ShutdownHook shutdownHook, [
ShutdownStage stage = ShutdownStage.CLEANUP,
]) {
assert(!_shutdownHooksRunning);
_shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
}
Future<void> runShutdownHooks() async {
printTrace('Running shutdown hooks');
_shutdownHooksRunning = true;
try {
for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
printTrace('Shutdown hook priority ${stage.priority}');
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);
printTrace('Shutdown hooks complete');
}
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;
}
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),
);
}
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<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.where((String line) => filter == null || 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, wrap: false);
}
});
final StreamSubscription<String> stderrSubscription = process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.where((String line) => filter == null || filter.hasMatch(line))
.listen((String line) {
if (mapFunction != null)
line = mapFunction(line);
if (line != null)
printError('$prefix$line', wrap: false);
});
await waitGroup<void>(<Future<void>>[
stdoutSubscription.asFuture<void>(),
stderrSubscription.asFuture<void>(),
]);
await waitGroup<void>(<Future<void>>[
stdoutSubscription.cancel(),
stderrSubscription.cancel(),
]);
return await process.exitCode;
}
Future<int> runInteractively(
List<String> command, {
String workingDirectory,
bool allowReentrantFlutter = false,
Map<String, String> environment,
}) async {
final Process process = await runCommand(
command,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment,
);
unawaited(process.stdin.addStream(stdin));
await Future.wait<dynamic>(<Future<dynamic>>[
stdout.addStream(process.stdout),
stderr.addStream(process.stderr),
]);
return await process.exitCode;
}
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 = RunResult(results, cmd);
printTrace(runResults.toString());
return runResults;
}
typedef RunResultChecker = bool Function(int);
Future<RunResult> runCheckedAsync(
List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter = false,
Map<String, String> environment,
RunResultChecker whiteListFailures,
}) async {
final RunResult result = await runAsync(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
environment: environment,
);
if (result.exitCode != 0) {
if (whiteListFailures == null || !whiteListFailures(result.exitCode)) {
throw ProcessException(cmd[0], cmd.sublist(1),
'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode);
}
}
return result;
}
bool exitsHappy(
List<String> cli, {
Map<String, String> environment,
}) {
_traceCommand(cli);
try {
return processManager.runSync(cli, environment: environment).exitCode == 0;
} catch (error) {
printTrace('$cli failed with $error');
return false;
}
}
Future<bool> exitsHappyAsync(
List<String> cli, {
Map<String, String> environment,
}) async {
_traceCommand(cli);
try {
return (await processManager.run(cli, environment: environment)).exitCode == 0;
} catch (error) {
printTrace('$cli failed with $error');
return false;
}
}
String runCheckedSync(
List<String> cmd, {
String workingDirectory,
bool allowReentrantFlutter = false,
bool hideStdout = false,
Map<String, String> environment,
RunResultChecker whiteListFailures,
}) {
return _runWithLoggingSync(
cmd,
workingDirectory: workingDirectory,
allowReentrantFlutter: allowReentrantFlutter,
hideStdout: hideStdout,
checked: true,
noisyErrors: true,
environment: environment,
whiteListFailures: whiteListFailures
);
}
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('executing: $argsText');
} else {
printTrace('executing: [$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,
RunResultChecker whiteListFailures,
}) {
_traceCommand(cmd, workingDirectory: workingDirectory);
final ProcessResult results = processManager.runSync(
cmd,
workingDirectory: workingDirectory,
environment: _environment(allowReentrantFlutter, environment),
);
printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}');
bool failedExitCode = results.exitCode != 0;
if (whiteListFailures != null && failedExitCode) {
failedExitCode = !whiteListFailures(results.exitCode);
}
if (results.stdout.isNotEmpty && !hideStdout) {
if (failedExitCode && noisyErrors)
printStatus(results.stdout.trim());
else
printTrace(results.stdout.trim());
}
if (failedExitCode) {
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, this._command)
: assert(_command != null),
assert(_command.isNotEmpty);
final ProcessResult processResult;
final List<String> _command;
int get exitCode => processResult.exitCode;
String get stdout => processResult.stdout;
String get stderr => processResult.stderr;
@override
String toString() {
final StringBuffer out = StringBuffer();
if (processResult.stdout.isNotEmpty)
out.writeln(processResult.stdout);
if (processResult.stderr.isNotEmpty)
out.writeln(processResult.stderr);
return out.toString().trimRight();
}
void throwException(String message) {
throw ProcessException(
_command.first,
_command.skip(1).toList(),
message,
exitCode,
);
}
}