| // Copyright 2014 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:core' hide print; |
| import 'dart:io' hide exit; |
| |
| import 'package:path/path.dart' as path; |
| |
| import 'utils.dart'; |
| |
| // TODO(ianh): These two functions should be refactored into something that avoids all this code duplication. |
| |
| Stream<String> runAndGetStdout(String executable, List<String> arguments, { |
| String workingDirectory, |
| Map<String, String> environment, |
| bool expectNonZeroExit = false, |
| int expectedExitCode, |
| String failureMessage, |
| bool skip = false, |
| }) async* { |
| final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; |
| final String relativeWorkingDir = path.relative(workingDirectory); |
| if (skip) { |
| printProgress('SKIPPING', relativeWorkingDir, commandDescription); |
| return; |
| } |
| printProgress('RUNNING', relativeWorkingDir, commandDescription); |
| |
| final Stopwatch time = Stopwatch()..start(); |
| final Process process = await Process.start(executable, arguments, |
| workingDirectory: workingDirectory, |
| environment: environment, |
| ); |
| |
| stderr.addStream(process.stderr); |
| final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter()); |
| yield* lines; |
| |
| final int exitCode = await process.exitCode; |
| if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { |
| exitWithError(<String>[ |
| if (failureMessage != null) |
| failureMessage |
| else |
| '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', |
| '${bold}Command: $green$commandDescription$reset', |
| '${bold}Relative working directory: $cyan$relativeWorkingDir$reset', |
| ]); |
| } |
| print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); |
| } |
| |
| /// Runs the `executable` and waits until the process exits. |
| /// |
| /// If the process exits with a non-zero exit code, exits this process with |
| /// exit code 1, unless `expectNonZeroExit` is set to true. |
| /// |
| /// `outputListener` is called for every line of standard output from the |
| /// process, and is given the [Process] object. This can be used to interrupt |
| /// an indefinitely running process, for example, by waiting until the process |
| /// emits certain output. |
| Future<void> runCommand(String executable, List<String> arguments, { |
| String workingDirectory, |
| Map<String, String> environment, |
| bool expectNonZeroExit = false, |
| int expectedExitCode, |
| String failureMessage, |
| OutputMode outputMode = OutputMode.print, |
| CapturedOutput output, |
| bool skip = false, |
| bool Function(String) removeLine, |
| void Function(String, Process) outputListener, |
| }) async { |
| assert( |
| (outputMode == OutputMode.capture) == (output != null), |
| 'The output parameter must be non-null with and only with OutputMode.capture', |
| ); |
| |
| final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; |
| final String relativeWorkingDir = path.relative(workingDirectory ?? Directory.current.path); |
| if (skip) { |
| printProgress('SKIPPING', relativeWorkingDir, commandDescription); |
| return; |
| } |
| printProgress('RUNNING', relativeWorkingDir, commandDescription); |
| |
| final Stopwatch time = Stopwatch()..start(); |
| final Process process = await Process.start(executable, arguments, |
| workingDirectory: workingDirectory, |
| environment: environment, |
| ); |
| |
| Future<List<List<int>>> savedStdout, savedStderr; |
| final Stream<List<int>> stdoutSource = process.stdout |
| .transform<String>(const Utf8Decoder()) |
| .transform(const LineSplitter()) |
| .where((String line) => removeLine == null || !removeLine(line)) |
| .map((String line) { |
| final String formattedLine = '$line\n'; |
| if (outputListener != null) { |
| outputListener(formattedLine, process); |
| } |
| return formattedLine; |
| }) |
| .transform(const Utf8Encoder()); |
| switch (outputMode) { |
| case OutputMode.print: |
| await Future.wait<void>(<Future<void>>[ |
| stdout.addStream(stdoutSource), |
| stderr.addStream(process.stderr), |
| ]); |
| break; |
| case OutputMode.capture: |
| case OutputMode.discard: |
| savedStdout = stdoutSource.toList(); |
| savedStderr = process.stderr.toList(); |
| break; |
| } |
| |
| final int exitCode = await process.exitCode; |
| if (output != null) { |
| output.stdout = _flattenToString(await savedStdout); |
| output.stderr = _flattenToString(await savedStderr); |
| } |
| |
| if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { |
| // Print the output when we get unexpected results (unless output was |
| // printed already). |
| switch (outputMode) { |
| case OutputMode.print: |
| break; |
| case OutputMode.capture: |
| case OutputMode.discard: |
| stdout.writeln(_flattenToString(await savedStdout)); |
| stderr.writeln(_flattenToString(await savedStderr)); |
| break; |
| } |
| exitWithError(<String>[ |
| if (failureMessage != null) |
| failureMessage |
| else |
| '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', |
| '${bold}Command: $green$commandDescription$reset', |
| '${bold}Relative working directory: $cyan$relativeWorkingDir$reset', |
| ]); |
| } |
| print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); |
| } |
| |
| /// Flattens a nested list of UTF-8 code units into a single string. |
| String _flattenToString(List<List<int>> chunks) => |
| utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList()); |
| |
| /// Specifies what to do with command output from [runCommand]. |
| enum OutputMode { print, capture, discard } |
| |
| /// Stores command output from [runCommand] when used with [OutputMode.capture]. |
| class CapturedOutput { |
| String stdout; |
| String stderr; |
| } |