Ian Hickson | 449f4a6 | 2019-11-27 15:04:02 -0800 | [diff] [blame] | 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'dart:async'; |
| 6 | import 'dart:convert'; |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 7 | import 'dart:core' hide print; |
| 8 | import 'dart:io' hide exit; |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 9 | |
| 10 | import 'package:path/path.dart' as path; |
| 11 | |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 12 | import 'utils.dart'; |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 13 | |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 14 | // TODO(ianh): These two functions should be refactored into something that avoids all this code duplication. |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 15 | |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 16 | Stream<String> runAndGetStdout(String executable, List<String> arguments, { |
| 17 | String workingDirectory, |
| 18 | Map<String, String> environment, |
| 19 | bool expectNonZeroExit = false, |
| 20 | int expectedExitCode, |
| 21 | String failureMessage, |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 22 | bool skip = false, |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 23 | }) async* { |
| 24 | final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; |
| 25 | final String relativeWorkingDir = path.relative(workingDirectory); |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 26 | if (skip) { |
| 27 | printProgress('SKIPPING', relativeWorkingDir, commandDescription); |
| 28 | return; |
| 29 | } |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 30 | printProgress('RUNNING', relativeWorkingDir, commandDescription); |
| 31 | |
Ian Hickson | e0a31de | 2019-08-20 14:53:39 -0700 | [diff] [blame] | 32 | final Stopwatch time = Stopwatch()..start(); |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 33 | final Process process = await Process.start(executable, arguments, |
| 34 | workingDirectory: workingDirectory, |
| 35 | environment: environment, |
| 36 | ); |
| 37 | |
Dan Field | b9f013c | 2019-03-10 11:26:17 -0700 | [diff] [blame] | 38 | stderr.addStream(process.stderr); |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 39 | final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter()); |
Alexandre Ardhuin | 1c79347 | 2020-01-08 07:34:36 +0100 | [diff] [blame] | 40 | yield* lines; |
Dan Field | dcc965a | 2019-03-13 12:58:10 -0700 | [diff] [blame] | 41 | |
Ian Hickson | e0a31de | 2019-08-20 14:53:39 -0700 | [diff] [blame] | 42 | final int exitCode = await process.exitCode; |
Francisco Magdaleno | 04ea318 | 2020-01-02 09:25:59 -0800 | [diff] [blame] | 43 | if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 44 | exitWithError(<String>[ |
| 45 | if (failureMessage != null) |
| 46 | failureMessage |
| 47 | else |
| 48 | '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', |
| 49 | '${bold}Command: $green$commandDescription$reset', |
| 50 | '${bold}Relative working directory: $cyan$relativeWorkingDir$reset', |
| 51 | ]); |
Francisco Magdaleno | 04ea318 | 2020-01-02 09:25:59 -0800 | [diff] [blame] | 52 | } |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 53 | print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); |
Dan Field | 20e0f13 | 2019-03-06 13:13:45 -0800 | [diff] [blame] | 54 | } |
| 55 | |
Yegor | b340469 | 2020-02-13 18:34:08 -0800 | [diff] [blame] | 56 | /// Runs the `executable` and waits until the process exits. |
| 57 | /// |
| 58 | /// If the process exits with a non-zero exit code, exits this process with |
| 59 | /// exit code 1, unless `expectNonZeroExit` is set to true. |
| 60 | /// |
| 61 | /// `outputListener` is called for every line of standard output from the |
| 62 | /// process, and is given the [Process] object. This can be used to interrupt |
| 63 | /// an indefinitely running process, for example, by waiting until the process |
| 64 | /// emits certain output. |
Alexandre Ardhuin | d340e2f | 2018-10-04 18:44:23 +0200 | [diff] [blame] | 65 | Future<void> runCommand(String executable, List<String> arguments, { |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 66 | String workingDirectory, |
| 67 | Map<String, String> environment, |
| 68 | bool expectNonZeroExit = false, |
| 69 | int expectedExitCode, |
| 70 | String failureMessage, |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 71 | OutputMode outputMode = OutputMode.print, |
| 72 | CapturedOutput output, |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 73 | bool skip = false, |
Jonah Williams | 2b20345 | 2019-07-10 08:48:01 -0700 | [diff] [blame] | 74 | bool Function(String) removeLine, |
Yegor | b340469 | 2020-02-13 18:34:08 -0800 | [diff] [blame] | 75 | void Function(String, Process) outputListener, |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 76 | }) async { |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 77 | assert( |
| 78 | (outputMode == OutputMode.capture) == (output != null), |
| 79 | 'The output parameter must be non-null with and only with OutputMode.capture', |
| 80 | ); |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 81 | |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 82 | final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; |
| 83 | final String relativeWorkingDir = path.relative(workingDirectory); |
| 84 | if (skip) { |
| 85 | printProgress('SKIPPING', relativeWorkingDir, commandDescription); |
Alexandre Ardhuin | 8b0de38 | 2018-10-17 11:01:37 +0200 | [diff] [blame] | 86 | return; |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 87 | } |
| 88 | printProgress('RUNNING', relativeWorkingDir, commandDescription); |
| 89 | |
Ian Hickson | e0a31de | 2019-08-20 14:53:39 -0700 | [diff] [blame] | 90 | final Stopwatch time = Stopwatch()..start(); |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 91 | final Process process = await Process.start(executable, arguments, |
| 92 | workingDirectory: workingDirectory, |
| 93 | environment: environment, |
| 94 | ); |
| 95 | |
| 96 | Future<List<List<int>>> savedStdout, savedStderr; |
Jonah Williams | 2b20345 | 2019-07-10 08:48:01 -0700 | [diff] [blame] | 97 | final Stream<List<int>> stdoutSource = process.stdout |
| 98 | .transform<String>(const Utf8Decoder()) |
| 99 | .transform(const LineSplitter()) |
| 100 | .where((String line) => removeLine == null || !removeLine(line)) |
Yegor | b340469 | 2020-02-13 18:34:08 -0800 | [diff] [blame] | 101 | .map((String line) { |
| 102 | final String formattedLine = '$line\n'; |
| 103 | if (outputListener != null) { |
| 104 | outputListener(formattedLine, process); |
| 105 | } |
| 106 | return formattedLine; |
| 107 | }) |
Jonah Williams | 2b20345 | 2019-07-10 08:48:01 -0700 | [diff] [blame] | 108 | .transform(const Utf8Encoder()); |
James Lin | e3ffa76 | 2019-08-09 17:01:07 -0700 | [diff] [blame] | 109 | switch (outputMode) { |
| 110 | case OutputMode.print: |
| 111 | await Future.wait<void>(<Future<void>>[ |
| 112 | stdout.addStream(stdoutSource), |
| 113 | stderr.addStream(process.stderr), |
| 114 | ]); |
| 115 | break; |
| 116 | case OutputMode.capture: |
| 117 | case OutputMode.discard: |
| 118 | savedStdout = stdoutSource.toList(); |
| 119 | savedStderr = process.stderr.toList(); |
| 120 | break; |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 121 | } |
| 122 | |
Ian Hickson | e0a31de | 2019-08-20 14:53:39 -0700 | [diff] [blame] | 123 | final int exitCode = await process.exitCode; |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 124 | if (output != null) { |
James Lin | 9823b3d | 2019-08-15 11:12:08 -0700 | [diff] [blame] | 125 | output.stdout = _flattenToString(await savedStdout); |
| 126 | output.stderr = _flattenToString(await savedStderr); |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 127 | } |
| 128 | |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 129 | if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) { |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 130 | // Print the output when we get unexpected results (unless output was |
| 131 | // printed already). |
James Lin | e3ffa76 | 2019-08-09 17:01:07 -0700 | [diff] [blame] | 132 | switch (outputMode) { |
| 133 | case OutputMode.print: |
| 134 | break; |
| 135 | case OutputMode.capture: |
| 136 | case OutputMode.discard: |
James Lin | 9823b3d | 2019-08-15 11:12:08 -0700 | [diff] [blame] | 137 | stdout.writeln(_flattenToString(await savedStdout)); |
| 138 | stderr.writeln(_flattenToString(await savedStderr)); |
James Lin | e3ffa76 | 2019-08-09 17:01:07 -0700 | [diff] [blame] | 139 | break; |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 140 | } |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 141 | exitWithError(<String>[ |
| 142 | if (failureMessage != null) |
| 143 | failureMessage |
| 144 | else |
| 145 | '${bold}ERROR: ${red}Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', |
| 146 | '${bold}Command: $green$commandDescription$reset', |
| 147 | '${bold}Relative working directory: $cyan$relativeWorkingDir$reset', |
| 148 | ]); |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 149 | } |
Dan Field | 24f39d4 | 2020-01-02 11:47:28 -0800 | [diff] [blame] | 150 | print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); |
Alexander Aprelev | 391e91c | 2018-08-30 07:30:25 -0700 | [diff] [blame] | 151 | } |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 152 | |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 153 | /// Flattens a nested list of UTF-8 code units into a single string. |
James Lin | 9823b3d | 2019-08-15 11:12:08 -0700 | [diff] [blame] | 154 | String _flattenToString(List<List<int>> chunks) => |
| 155 | utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList()); |
James Lin | c02b805 | 2019-08-09 15:10:45 -0700 | [diff] [blame] | 156 | |
| 157 | /// Specifies what to do with command output from [runCommand]. |
| 158 | enum OutputMode { print, capture, discard } |
| 159 | |
| 160 | /// Stores command output from [runCommand] when used with [OutputMode.capture]. |
| 161 | class CapturedOutput { |
| 162 | String stdout; |
| 163 | String stderr; |
| 164 | } |