
* Whitelist adb.exe heap corruption exit code. In android platform tools 29.0.0 adb.exe shell seems to be exiting with heap corruption exit code, otherwise producing results as expected. This PR whitelists this exit code on Windows. Fixes https://github.com/flutter/flutter/issues/33938. * Fix condition * Fix 'shell am start' command * Fix stop command * Refactor into runAdbMostlyChecked(Sync/Async) * runAdbMostlyChecked -> runAdbChecked
426 lines
13 KiB
Dart
426 lines
13 KiB
Dart
// 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 '../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);
|
|
|
|
/// A function that will be run before the VM exits.
|
|
typedef ShutdownHook = Future<dynamic> Function();
|
|
|
|
// TODO(ianh): We have way too many ways to run subprocesses in this project.
|
|
// Convert most of these into one or more lightweight wrappers around the
|
|
// [ProcessManager] API using named parameters for the various options.
|
|
// See [here](https://github.com/flutter/flutter/pull/14535#discussion_r167041161)
|
|
// for more details.
|
|
|
|
/// 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 = 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 = 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 = ShutdownStage._(3);
|
|
|
|
/// The stage during which temporary files and directories will be deleted.
|
|
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;
|
|
|
|
/// 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<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;
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// If [filter] is null, no lines are removed.
|
|
///
|
|
/// If [filter] is non-null, all lines that do not match it are removed. If
|
|
/// [mapFunction] is present, all lines that match [filter] are also forwarded
|
|
/// to [mapFunction] for further processing.
|
|
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 ? 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, wrap: false);
|
|
}
|
|
});
|
|
final StreamSubscription<String> stderrSubscription = process.stderr
|
|
.transform<String>(utf8.decoder)
|
|
.transform<String>(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', wrap: false);
|
|
});
|
|
|
|
// Wait for stdout to be fully processed
|
|
// because process.exitCode may complete first causing flaky tests.
|
|
await waitGroup<void>(<Future<void>>[
|
|
stdoutSubscription.asFuture<void>(),
|
|
stderrSubscription.asFuture<void>(),
|
|
]);
|
|
|
|
await waitGroup<void>(<Future<void>>[
|
|
stdoutSubscription.cancel(),
|
|
stderrSubscription.cancel(),
|
|
]);
|
|
|
|
return await process.exitCode;
|
|
}
|
|
|
|
/// Runs the [command] interactively, connecting the stdin/stdout/stderr
|
|
/// streams of this process to those of the child process. Completes with
|
|
/// the exit code of the child process.
|
|
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,
|
|
);
|
|
// The real stdin will never finish streaming. Pipe until the child process
|
|
// finishes.
|
|
unawaited(process.stdin.addStream(stdin));
|
|
// Wait for stdout and stderr to be fully processed, because process.exitCode
|
|
// may complete first.
|
|
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) {
|
|
_traceCommand(cli);
|
|
try {
|
|
return processManager.runSync(cli).exitCode == 0;
|
|
} catch (error) {
|
|
printTrace('$cli failed with $error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> exitsHappyAsync(List<String> cli) async {
|
|
_traceCommand(cli);
|
|
try {
|
|
return (await processManager.run(cli)).exitCode == 0;
|
|
} catch (error) {
|
|
printTrace('$cli failed with $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,
|
|
RunResultChecker whiteListFailures,
|
|
}) {
|
|
return _runWithLoggingSync(
|
|
cmd,
|
|
workingDirectory: workingDirectory,
|
|
allowReentrantFlutter: allowReentrantFlutter,
|
|
hideStdout: hideStdout,
|
|
checked: true,
|
|
noisyErrors: true,
|
|
environment: environment,
|
|
whiteListFailures: whiteListFailures
|
|
);
|
|
}
|
|
|
|
/// 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('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();
|
|
}
|
|
|
|
/// Throws a [ProcessException] with the given `message`.
|
|
void throwException(String message) {
|
|
throw ProcessException(
|
|
_command.first,
|
|
_command.skip(1).toList(),
|
|
message,
|
|
exitCode,
|
|
);
|
|
}
|
|
}
|