process.dart 19.2 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';

7 8 9
import 'package:meta/meta.dart';
import 'package:process/process.dart';

10
import '../convert.dart';
11
import 'common.dart';
12
import 'context.dart';
13
import 'io.dart';
14
import 'logger.dart';
15
import 'utils.dart';
16

17
typedef StringConverter = String Function(String string);
18 19

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

22
// TODO(ianh): We have way too many ways to run subprocesses in this project.
23 24 25 26
// 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.
27

28 29 30 31
/// 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> {
32
  const ShutdownStage._(this.priority);
33 34

  /// The stage priority. Smaller values will be run before larger values.
35
  final int priority;
36

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

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

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

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

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

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
ShutdownHooks get shutdownHooks => ShutdownHooks.instance;

abstract class ShutdownHooks {
  factory ShutdownHooks({
    @required Logger logger,
  }) => _DefaultShutdownHooks(
    logger: logger,
  );

  static ShutdownHooks get instance => context.get<ShutdownHooks>();

  /// 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,
  ]);

  /// 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.
  Future<void> runShutdownHooks();
86 87
}

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
class _DefaultShutdownHooks implements ShutdownHooks {
  _DefaultShutdownHooks({
    @required Logger logger,
  }) : _logger = logger;

  final Logger _logger;

  final Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{};

  bool _shutdownHooksRunning = false;

  @override
  void addShutdownHook(
    ShutdownHook shutdownHook, [
    ShutdownStage stage = ShutdownStage.CLEANUP,
  ]) {
    assert(!_shutdownHooksRunning);
    _shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
  }

  @override
  Future<void> runShutdownHooks() async {
    _logger.printTrace('Running shutdown hooks');
    _shutdownHooksRunning = true;
    try {
      for (final ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
        _logger.printTrace('Shutdown hook priority ${stage.priority}');
        final List<ShutdownHook> hooks = _shutdownHooks.remove(stage);
        final List<Future<dynamic>> futures = <Future<dynamic>>[];
        for (final ShutdownHook shutdownHook in hooks) {
          final FutureOr<dynamic> result = shutdownHook();
          if (result is Future<dynamic>) {
            futures.add(result);
          }
122
        }
123
        await Future.wait<dynamic>(futures);
124
      }
125 126
    } finally {
      _shutdownHooksRunning = false;
127
    }
128 129
    assert(_shutdownHooks.isEmpty);
    _logger.printTrace('Shutdown hooks complete');
130
  }
131 132
}

133
class ProcessExit implements Exception {
134
  ProcessExit(this.exitCode, {this.immediate = false});
135

136
  final bool immediate;
137 138
  final int exitCode;

Hixie's avatar
Hixie committed
139
  String get message => 'ProcessExit: $exitCode';
140 141

  @override
142 143
  String toString() => message;
}
144 145

class RunResult {
146 147 148
  RunResult(this.processResult, this._command)
    : assert(_command != null),
      assert(_command.isNotEmpty);
149 150 151

  final ProcessResult processResult;

152 153
  final List<String> _command;

154
  int get exitCode => processResult.exitCode;
155 156
  String get stdout => processResult.stdout as String;
  String get stderr => processResult.stderr as String;
157 158 159

  @override
  String toString() {
160
    final StringBuffer out = StringBuffer();
161 162
    if (stdout.isNotEmpty) {
      out.writeln(stdout);
163
    }
164 165
    if (stderr.isNotEmpty) {
      out.writeln(stderr);
166
    }
167 168
    return out.toString().trimRight();
  }
169

170 171
  /// Throws a [ProcessException] with the given `message`.
  void throwException(String message) {
172
    throw ProcessException(
173 174 175 176 177 178
      _command.first,
      _command.skip(1).toList(),
      message,
      exitCode,
    );
  }
179
}
180 181 182 183

typedef RunResultChecker = bool Function(int);

abstract class ProcessUtils {
184 185 186 187 188 189 190
  factory ProcessUtils({
    @required ProcessManager processManager,
    @required Logger logger,
  }) => _DefaultProcessUtils(
    processManager: processManager,
    logger: logger,
  );
191 192 193 194 195 196

  /// Spawns a child process to run the command [cmd].
  ///
  /// When [throwOnError] is `true`, if the child process finishes with a non-zero
  /// exit code, a [ProcessException] is thrown.
  ///
197
  /// If [throwOnError] is `true`, and [allowedFailures] is supplied,
198
  /// a [ProcessException] is only thrown on a non-zero exit code if
199
  /// [allowedFailures] returns false when passed the exit code.
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
  ///
  /// When [workingDirectory] is set, it is the working directory of the child
  /// process.
  ///
  /// When [allowReentrantFlutter] is set to `true`, the child process is
  /// permitted to call the Flutter tool. By default it is not.
  ///
  /// When [environment] is supplied, it is used as the environment for the child
  /// process.
  ///
  /// When [timeout] is supplied, [runAsync] will kill the child process and
  /// throw a [ProcessException] when it doesn't finish in time.
  ///
  /// If [timeout] is supplied, the command will be retried [timeoutRetries] times
  /// if it times out.
  Future<RunResult> run(
    List<String> cmd, {
    bool throwOnError = false,
218
    RunResultChecker allowedFailures,
219 220 221 222 223 224 225 226 227 228 229
    String workingDirectory,
    bool allowReentrantFlutter = false,
    Map<String, String> environment,
    Duration timeout,
    int timeoutRetries = 0,
  });

  /// Run the command and block waiting for its result.
  RunResult runSync(
    List<String> cmd, {
    bool throwOnError = false,
230
    bool verboseExceptions = false,
231
    RunResultChecker allowedFailures,
232 233 234 235
    bool hideStdout = false,
    String workingDirectory,
    Map<String, String> environment,
    bool allowReentrantFlutter = false,
236
    Encoding encoding = systemEncoding,
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
  });

  /// This runs the command in the background from the specified working
  /// directory. Completes when the process has been started.
  Future<Process> start(
    List<String> cmd, {
    String workingDirectory,
    bool allowReentrantFlutter = false,
    Map<String, String> environment,
  });

  /// This runs the command and streams stdout/stderr from the child process to
  /// this process' stdout/stderr. Completes with the process's exit code.
  ///
  /// 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.
256 257 258
  ///
  /// If [stdoutErrorMatcher] is non-null, matching lines from stdout will be
  /// treated as errors, just as if they had been logged to stderr instead.
259 260 261 262 263 264 265
  Future<int> stream(
    List<String> cmd, {
    String workingDirectory,
    bool allowReentrantFlutter = false,
    String prefix = '',
    bool trace = false,
    RegExp filter,
266
    RegExp stdoutErrorMatcher,
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    StringConverter mapFunction,
    Map<String, String> environment,
  });

  bool exitsHappySync(
    List<String> cli, {
    Map<String, String> environment,
  });

  Future<bool> exitsHappy(
    List<String> cli, {
    Map<String, String> environment,
  });
}

class _DefaultProcessUtils implements ProcessUtils {
283 284 285 286 287 288 289 290 291 292
  _DefaultProcessUtils({
    @required ProcessManager processManager,
    @required Logger logger,
  }) : _processManager = processManager,
      _logger = logger;

  final ProcessManager _processManager;

  final Logger _logger;

293 294 295 296
  @override
  Future<RunResult> run(
    List<String> cmd, {
    bool throwOnError = false,
297
    RunResultChecker allowedFailures,
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
    String workingDirectory,
    bool allowReentrantFlutter = false,
    Map<String, String> environment,
    Duration timeout,
    int timeoutRetries = 0,
  }) async {
    if (cmd == null || cmd.isEmpty) {
      throw ArgumentError('cmd must be a non-empty list');
    }
    if (timeoutRetries < 0) {
      throw ArgumentError('timeoutRetries must be non-negative');
    }
    _traceCommand(cmd, workingDirectory: workingDirectory);

    // When there is no timeout, there's no need to kill a running process, so
313
    // we can just use _processManager.run().
314
    if (timeout == null) {
315
      final ProcessResult results = await _processManager.run(
316 317 318 319 320
        cmd,
        workingDirectory: workingDirectory,
        environment: _environment(allowReentrantFlutter, environment),
      );
      final RunResult runResult = RunResult(results, cmd);
321
      _logger.printTrace(runResult.toString());
322
      if (throwOnError && runResult.exitCode != 0 &&
323
          (allowedFailures == null || !allowedFailures(runResult.exitCode))) {
324 325 326 327 328 329
        runResult.throwException('Process exited abnormally:\n$runResult');
      }
      return runResult;
    }

    // When there is a timeout, we have to kill the running process, so we have
330
    // to use _processManager.start() through _runCommand() above.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    while (true) {
      assert(timeoutRetries >= 0);
      timeoutRetries = timeoutRetries - 1;

      final Process process = await start(
          cmd,
          workingDirectory: workingDirectory,
          allowReentrantFlutter: allowReentrantFlutter,
          environment: environment,
      );

      final StringBuffer stdoutBuffer = StringBuffer();
      final StringBuffer stderrBuffer = StringBuffer();
      final Future<void> stdoutFuture = process.stdout
          .transform<String>(const Utf8Decoder(reportErrors: false))
          .listen(stdoutBuffer.write)
          .asFuture<void>(null);
      final Future<void> stderrFuture = process.stderr
          .transform<String>(const Utf8Decoder(reportErrors: false))
          .listen(stderrBuffer.write)
          .asFuture<void>(null);

      int exitCode;
      exitCode = await process.exitCode.timeout(timeout, onTimeout: () {
        // The process timed out. Kill it.
356
        _processManager.killPid(process.pid);
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
        return null;
      });

      String stdoutString;
      String stderrString;
      try {
        Future<void> stdioFuture =
            Future.wait<void>(<Future<void>>[stdoutFuture, stderrFuture]);
        if (exitCode == null) {
          // If we had to kill the process for a timeout, only wait a short time
          // for the stdio streams to drain in case killing the process didn't
          // work.
          stdioFuture = stdioFuture.timeout(const Duration(seconds: 1));
        }
        await stdioFuture;
372
      } on Exception catch (_) {
373 374 375 376 377 378 379 380 381 382 383 384
        // Ignore errors on the process' stdout and stderr streams. Just capture
        // whatever we got, and use the exit code
      }
      stdoutString = stdoutBuffer.toString();
      stderrString = stderrBuffer.toString();

      final ProcessResult result = ProcessResult(
          process.pid, exitCode ?? -1, stdoutString, stderrString);
      final RunResult runResult = RunResult(result, cmd);

      // If the process did not timeout. We are done.
      if (exitCode != null) {
385
        _logger.printTrace(runResult.toString());
386
        if (throwOnError && runResult.exitCode != 0 &&
387
            (allowedFailures == null || !allowedFailures(exitCode))) {
388 389 390 391 392 393 394 395 396 397 398
          runResult.throwException('Process exited abnormally:\n$runResult');
        }
        return runResult;
      }

      // If we are out of timeoutRetries, throw a ProcessException.
      if (timeoutRetries < 0) {
        runResult.throwException('Process timed out:\n$runResult');
      }

      // Log the timeout with a trace message in verbose mode.
399 400 401 402
      _logger.printTrace(
        'Process "${cmd[0]}" timed out. $timeoutRetries attempts left:\n'
        '$runResult',
      );
403 404 405 406 407 408 409 410 411
    }

    // Unreachable.
  }

  @override
  RunResult runSync(
    List<String> cmd, {
    bool throwOnError = false,
412
    bool verboseExceptions = false,
413
    RunResultChecker allowedFailures,
414 415 416 417
    bool hideStdout = false,
    String workingDirectory,
    Map<String, String> environment,
    bool allowReentrantFlutter = false,
418
    Encoding encoding = systemEncoding,
419 420
  }) {
    _traceCommand(cmd, workingDirectory: workingDirectory);
421
    final ProcessResult results = _processManager.runSync(
422 423 424
      cmd,
      workingDirectory: workingDirectory,
      environment: _environment(allowReentrantFlutter, environment),
425 426
      stderrEncoding: encoding,
      stdoutEncoding: encoding,
427 428 429
    );
    final RunResult runResult = RunResult(results, cmd);

430
    _logger.printTrace('Exit code ${runResult.exitCode} from: ${cmd.join(' ')}');
431 432

    bool failedExitCode = runResult.exitCode != 0;
433 434
    if (allowedFailures != null && failedExitCode) {
      failedExitCode = !allowedFailures(runResult.exitCode);
435 436 437 438
    }

    if (runResult.stdout.isNotEmpty && !hideStdout) {
      if (failedExitCode && throwOnError) {
439
        _logger.printStatus(runResult.stdout.trim());
440
      } else {
441
        _logger.printTrace(runResult.stdout.trim());
442 443 444 445 446
      }
    }

    if (runResult.stderr.isNotEmpty) {
      if (failedExitCode && throwOnError) {
447
        _logger.printError(runResult.stderr.trim());
448
      } else {
449
        _logger.printTrace(runResult.stderr.trim());
450 451 452 453
      }
    }

    if (failedExitCode && throwOnError) {
454 455 456 457 458 459
      String message = 'The command failed';
      if (verboseExceptions) {
        message = 'The command failed\nStdout:\n${runResult.stdout}\n'
            'Stderr:\n${runResult.stderr}';
      }
      runResult.throwException(message);
460 461 462 463 464 465 466 467 468 469 470 471 472
    }

    return runResult;
  }

  @override
  Future<Process> start(
    List<String> cmd, {
    String workingDirectory,
    bool allowReentrantFlutter = false,
    Map<String, String> environment,
  }) {
    _traceCommand(cmd, workingDirectory: workingDirectory);
473
    return _processManager.start(
474 475 476 477 478 479 480 481 482 483 484 485 486 487
      cmd,
      workingDirectory: workingDirectory,
      environment: _environment(allowReentrantFlutter, environment),
    );
  }

  @override
  Future<int> stream(
    List<String> cmd, {
    String workingDirectory,
    bool allowReentrantFlutter = false,
    String prefix = '',
    bool trace = false,
    RegExp filter,
488
    RegExp stdoutErrorMatcher,
489 490 491 492 493 494 495 496 497 498 499 500 501 502
    StringConverter mapFunction,
    Map<String, String> environment,
  }) async {
    final Process process = await start(
      cmd,
      workingDirectory: workingDirectory,
      allowReentrantFlutter: allowReentrantFlutter,
      environment: environment,
    );
    final StreamSubscription<String> stdoutSubscription = process.stdout
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .where((String line) => filter == null || filter.hasMatch(line))
      .listen((String line) {
503
        if (mapFunction != null) {
504
          line = mapFunction(line);
505
        }
506 507
        if (line != null) {
          final String message = '$prefix$line';
508 509 510
          if (stdoutErrorMatcher?.hasMatch(line) == true) {
            _logger.printError(message, wrap: false);
          } else if (trace) {
511
            _logger.printTrace(message);
512
          } else {
513
            _logger.printStatus(message, wrap: false);
514
          }
515 516 517 518 519 520 521
        }
      });
    final StreamSubscription<String> stderrSubscription = process.stderr
      .transform<String>(utf8.decoder)
      .transform<String>(const LineSplitter())
      .where((String line) => filter == null || filter.hasMatch(line))
      .listen((String line) {
522
        if (mapFunction != null) {
523
          line = mapFunction(line);
524 525
        }
        if (line != null) {
526
          _logger.printError('$prefix$line', wrap: false);
527
        }
528 529 530 531 532 533 534 535 536
      });

    // 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>(),
    ]);

537 538 539 540 541 542 543
    // The streams as futures have already completed, so waiting for the
    // potentially async stream cancellation to complete likely has no benefit.
    // Further, some Stream implementations commonly used in tests don't
    // complete the Future returned here, which causes tests using
    // mocks/FakeAsync to fail when these Futures are awaited.
    unawaited(stdoutSubscription.cancel());
    unawaited(stderrSubscription.cancel());
544 545 546 547 548 549 550 551 552 553

    return await process.exitCode;
  }

  @override
  bool exitsHappySync(
    List<String> cli, {
    Map<String, String> environment,
  }) {
    _traceCommand(cli);
554 555 556 557 558
    if (!_processManager.canRun(cli.first)) {
      _logger.printTrace('$cli either does not exist or is not executable.');
      return false;
    }

559
    try {
560
      return _processManager.runSync(cli, environment: environment).exitCode == 0;
561
    } on Exception catch (error) {
562
      _logger.printTrace('$cli failed with $error');
563 564 565 566 567 568 569 570 571 572
      return false;
    }
  }

  @override
  Future<bool> exitsHappy(
    List<String> cli, {
    Map<String, String> environment,
  }) async {
    _traceCommand(cli);
573 574 575 576 577
    if (!_processManager.canRun(cli.first)) {
      _logger.printTrace('$cli either does not exist or is not executable.');
      return false;
    }

578
    try {
579
      return (await _processManager.run(cli, environment: environment)).exitCode == 0;
580
    } on Exception catch (error) {
581
      _logger.printTrace('$cli failed with $error');
582 583 584 585 586 587 588 589
      return false;
    }
  }

  Map<String, String> _environment(bool allowReentrantFlutter, [
    Map<String, String> environment,
  ]) {
    if (allowReentrantFlutter) {
590
      if (environment == null) {
591
        environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'};
592
      } else {
593
        environment['FLUTTER_ALREADY_LOCKED'] = 'true';
594
      }
595 596 597 598 599 600 601 602
    }

    return environment;
  }

  void _traceCommand(List<String> args, { String workingDirectory }) {
    final String argsText = args.join(' ');
    if (workingDirectory == null) {
603
      _logger.printTrace('executing: $argsText');
604
    } else {
605
      _logger.printTrace('executing: [$workingDirectory/] $argsText');
606 607 608
    }
  }
}