• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import '../convert.dart';
8import '../globals.dart';
9import 'common.dart';
10import 'file_system.dart';
11import 'io.dart';
12import 'process_manager.dart';
13import 'utils.dart';
14
15typedef StringConverter = String Function(String string);
16
17/// A function that will be run before the VM exits.
18typedef ShutdownHook = Future<dynamic> Function();
19
20// TODO(ianh): We have way too many ways to run subprocesses in this project.
21// Convert most of these into one or more lightweight wrappers around the
22// [ProcessManager] API using named parameters for the various options.
23// See [here](https://github.com/flutter/flutter/pull/14535#discussion_r167041161)
24// for more details.
25
26/// The stage in which a [ShutdownHook] will be run. All shutdown hooks within
27/// a given stage will be started in parallel and will be guaranteed to run to
28/// completion before shutdown hooks in the next stage are started.
29class 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  /// The stage before the invocation recording (if one exists) is serialized
36  /// to disk. Tasks performed during this stage *will* be recorded.
37  static const ShutdownStage STILL_RECORDING = ShutdownStage._(1);
38
39  /// The stage during which the invocation recording (if one exists) will be
40  /// serialized to disk. Invocations performed after this stage will not be
41  /// recorded.
42  static const ShutdownStage SERIALIZE_RECORDING = ShutdownStage._(2);
43
44  /// The stage during which a serialized recording will be refined (e.g.
45  /// 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
55Map<ShutdownStage, List<ShutdownHook>> _shutdownHooks = <ShutdownStage, List<ShutdownHook>>{};
56bool _shutdownHooksRunning = false;
57
58/// Registers a [ShutdownHook] to be executed before the VM exits.
59///
60/// If [stage] is specified, the shutdown hook will be run during the specified
61/// stage. By default, the shutdown hook will be run during the
62/// [ShutdownStage.CLEANUP] stage.
63void addShutdownHook(
64  ShutdownHook shutdownHook, [
65  ShutdownStage stage = ShutdownStage.CLEANUP,
66]) {
67  assert(!_shutdownHooksRunning);
68  _shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
69}
70
71/// Runs all registered shutdown hooks and returns a future that completes when
72/// all such hooks have finished.
73///
74/// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown
75/// hooks within a given stage will be started in parallel and will be
76/// guaranteed to run to completion before shutdown hooks in the next stage are
77/// started.
78Future<void> runShutdownHooks() async {
79  printTrace('Running shutdown hooks');
80  _shutdownHooksRunning = true;
81  try {
82    for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
83      printTrace('Shutdown hook priority ${stage.priority}');
84      final List<ShutdownHook> hooks = _shutdownHooks.remove(stage);
85      final List<Future<dynamic>> futures = <Future<dynamic>>[];
86      for (ShutdownHook shutdownHook in hooks)
87        futures.add(shutdownHook());
88      await Future.wait<dynamic>(futures);
89    }
90  } finally {
91    _shutdownHooksRunning = false;
92  }
93  assert(_shutdownHooks.isEmpty);
94  printTrace('Shutdown hooks complete');
95}
96
97Map<String, String> _environment(bool allowReentrantFlutter, [ Map<String, String> environment ]) {
98  if (allowReentrantFlutter) {
99    if (environment == null)
100      environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'};
101    else
102      environment['FLUTTER_ALREADY_LOCKED'] = 'true';
103  }
104
105  return environment;
106}
107
108/// This runs the command in the background from the specified working
109/// directory. Completes when the process has been started.
110Future<Process> runCommand(
111  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/// If [filter] is null, no lines are removed.
128///
129/// If [filter] is non-null, all lines that do not match it are removed. If
130/// [mapFunction] is present, all lines that match [filter] are also forwarded
131/// to [mapFunction] for further processing.
132Future<int> runCommandAndStreamOutput(
133  List<String> cmd, {
134  String workingDirectory,
135  bool allowReentrantFlutter = false,
136  String prefix = '',
137  bool trace = false,
138  RegExp filter,
139  StringConverter mapFunction,
140  Map<String, String> environment,
141}) async {
142  final Process process = await runCommand(
143    cmd,
144    workingDirectory: workingDirectory,
145    allowReentrantFlutter: allowReentrantFlutter,
146    environment: environment,
147  );
148  final StreamSubscription<String> stdoutSubscription = process.stdout
149    .transform<String>(utf8.decoder)
150    .transform<String>(const LineSplitter())
151    .where((String line) => filter == null || filter.hasMatch(line))
152    .listen((String line) {
153      if (mapFunction != null)
154        line = mapFunction(line);
155      if (line != null) {
156        final String message = '$prefix$line';
157        if (trace)
158          printTrace(message);
159        else
160          printStatus(message, wrap: false);
161      }
162    });
163  final StreamSubscription<String> stderrSubscription = process.stderr
164    .transform<String>(utf8.decoder)
165    .transform<String>(const LineSplitter())
166    .where((String line) => filter == null || filter.hasMatch(line))
167    .listen((String line) {
168      if (mapFunction != null)
169        line = mapFunction(line);
170      if (line != null)
171        printError('$prefix$line', wrap: false);
172    });
173
174  // Wait for stdout to be fully processed
175  // because process.exitCode may complete first causing flaky tests.
176  await waitGroup<void>(<Future<void>>[
177    stdoutSubscription.asFuture<void>(),
178    stderrSubscription.asFuture<void>(),
179  ]);
180
181  await waitGroup<void>(<Future<void>>[
182    stdoutSubscription.cancel(),
183    stderrSubscription.cancel(),
184  ]);
185
186  return await process.exitCode;
187}
188
189/// Runs the [command] interactively, connecting the stdin/stdout/stderr
190/// streams of this process to those of the child process. Completes with
191/// the exit code of the child process.
192Future<int> runInteractively(
193  List<String> command, {
194  String workingDirectory,
195  bool allowReentrantFlutter = false,
196  Map<String, String> environment,
197}) async {
198  final Process process = await runCommand(
199    command,
200    workingDirectory: workingDirectory,
201    allowReentrantFlutter: allowReentrantFlutter,
202    environment: environment,
203  );
204  // The real stdin will never finish streaming. Pipe until the child process
205  // finishes.
206  unawaited(process.stdin.addStream(stdin));
207  // Wait for stdout and stderr to be fully processed, because process.exitCode
208  // may complete first.
209  await Future.wait<dynamic>(<Future<dynamic>>[
210    stdout.addStream(process.stdout),
211    stderr.addStream(process.stderr),
212  ]);
213  return await process.exitCode;
214}
215
216Future<Process> runDetached(List<String> cmd) {
217  _traceCommand(cmd);
218  final Future<Process> proc = processManager.start(
219    cmd,
220    mode: ProcessStartMode.detached,
221  );
222  return proc;
223}
224
225Future<RunResult> runAsync(
226  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  printTrace(runResults.toString());
239  return runResults;
240}
241
242typedef RunResultChecker = bool Function(int);
243
244Future<RunResult> runCheckedAsync(
245  List<String> cmd, {
246  String workingDirectory,
247  bool allowReentrantFlutter = false,
248  Map<String, String> environment,
249  RunResultChecker whiteListFailures,
250}) async {
251  final RunResult result = await runAsync(
252    cmd,
253    workingDirectory: workingDirectory,
254    allowReentrantFlutter: allowReentrantFlutter,
255    environment: environment,
256  );
257  if (result.exitCode != 0) {
258    if (whiteListFailures == null || !whiteListFailures(result.exitCode)) {
259      throw ProcessException(cmd[0], cmd.sublist(1),
260          'Process "${cmd[0]}" exited abnormally:\n$result', result.exitCode);
261    }
262  }
263  return result;
264}
265
266bool exitsHappy(
267  List<String> cli, {
268  Map<String, String> environment,
269}) {
270  _traceCommand(cli);
271  try {
272    return processManager.runSync(cli, environment: environment).exitCode == 0;
273  } catch (error) {
274    printTrace('$cli failed with $error');
275    return false;
276  }
277}
278
279Future<bool> exitsHappyAsync(
280  List<String> cli, {
281  Map<String, String> environment,
282}) async {
283  _traceCommand(cli);
284  try {
285    return (await processManager.run(cli, environment: environment)).exitCode == 0;
286  } catch (error) {
287    printTrace('$cli failed with $error');
288    return false;
289  }
290}
291
292/// Run cmd and return stdout.
293///
294/// Throws an error if cmd exits with a non-zero value.
295String runCheckedSync(
296  List<String> cmd, {
297  String workingDirectory,
298  bool allowReentrantFlutter = false,
299  bool hideStdout = false,
300  Map<String, String> environment,
301  RunResultChecker whiteListFailures,
302}) {
303  return _runWithLoggingSync(
304    cmd,
305    workingDirectory: workingDirectory,
306    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.
316String runSync(
317  List<String> cmd, {
318  String workingDirectory,
319  bool allowReentrantFlutter = false,
320}) {
321  return _runWithLoggingSync(
322    cmd,
323    workingDirectory: workingDirectory,
324    allowReentrantFlutter: allowReentrantFlutter,
325  );
326}
327
328void _traceCommand(List<String> args, { String workingDirectory }) {
329  final String argsText = args.join(' ');
330  if (workingDirectory == null) {
331    printTrace('executing: $argsText');
332  } else {
333    printTrace('executing: [$workingDirectory${fs.path.separator}] $argsText');
334  }
335}
336
337String _runWithLoggingSync(
338  List<String> cmd, {
339  bool checked = false,
340  bool noisyErrors = false,
341  bool throwStandardErrorOnError = false,
342  String workingDirectory,
343  bool allowReentrantFlutter = false,
344  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  );
354
355  printTrace('Exit code ${results.exitCode} from: ${cmd.join(' ')}');
356
357  bool failedExitCode = results.exitCode != 0;
358  if (whiteListFailures != null && failedExitCode) {
359    failedExitCode = !whiteListFailures(results.exitCode);
360  }
361
362  if (results.stdout.isNotEmpty && !hideStdout) {
363    if (failedExitCode && noisyErrors)
364      printStatus(results.stdout.trim());
365    else
366      printTrace(results.stdout.trim());
367  }
368
369  if (failedExitCode) {
370    if (results.stderr.isNotEmpty) {
371      if (noisyErrors)
372        printError(results.stderr.trim());
373      else
374        printTrace(results.stderr.trim());
375    }
376
377    if (throwStandardErrorOnError)
378      throw results.stderr.trim();
379
380    if (checked)
381      throw 'Exit code ${results.exitCode} from: ${cmd.join(' ')}';
382  }
383
384  return results.stdout.trim();
385}
386
387class ProcessExit implements Exception {
388  ProcessExit(this.exitCode, {this.immediate = false});
389
390  final bool immediate;
391  final int exitCode;
392
393  String get message => 'ProcessExit: $exitCode';
394
395  @override
396  String toString() => message;
397}
398
399class RunResult {
400  RunResult(this.processResult, this._command)
401    : assert(_command != null),
402      assert(_command.isNotEmpty);
403
404  final ProcessResult processResult;
405
406  final List<String> _command;
407
408  int get exitCode => processResult.exitCode;
409  String get stdout => processResult.stdout;
410  String get stderr => processResult.stderr;
411
412  @override
413  String toString() {
414    final StringBuffer out = StringBuffer();
415    if (processResult.stdout.isNotEmpty)
416      out.writeln(processResult.stdout);
417    if (processResult.stderr.isNotEmpty)
418      out.writeln(processResult.stderr);
419    return out.toString().trimRight();
420  }
421
422  /// Throws a [ProcessException] with the given `message`.
423  void throwException(String message) {
424    throw ProcessException(
425      _command.first,
426      _command.skip(1).toList(),
427      message,
428      exitCode,
429    );
430  }
431}
432