run_command.dart 6.04 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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';
7 8
import 'dart:core' hide print;
import 'dart:io' hide exit;
9 10 11

import 'package:path/path.dart' as path;

12
import 'utils.dart';
13

14
// TODO(ianh): These two functions should be refactored into something that avoids all this code duplication.
15

Dan Field's avatar
Dan Field committed
16 17 18 19 20 21
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
22
  bool skip = false,
Dan Field's avatar
Dan Field committed
23 24 25
}) async* {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);
26 27 28 29
  if (skip) {
    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
    return;
  }
Dan Field's avatar
Dan Field committed
30 31
  printProgress('RUNNING', relativeWorkingDir, commandDescription);

32
  final Stopwatch time = Stopwatch()..start();
Dan Field's avatar
Dan Field committed
33 34 35 36 37
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

38
  stderr.addStream(process.stderr);
Dan Field's avatar
Dan Field committed
39
  final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter());
40
  yield* lines;
41

42
  final int exitCode = await process.exitCode;
Dan Field's avatar
Dan Field committed
43
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
44 45 46 47 48 49 50 51
    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',
    ]);
Dan Field's avatar
Dan Field committed
52
  }
53
  print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
Dan Field's avatar
Dan Field committed
54 55
}

56 57 58 59 60 61 62 63 64
/// 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.
65
Future<void> runCommand(String executable, List<String> arguments, {
66 67 68 69 70
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
71 72
  OutputMode outputMode = OutputMode.print,
  CapturedOutput output,
73
  bool skip = false,
74
  bool Function(String) removeLine,
75
  void Function(String, Process) outputListener,
76
}) async {
77 78 79 80
  assert(
    (outputMode == OutputMode.capture) == (output != null),
    'The output parameter must be non-null with and only with OutputMode.capture',
  );
81

82
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
83
  final String relativeWorkingDir = path.relative(workingDirectory ?? Directory.current.path);
84 85
  if (skip) {
    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
86
    return;
87 88 89
  }
  printProgress('RUNNING', relativeWorkingDir, commandDescription);

90
  final Stopwatch time = Stopwatch()..start();
91 92 93 94 95 96
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

  Future<List<List<int>>> savedStdout, savedStderr;
97 98 99 100
  final Stream<List<int>> stdoutSource = process.stdout
    .transform<String>(const Utf8Decoder())
    .transform(const LineSplitter())
    .where((String line) => removeLine == null || !removeLine(line))
101 102 103 104 105 106 107
    .map((String line) {
      final String formattedLine = '$line\n';
      if (outputListener != null) {
        outputListener(formattedLine, process);
      }
      return formattedLine;
    })
108
    .transform(const Utf8Encoder());
109 110 111 112 113 114 115 116 117 118 119 120
  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;
121 122
  }

123
  final int exitCode = await process.exitCode;
124
  if (output != null) {
125 126
    output.stdout = _flattenToString(await savedStdout);
    output.stderr = _flattenToString(await savedStderr);
127 128
  }

129
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
130 131
    // Print the output when we get unexpected results (unless output was
    // printed already).
132 133 134 135 136
    switch (outputMode) {
      case OutputMode.print:
        break;
      case OutputMode.capture:
      case OutputMode.discard:
137 138
        stdout.writeln(_flattenToString(await savedStdout));
        stderr.writeln(_flattenToString(await savedStderr));
139
        break;
140
    }
141 142 143 144 145 146 147 148
    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',
    ]);
149
  }
150
  print('$clock ELAPSED TIME: ${prettyPrintDuration(time.elapsed)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
151
}
152 153

/// Flattens a nested list of UTF-8 code units into a single string.
154 155
String _flattenToString(List<List<int>> chunks) =>
  utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
156 157 158 159 160 161 162 163 164

/// 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;
}