// 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 'file_system.dart'; import 'io.dart'; import 'process_manager.dart'; import '../globals.dart'; typedef String StringConverter(String string); typedef Future ShutdownHook(); // TODO(ianh): We have way too many ways to run subprocesses in this project. List _shutdownHooks = []; bool _shutdownHooksRunning = false; void addShutdownHook(ShutdownHook shutdownHook) { assert(!_shutdownHooksRunning); _shutdownHooks.add(shutdownHook); } Future runShutdownHooks() async { List hooks = new List.from(_shutdownHooks); _shutdownHooks.clear(); _shutdownHooksRunning = true; try { for (ShutdownHook shutdownHook in hooks) await shutdownHook(); } finally { _shutdownHooksRunning = false; } assert(_shutdownHooks.isEmpty); } Map _environment(bool allowReentrantFlutter, [Map environment]) { if (allowReentrantFlutter) { if (environment == null) environment = { '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 runCommand(List cmd, { String workingDirectory, bool allowReentrantFlutter: false, Map environment }) async { _traceCommand(cmd, workingDirectory: workingDirectory); String executable = cmd[0]; List arguments = cmd.length > 1 ? cmd.sublist(1) : []; Process process = await processManager.start( executable, arguments, workingDirectory: workingDirectory, environment: _environment(allowReentrantFlutter, environment) ); return process; } /// 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 runCommandAndStreamOutput(List cmd, { String workingDirectory, bool allowReentrantFlutter: false, String prefix: '', bool trace: false, RegExp filter, StringConverter mapFunction, Map environment }) async { Process process = await runCommand( cmd, workingDirectory: workingDirectory, allowReentrantFlutter: allowReentrantFlutter, environment: environment ); StreamSubscription subscription = 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) { String message = '$prefix$line'; if (trace) printTrace(message); else printStatus(message); } }); 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 subscription.asFuture(); subscription.cancel(); return await process.exitCode; } Future runAndKill(List cmd, Duration timeout) { Future proc = runDetached(cmd); return new Future.delayed(timeout, () async { printTrace('Intentionally killing ${cmd[0]}'); processManager.killPid((await proc).pid); }); } Future runDetached(List cmd) { _traceCommand(cmd); Future proc = processManager.start( cmd[0], cmd.getRange(1, cmd.length).toList(), mode: ProcessStartMode.DETACHED ); return proc; } Future runAsync(List cmd, { String workingDirectory, bool allowReentrantFlutter: false }) async { _traceCommand(cmd, workingDirectory: workingDirectory); ProcessResult results = await processManager.run( cmd[0], cmd.getRange(1, cmd.length).toList(), workingDirectory: workingDirectory, environment: _environment(allowReentrantFlutter) ); RunResult runResults = new RunResult(results); printTrace(runResults.toString()); return runResults; } bool exitsHappy(List cli) { _traceCommand(cli); try { return processManager.runSync(cli.first, cli.sublist(1)).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 cmd, { String workingDirectory, bool allowReentrantFlutter: false, bool hideStdout: false, }) { return _runWithLoggingSync( cmd, workingDirectory: workingDirectory, allowReentrantFlutter: allowReentrantFlutter, hideStdout: hideStdout, checked: true, noisyErrors: true, ); } /// Run cmd and return stdout on success. /// /// Throws the standard error output if cmd exits with a non-zero value. String runSyncAndThrowStdErrOnError(List cmd) { return _runWithLoggingSync(cmd, checked: true, throwStandardErrorOnError: true, hideStdout: true); } /// Run cmd and return stdout. String runSync(List cmd, { String workingDirectory, bool allowReentrantFlutter: false }) { return _runWithLoggingSync( cmd, workingDirectory: workingDirectory, allowReentrantFlutter: allowReentrantFlutter ); } void _traceCommand(List args, { String workingDirectory }) { String argsText = args.join(' '); if (workingDirectory == null) printTrace(argsText); else printTrace("[$workingDirectory${fs.pathSeparator}] $argsText"); } String _runWithLoggingSync(List cmd, { bool checked: false, bool noisyErrors: false, bool throwStandardErrorOnError: false, String workingDirectory, bool allowReentrantFlutter: false, bool hideStdout: false, }) { _traceCommand(cmd, workingDirectory: workingDirectory); ProcessResult results = processManager.runSync( cmd[0], cmd.getRange(1, cmd.length).toList(), workingDirectory: workingDirectory, environment: _environment(allowReentrantFlutter) ); 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); 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() { StringBuffer out = new StringBuffer(); if (processResult.stdout.isNotEmpty) out.writeln(processResult.stdout); if (processResult.stderr.isNotEmpty) out.writeln(processResult.stderr); return out.toString().trimRight(); } }