run_command.dart 5.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Copyright 2017 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 'dart:io';

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

final bool hasColor = stdout.supportsAnsiEscapes;

final String bold = hasColor ? '\x1B[1m' : '';
final String red = hasColor ? '\x1B[31m' : '';
final String green = hasColor ? '\x1B[32m' : '';
final String yellow = hasColor ? '\x1B[33m' : '';
final String cyan = hasColor ? '\x1B[36m' : '';
final String reset = hasColor ? '\x1B[0m' : '';
final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
const String arrow = '⏩';
const String clock = '🕐';

const Duration _kLongTimeout = Duration(minutes: 45);

String elapsedTime(DateTime start) {
26
  return DateTime.now().difference(start).toString();
27 28 29 30 31 32
}

void printProgress(String action, String workingDir, String command) {
  print('$arrow $action: cd $cyan$workingDir$reset; $yellow$command$reset');
}

Dan Field's avatar
Dan Field committed
33 34 35 36 37 38 39
Stream<String> runAndGetStdout(String executable, List<String> arguments, {
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
  Duration timeout = _kLongTimeout,
40
  Function beforeExit,
Dan Field's avatar
Dan Field committed
41 42 43 44 45 46 47 48 49 50 51 52
}) async* {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);

  printProgress('RUNNING', relativeWorkingDir, commandDescription);

  final DateTime start = DateTime.now();
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

53
  stderr.addStream(process.stderr);
Dan Field's avatar
Dan Field committed
54 55 56 57
  final Stream<String> lines = process.stdout.transform(utf8.decoder).transform(const LineSplitter());
  await for (String line in lines) {
    yield line;
  }
58

Dan Field's avatar
Dan Field committed
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  final int exitCode = await process.exitCode.timeout(timeout, onTimeout: () {
    stderr.writeln('Process timed out after $timeout');
    return expectNonZeroExit ? 0 : 1;
  });
  print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: ');
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
    if (failureMessage != null) {
      print(failureMessage);
    }
    print(
        '$redLine\n'
            '${bold}ERROR:$red Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n'
            '${bold}Command:$cyan $commandDescription$reset\n'
            '${bold}Relative working directory:$red $relativeWorkingDir$reset\n'
            '$redLine'
    );
75
    beforeExit?.call();
Dan Field's avatar
Dan Field committed
76 77 78 79
    exit(1);
  }
}

80
Future<void> runCommand(String executable, List<String> arguments, {
81 82 83 84 85 86 87 88 89 90 91 92 93
  String workingDirectory,
  Map<String, String> environment,
  bool expectNonZeroExit = false,
  int expectedExitCode,
  String failureMessage,
  bool printOutput = true,
  bool skip = false,
  Duration timeout = _kLongTimeout,
}) async {
  final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
  final String relativeWorkingDir = path.relative(workingDirectory);
  if (skip) {
    printProgress('SKIPPING', relativeWorkingDir, commandDescription);
94
    return;
95 96 97
  }
  printProgress('RUNNING', relativeWorkingDir, commandDescription);

98
  final DateTime start = DateTime.now();
99 100 101 102 103 104 105
  final Process process = await Process.start(executable, arguments,
    workingDirectory: workingDirectory,
    environment: environment,
  );

  Future<List<List<int>>> savedStdout, savedStderr;
  if (printOutput) {
106
    await Future.wait<void>(<Future<void>>[
107
      stdout.addStream(process.stdout),
108
      stderr.addStream(process.stderr),
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    ]);
  } else {
    savedStdout = process.stdout.toList();
    savedStderr = process.stderr.toList();
  }

  final int exitCode = await process.exitCode.timeout(timeout, onTimeout: () {
    stderr.writeln('Process timed out after $timeout');
    return expectNonZeroExit ? 0 : 1;
  });
  print('$clock ELAPSED TIME: $bold${elapsedTime(start)}$reset for $commandDescription in $relativeWorkingDir: ');
  if ((exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && exitCode != expectedExitCode)) {
    if (failureMessage != null) {
      print(failureMessage);
    }
    if (!printOutput) {
125 126
      stdout.writeln(utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()));
      stderr.writeln(utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()));
127 128 129 130 131 132 133 134 135 136 137
    }
    print(
        '$redLine\n'
            '${bold}ERROR:$red Last command exited with $exitCode (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset\n'
            '${bold}Command:$cyan $commandDescription$reset\n'
            '${bold}Relative working directory:$red $relativeWorkingDir$reset\n'
            '$redLine'
    );
    exit(1);
  }
}