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

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

Devon Carew's avatar
Devon Carew committed
14
typedef String StringConverter(String string);
15 16

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

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

25 26 27 28
/// 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> {
29
  const ShutdownStage._(this.priority);
30 31

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

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

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

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

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

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

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

/// 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,
]) {
66
  assert(!_shutdownHooksRunning);
67
  _shutdownHooks.putIfAbsent(stage, () => <ShutdownHook>[]).add(shutdownHook);
68 69
}

70 71 72 73 74 75 76
/// 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.
77
Future<Null> runShutdownHooks() async {
78
  printTrace('Running shutdown hooks');
79 80
  _shutdownHooksRunning = true;
  try {
81
    for (ShutdownStage stage in _shutdownHooks.keys.toList()..sort()) {
82
      printTrace('Shutdown hook priority ${stage.priority}');
83 84
      final List<ShutdownHook> hooks = _shutdownHooks.remove(stage);
      final List<Future<dynamic>> futures = <Future<dynamic>>[];
85 86 87 88
      for (ShutdownHook shutdownHook in hooks)
        futures.add(shutdownHook());
      await Future.wait<dynamic>(futures);
    }
89 90 91 92
  } finally {
    _shutdownHooksRunning = false;
  }
  assert(_shutdownHooks.isEmpty);
93
  printTrace('Shutdown hooks complete');
94 95
}

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

  return environment;
105 106
}

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

122
/// This runs the command and streams stdout/stderr from the child process to
123
/// this process' stdout/stderr. Completes with the process's exit code.
124
Future<int> runCommandAndStreamOutput(List<String> cmd, {
Devon Carew's avatar
Devon Carew committed
125
  String workingDirectory,
126
  bool allowReentrantFlutter: false,
127
  String prefix: '',
128
  bool trace: false,
129
  RegExp filter,
130 131
  StringConverter mapFunction,
  Map<String, String> environment
132
}) async {
133
  final Process process = await runCommand(
134 135
    cmd,
    workingDirectory: workingDirectory,
136 137
    allowReentrantFlutter: allowReentrantFlutter,
    environment: environment
138
  );
139
  final StreamSubscription<String> stdoutSubscription = process.stdout
140
    .transform(utf8.decoder)
141 142 143
    .transform(const LineSplitter())
    .where((String line) => filter == null ? true : filter.hasMatch(line))
    .listen((String line) {
Devon Carew's avatar
Devon Carew committed
144 145
      if (mapFunction != null)
        line = mapFunction(line);
146
      if (line != null) {
147
        final String message = '$prefix$line';
148 149 150 151 152
        if (trace)
          printTrace(message);
        else
          printStatus(message);
      }
153
    });
154
  final StreamSubscription<String> stderrSubscription = process.stderr
155
    .transform(utf8.decoder)
156 157 158
    .transform(const LineSplitter())
    .where((String line) => filter == null ? true : filter.hasMatch(line))
    .listen((String line) {
Devon Carew's avatar
Devon Carew committed
159 160 161 162
      if (mapFunction != null)
        line = mapFunction(line);
      if (line != null)
        printError('$prefix$line');
163
    });
164 165 166

  // Wait for stdout to be fully processed
  // because process.exitCode may complete first causing flaky tests.
167
  await waitGroup<Null>(<Future<Null>>[
168 169 170
    stdoutSubscription.asFuture<Null>(),
    stderrSubscription.asFuture<Null>(),
  ]);
171 172 173 174 175

  await waitGroup<Null>(<Future<Null>>[
    stdoutSubscription.cancel(),
    stderrSubscription.cancel(),
  ]);
176

Hixie's avatar
Hixie committed
177
  return await process.exitCode;
178
}
179

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
/// 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.
Future<int> runInteractively(List<String> command, {
  String workingDirectory,
  bool allowReentrantFlutter: false,
  Map<String, String> environment
}) async {
  final Process process = await runCommand(
    command,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter,
    environment: environment,
  );
  process.stdin.addStream(stdin);
  // Wait for stdout and stderr to be fully processed, because process.exitCode
  // may complete first.
197
  await Future.wait<dynamic>(<Future<dynamic>>[
198 199 200 201 202 203
    stdout.addStream(process.stdout),
    stderr.addStream(process.stderr),
  ]);
  return await process.exitCode;
}

Ian Hickson's avatar
Ian Hickson committed
204
Future<Null> runAndKill(List<String> cmd, Duration timeout) {
205
  final Future<Process> proc = runDetached(cmd);
Ian Hickson's avatar
Ian Hickson committed
206
  return new Future<Null>.delayed(timeout, () async {
207
    printTrace('Intentionally killing ${cmd[0]}');
208
    processManager.killPid((await proc).pid);
209 210 211
  });
}

Hixie's avatar
Hixie committed
212
Future<Process> runDetached(List<String> cmd) {
213
  _traceCommand(cmd);
214
  final Future<Process> proc = processManager.start(
215
    cmd,
216
    mode: ProcessStartMode.DETACHED, // ignore: deprecated_member_use
Devon Carew's avatar
Devon Carew committed
217
  );
218
  return proc;
219
}
220

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

237 238 239 240 241 242 243 244 245 246 247 248
Future<RunResult> runCheckedAsync(List<String> cmd, {
  String workingDirectory,
  bool allowReentrantFlutter: false,
  Map<String, String> environment
}) async {
  final RunResult result = await runAsync(
      cmd,
      workingDirectory: workingDirectory,
      allowReentrantFlutter: allowReentrantFlutter,
      environment: environment
  );
  if (result.exitCode != 0)
249
    throw 'Exit code ${result.exitCode} from: ${cmd.join(' ')}:\n$result';
250 251 252
  return result;
}

253
bool exitsHappy(List<String> cli) {
254
  _traceCommand(cli);
255
  try {
256
    return processManager.runSync(cli).exitCode == 0;
257 258 259 260 261
  } catch (error) {
    return false;
  }
}

262 263 264 265 266 267 268 269 270
Future<bool> exitsHappyAsync(List<String> cli) async {
  _traceCommand(cli);
  try {
    return (await processManager.run(cli)).exitCode == 0;
  } catch (error) {
    return false;
  }
}

271 272 273 274 275
/// Run cmd and return stdout.
///
/// Throws an error if cmd exits with a non-zero value.
String runCheckedSync(List<String> cmd, {
  String workingDirectory,
276 277
  bool allowReentrantFlutter: false,
  bool hideStdout: false,
278
  Map<String, String> environment,
279 280 281 282 283
}) {
  return _runWithLoggingSync(
    cmd,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter,
284
    hideStdout: hideStdout,
285
    checked: true,
286
    noisyErrors: true,
287
    environment: environment,
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
  );
}

/// Run cmd and return stdout.
String runSync(List<String> cmd, {
  String workingDirectory,
  bool allowReentrantFlutter: false
}) {
  return _runWithLoggingSync(
    cmd,
    workingDirectory: workingDirectory,
    allowReentrantFlutter: allowReentrantFlutter
  );
}

303
void _traceCommand(List<String> args, { String workingDirectory }) {
304
  final String argsText = args.join(' ');
305 306 307
  if (workingDirectory == null)
    printTrace(argsText);
  else
308
    printTrace('[$workingDirectory${fs.path.separator}] $argsText');
309 310
}

311 312
String _runWithLoggingSync(List<String> cmd, {
  bool checked: false,
Devon Carew's avatar
Devon Carew committed
313
  bool noisyErrors: false,
314
  bool throwStandardErrorOnError: false,
Devon Carew's avatar
Devon Carew committed
315
  String workingDirectory,
316 317
  bool allowReentrantFlutter: false,
  bool hideStdout: false,
318
  Map<String, String> environment,
319
}) {
320
  _traceCommand(cmd, workingDirectory: workingDirectory);
321
  final ProcessResult results = processManager.runSync(
322
    cmd,
323
    workingDirectory: workingDirectory,
324
    environment: _environment(allowReentrantFlutter, environment),
325
  );
Devon Carew's avatar
Devon Carew committed
326 327 328

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

329
  if (results.stdout.isNotEmpty && !hideStdout) {
Devon Carew's avatar
Devon Carew committed
330 331 332 333 334 335
    if (results.exitCode != 0 && noisyErrors)
      printStatus(results.stdout.trim());
    else
      printTrace(results.stdout.trim());
  }

336
  if (results.exitCode != 0) {
Devon Carew's avatar
Devon Carew committed
337 338
    if (results.stderr.isNotEmpty) {
      if (noisyErrors)
Devon Carew's avatar
Devon Carew committed
339
        printError(results.stderr.trim());
Devon Carew's avatar
Devon Carew committed
340 341
      else
        printTrace(results.stderr.trim());
Devon Carew's avatar
Devon Carew committed
342
    }
Devon Carew's avatar
Devon Carew committed
343

344 345 346
    if (throwStandardErrorOnError)
      throw results.stderr.trim();

347
    if (checked)
Devon Carew's avatar
Devon Carew committed
348
      throw 'Exit code ${results.exitCode} from: ${cmd.join(' ')}';
349
  }
Devon Carew's avatar
Devon Carew committed
350

351
  return results.stdout.trim();
352
}
353 354

class ProcessExit implements Exception {
355
  ProcessExit(this.exitCode, {this.immediate: false});
356

357
  final bool immediate;
358 359
  final int exitCode;

Hixie's avatar
Hixie committed
360
  String get message => 'ProcessExit: $exitCode';
361 362

  @override
363 364
  String toString() => message;
}
365 366 367 368 369 370 371

class RunResult {
  RunResult(this.processResult);

  final ProcessResult processResult;

  int get exitCode => processResult.exitCode;
372 373
  String get stdout => processResult.stdout;
  String get stderr => processResult.stderr;
374 375 376

  @override
  String toString() {
377
    final StringBuffer out = new StringBuffer();
378 379 380 381 382 383 384
    if (processResult.stdout.isNotEmpty)
      out.writeln(processResult.stdout);
    if (processResult.stderr.isNotEmpty)
      out.writeln(processResult.stderr);
    return out.toString().trimRight();
  }
}