process.dart 13 KB
Newer Older
1 2 3 4 5 6
// 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';

7
import '../convert.dart';
8
import '../globals.dart';
9
import 'common.dart';
10
import 'file_system.dart';
11
import 'io.dart';
12
import 'process_manager.dart';
13
import 'utils.dart';
14

15
typedef StringConverter = String Function(String string);
16 17

/// A function that will be run before the VM exits.
18
typedef ShutdownHook = Future<dynamic> Function();
Devon Carew's avatar
Devon Carew committed
19

20
// TODO(ianh): We have way too many ways to run subprocesses in this project.
21 22 23 24
// Convert most of these into one or more lightweight wrappers around the
// [ProcessManager] API using named parameters for the various options.
// See [here](https://github.com/flutter/flutter/pull/14535#discussion_r167041161)
// for more details.
25

26 27 28 29
/// The stage in which a [ShutdownHook] will be run. All shutdown hooks within
/// a given stage will be started in parallel and will be guaranteed to run to
/// completion before shutdown hooks in the next stage are started.
class ShutdownStage implements Comparable<ShutdownStage> {
30
  const ShutdownStage._(this.priority);
31 32

  /// The stage priority. Smaller values will be run before larger values.
33
  final int priority;
34

35 36
  /// The stage before the invocation recording (if one exists) is serialized
  /// to disk. Tasks performed during this stage *will* be recorded.
37
  static const ShutdownStage STILL_RECORDING = ShutdownStage._(1);
38

39 40 41
  /// The stage during which the invocation recording (if one exists) will be
  /// serialized to disk. Invocations performed after this stage will not be
  /// recorded.
42
  static const ShutdownStage SERIALIZE_RECORDING = ShutdownStage._(2);
43 44 45

  /// The stage during which a serialized recording will be refined (e.g.
  /// cleansed for tests, zipped up for bug reporting purposes, etc.).
46
  static const ShutdownStage POST_PROCESS_RECORDING = ShutdownStage._(3);
47 48

  /// The stage during which temporary files and directories will be deleted.
49
  static const ShutdownStage CLEANUP = ShutdownStage._(4);
50 51

  @override
52
  int compareTo(ShutdownStage other) => priority.compareTo(other.priority);
53 54 55
}

Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{};
56
bool _shutdownHooksRunning = false;
57 58 59 60 61 62 63 64 65 66

/// Registers a [ShutdownHook] to be executed before the VM exits.
///
/// If [stage] is specified, the shutdown hook will be run during the specified
/// stage. By default, the shutdown hook will be run during the
/// [ShutdownStage.CLEANUP] stage.
void addShutdownHook(
  ShutdownHook shutdownHook, [
  ShutdownStage stage = ShutdownStage.CLEANUP,
]) {
67
  assert(!_shutdownHooksRunning);
68
  _shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
69 70
}

71 72 73 74 75 76 77
/// Runs all registered shutdown hooks and returns a future that completes when
/// all such hooks have finished.
///
/// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown
/// hooks within a given stage will be started in parallel and will be
/// guaranteed to run to completion before shutdown hooks in the next stage are
/// started.
78
Future<void> runShutdownHooks() async {
79
  printTrace('Running shutdown hooks');
80 81
  _shutdownHooksRunning = true;
  try {
82
    for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
83
      printTrace('Shutdown hook priority ${stage.priority}');
84 85
      final List<ShutdownHook> hooks = _shutdownHooks.remove(stage);
      final List<Future<dynamic>> futures = <Future<dynamic>>[];
86 87 88 89
      for (ShutdownHook shutdownHook in hooks)
        futures.add(shutdownHook());
      await Future.wait<dynamic>(futures);
    }
90 91 92 93
  } finally {
    _shutdownHooksRunning = false;
  }
  assert(_shutdownHooks.isEmpty);
94
  printTrace('Shutdown hooks complete');
95 96
}

97
Map<String, String> _environment(bool allowReentrantFlutter, [ Map<String, String> environment ]) {
98 99
  if (allowReentrantFlutter) {
    if (environment == null)
100
      environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'};
101 102 103 104 105
    else
      environment['FLUTTER_ALREADY_LOCKED'] = 'true';
  }

  return environment;
106 107
}

108 109
/// This runs the command in the background from the specified working
/// directory. Completes when the process has been started.
110 111
Future<Process> runCommand(
  List<String> cmd, {
112
  String workingDirectory,
113
  bool allowReentrantFlutter = false,
114
  Map<String, String> environment,
115
}) {
116
  _traceCommand(cmd, workingDirectory: workingDirectory);
117
  return processManager.start(
118
    cmd,
119
    workingDirectory: workingDirectory,
120
    environment: _environment(allowReentrantFlutter, environment),
121 122 123
  );
}

124
/// This runs the command and streams stdout/stderr from the child process to
125
/// this process' stdout/stderr. Completes with the process's exit code.
126 127 128 129 130 131
///
/// If [filter] is null, no lines are removed.
///
/// If [filter] is non-null, all lines that do not match it are removed. If
/// [mapFunction] is present, all lines that match [filter] are also forwarded
/// to [mapFunction] for further processing.
132 133
Future<int> runCommandAndStreamOutput(
  List<String> cmd, {
Devon Carew's avatar
Devon Carew committed
134
  String workingDirectory,
135 136 137
  bool allowReentrantFlutter = false,
  String prefix = '',
  bool trace = false,
138
  RegExp filter,
139
  StringConverter mapFunction,
140
  Map<String, String> environment,
141
}) async {
142
  final Process process = await runCommand(
143 144
    cmd,
    workingDirectory: workingDirectory,
145
    allowReentrantFlutter: allowReentrantFlutter,
146
    environment: environment,
147
  );
148
  final StreamSubscription<String> stdoutSubscription = process.stdout
149 150
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
151
    .where((String line) => filter == null || filter.hasMatch(line))
152
    .listen((String line) {
Devon Carew's avatar
Devon Carew committed
153 154
      if (mapFunction != null)
        line = mapFunction(line);
155
      if (line != null) {
156
        final String message = '$prefix$line';
157 158 159
        if (trace)
          printTrace(message);
        else
160
          printStatus(message, wrap: false);
161
      }
162
    });
163
  final StreamSubscription<String> stderrSubscription = process.stderr
164 165
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
166
    .where((String line) => filter == null || filter.hasMatch(line))
167
    .listen((String line) {
Devon Carew's avatar
Devon Carew committed
168 169 170
      if (mapFunction != null)
        line = mapFunction(line);
      if (line != null)
171
        printError('$prefix$line', wrap: false);
172
    });
173

174 175 176 177 178 179 180 181 182 183 184
  // Wait for stdout to be fully processed
  // because process.exitCode may complete first causing flaky tests.
  await waitGroup<void>(<Future<void>>[
    stdoutSubscription.asFuture<void>(),
    stderrSubscription.asFuture<void>(),
  ]);

  await waitGroup<void>(<Future<void>>[
    stdoutSubscription.cancel(),
    stderrSubscription.cancel(),
  ]);
185

186
  return await process.exitCode;
187
}
188

189 190 191
/// Runs the [command] interactively, connecting the stdin/stdout/stderr
/// streams of this process to those of the child process. Completes with
/// the exit code of the child process.
192 193
Future<int> runInteractively(
  List<String> command, {
194
  String workingDirectory,
195
  bool allowReentrantFlutter = false,
196
  Map<String, String> environment,
197 198 199 200 201 202 203
}) async {
  final Process process = await runCommand(
    command,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter,
    environment: environment,
  );
204 205
  // The real stdin will never finish streaming. Pipe until the child process
  // finishes.
206
  unawaited(process.stdin.addStream(stdin));
207 208
  // Wait for stdout and stderr to be fully processed, because process.exitCode
  // may complete first.
209
  await Future.wait<dynamic>(<Future<dynamic>>[
210 211 212 213 214 215
    stdout.addStream(process.stdout),
    stderr.addStream(process.stderr),
  ]);
  return await process.exitCode;
}

Hixie's avatar
Hixie committed
216
Future<Process> runDetached(List<String> cmd) {
217
  _traceCommand(cmd);
218
  final Future<Process> proc = processManager.start(
219
    cmd,
220
    mode: ProcessStartMode.detached,
Devon Carew's avatar
Devon Carew committed
221
  );
222
  return proc;
223
}
224

225 226
Future<RunResult> runAsync(
  List<String> cmd, {
227
  String workingDirectory,
228
  bool allowReentrantFlutter = false,
229
  Map<String, String> environment,
230
}) async {
231
  _traceCommand(cmd, workingDirectory: workingDirectory);
232
  final ProcessResult results = await processManager.run(
233
    cmd,
234
    workingDirectory: workingDirectory,
235
    environment: _environment(allowReentrantFlutter, environment),
236
  );
237
  final RunResult runResults = RunResult(results, cmd);
238 239 240 241
  printTrace(runResults.toString());
  return runResults;
}

242 243
typedef RunResultChecker = bool Function(int);

244 245
Future<RunResult> runCheckedAsync(
  List<String> cmd, {
246
  String workingDirectory,
247
  bool allowReentrantFlutter = false,
248
  Map<String, String> environment,
249
  RunResultChecker whiteListFailures,
250 251
}) async {
  final RunResult result = await runAsync(
252 253 254 255
    cmd,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter,
    environment: environment,
256
  );
257
  if (result.exitCode != 0) {
258 259 260 261
    if (whiteListFailures == null || !whiteListFailures(result.exitCode)) {
      throw ProcessException(cmd[0], cmd.sublist(1),
          'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode);
    }
262
  }
263 264 265
  return result;
}

266 267 268 269
bool exitsHappy(
  List<String> cli, {
  Map<String, String> environment,
}) {
270
  _traceCommand(cli);
271
  try {
272
    return processManager.runSync(cli, environment: environment).exitCode == 0;
273
  } catch (error) {
274
    printTrace('$cli failed with $error');
275 276 277 278
    return false;
  }
}

279 280 281 282
Future<bool> exitsHappyAsync(
  List<String> cli, {
  Map<String, String> environment,
}) async {
283 284
  _traceCommand(cli);
  try {
285
    return (await processManager.run(cli, environment: environment)).exitCode == 0;
286
  } catch (error) {
287
    printTrace('$cli failed with $error');
288 289 290 291
    return false;
  }
}

292 293 294
/// Run cmd and return stdout.
///
/// Throws an error if cmd exits with a non-zero value.
295 296
String runCheckedSync(
  List<String> cmd, {
297
  String workingDirectory,
298 299
  bool allowReentrantFlutter = false,
  bool hideStdout = false,
300
  Map<String, String> environment,
301
  RunResultChecker whiteListFailures,
302 303 304 305 306
}) {
  return _runWithLoggingSync(
    cmd,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter,
307
    hideStdout: hideStdout,
308
    checked: true,
309
    noisyErrors: true,
310
    environment: environment,
311
    whiteListFailures: whiteListFailures
312 313 314 315
  );
}

/// Run cmd and return stdout.
316 317
String runSync(
  List<String> cmd, {
318
  String workingDirectory,
319
  bool allowReentrantFlutter = false,
320 321 322 323
}) {
  return _runWithLoggingSync(
    cmd,
    workingDirectory: workingDirectory,
324
    allowReentrantFlutter: allowReentrantFlutter,
325 326 327
  );
}

328
void _traceCommand(List<String> args, { String workingDirectory }) {
329
  final String argsText = args.join(' ');
330 331 332 333 334
  if (workingDirectory == null) {
    printTrace('executing: $argsText');
  } else {
    printTrace('executing: [$workingDirectory${fs.path.separator}] $argsText');
  }
335 336
}

337 338
String _runWithLoggingSync(
  List<String> cmd, {
339 340 341
  bool checked = false,
  bool noisyErrors = false,
  bool throwStandardErrorOnError = false,
Devon Carew's avatar
Devon Carew committed
342
  String workingDirectory,
343 344
  bool allowReentrantFlutter = false,
  bool hideStdout = false,
345
  Map<String, String> environment,
346
  RunResultChecker whiteListFailures,
347
}) {
348
  _traceCommand(cmd, workingDirectory: workingDirectory);
349
  final ProcessResult results = processManager.runSync(
350
    cmd,
351
    workingDirectory: workingDirectory,
352
    environment: _environment(allowReentrantFlutter, environment),
353
  );
Devon Carew's avatar
Devon Carew committed
354 355 356

  printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}');

357 358 359 360 361
  bool failedExitCode = results.exitCode != 0;
  if (whiteListFailures != null && failedExitCode) {
    failedExitCode = !whiteListFailures(results.exitCode);
  }

362
  if (results.stdout.isNotEmpty && !hideStdout) {
363
    if (failedExitCode && noisyErrors)
Devon Carew's avatar
Devon Carew committed
364 365 366 367 368
      printStatus(results.stdout.trim());
    else
      printTrace(results.stdout.trim());
  }

369
  if (failedExitCode) {
Devon Carew's avatar
Devon Carew committed
370 371
    if (results.stderr.isNotEmpty) {
      if (noisyErrors)
Devon Carew's avatar
Devon Carew committed
372
        printError(results.stderr.trim());
Devon Carew's avatar
Devon Carew committed
373 374
      else
        printTrace(results.stderr.trim());
Devon Carew's avatar
Devon Carew committed
375
    }
Devon Carew's avatar
Devon Carew committed
376

377 378 379
    if (throwStandardErrorOnError)
      throw results.stderr.trim();

380
    if (checked)
Devon Carew's avatar
Devon Carew committed
381
      throw 'Exit code ${results.exitCode} from: ${cmd.join(' ')}';
382
  }
Devon Carew's avatar
Devon Carew committed
383

384
  return results.stdout.trim();
385
}
386 387

class ProcessExit implements Exception {
388
  ProcessExit(this.exitCode, {this.immediate = false});
389

390
  final bool immediate;
391 392
  final int exitCode;

Hixie's avatar
Hixie committed
393
  String get message => 'ProcessExit: $exitCode';
394 395

  @override
396 397
  String toString() => message;
}
398 399

class RunResult {
400 401 402
  RunResult(this.processResult, this._command)
    : assert(_command != null),
      assert(_command.isNotEmpty);
403 404 405

  final ProcessResult processResult;

406 407
  final List<String> _command;

408
  int get exitCode => processResult.exitCode;
409 410
  String get stdout => processResult.stdout;
  String get stderr => processResult.stderr;
411 412 413

  @override
  String toString() {
414
    final StringBuffer out = StringBuffer();
415 416 417 418 419 420
    if (processResult.stdout.isNotEmpty)
      out.writeln(processResult.stdout);
    if (processResult.stderr.isNotEmpty)
      out.writeln(processResult.stderr);
    return out.toString().trimRight();
  }
421

422 423
  /// Throws a [ProcessException] with the given `message`.
  void throwException(String message) {
424
    throw ProcessException(
425 426 427 428 429 430
      _command.first,
      _command.skip(1).toList(),
      message,
      exitCode,
    );
  }
431
}