1// Copyright 2014 The Flutter 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 'package:meta/meta.dart';
8import 'package:process/process.dart';
9
10import '../convert.dart';
11import '../globals.dart' as globals;
12import 'io.dart';
13import 'logger.dart';
14
15typedef StringConverter = String? Function(String string);
16
17/// A function that will be run before the VM exits.
18typedef ShutdownHook = FutureOr<void> 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
26abstract class ShutdownHooks {
27 factory ShutdownHooks() = _DefaultShutdownHooks;
28
29 /// Registers a [ShutdownHook] to be executed before the VM exits.
30 void addShutdownHook(ShutdownHook shutdownHook);
31
32 @visibleForTesting
33 List<ShutdownHook> get registeredHooks;
34
35 /// Runs all registered shutdown hooks and returns a future that completes when
36 /// all such hooks have finished.
37 ///
38 /// Shutdown hooks will be run in groups by their [ShutdownStage]. All shutdown
39 /// hooks within a given stage will be started in parallel and will be
40 /// guaranteed to run to completion before shutdown hooks in the next stage are
41 /// started.
42 ///
43 /// This class is constructed before the [Logger], so it cannot be direct
44 /// injected in the constructor.
45 Future<void> runShutdownHooks(Logger logger);
46}
47
48class _DefaultShutdownHooks implements ShutdownHooks {
49 _DefaultShutdownHooks();
50
51 @override
52 final List<ShutdownHook> registeredHooks = <ShutdownHook>[];
53
54 bool _shutdownHooksRunning = false;
55
56 @override
57 void addShutdownHook(ShutdownHook shutdownHook) {
58 assert(!_shutdownHooksRunning);
59 registeredHooks.add(shutdownHook);
60 }
61
62 @override
63 Future<void> runShutdownHooks(Logger logger) async {
64 logger.printTrace(
65 'Running ${registeredHooks.length} shutdown hook${registeredHooks.length == 1 ? '' : 's'}',
66 );
67 _shutdownHooksRunning = true;
68 try {
69 final List<Future<dynamic>> futures = <Future<dynamic>>[
70 for (final ShutdownHook shutdownHook in registeredHooks)
71 if (shutdownHook() case final Future<dynamic> result) result,
72 ];
73 await Future.wait<dynamic>(futures);
74 } finally {
75 _shutdownHooksRunning = false;
76 }
77 logger.printTrace('Shutdown hooks complete');
78 }
79}
80
81class ProcessExit implements Exception {
82 ProcessExit(this.exitCode, {this.immediate = false});
83
84 final bool immediate;
85 final int exitCode;
86
87 String get message => 'ProcessExit: $exitCode';
88
89 @override
90 String toString() => message;
91}
92
93class RunResult {
94 RunResult(this.processResult, this._command) : assert(_command.isNotEmpty);
95
96 final ProcessResult processResult;
97
98 final List<String> _command;
99
100 int get exitCode => processResult.exitCode;
101 String get stdout => processResult.stdout as String;
102 String get stderr => processResult.stderr as String;
103
104 @override
105 String toString() {
106 final StringBuffer out = StringBuffer();
107 if (stdout.isNotEmpty) {
108 out.writeln(stdout);
109 }
110 if (stderr.isNotEmpty) {
111 out.writeln(stderr);
112 }
113 return out.toString().trimRight();
114 }
115
116 /// Throws a [ProcessException] with the given `message`.
117 void throwException(String message) {
118 throw ProcessException(_command.first, _command.skip(1).toList(), message, exitCode);
119 }
120}
121
122typedef RunResultChecker = bool Function(int);
123
124abstract class ProcessUtils {
125 factory ProcessUtils({required ProcessManager processManager, required Logger logger}) =
126 _DefaultProcessUtils;
127
128 /// Spawns a child process to run the command [cmd].
129 ///
130 /// When [throwOnError] is `true`, if the child process finishes with a non-zero
131 /// exit code, a [ProcessException] is thrown.
132 ///
133 /// If [throwOnError] is `true`, and [allowedFailures] is supplied,
134 /// a [ProcessException] is only thrown on a non-zero exit code if
135 /// [allowedFailures] returns false when passed the exit code.
136 ///
137 /// When [workingDirectory] is set, it is the working directory of the child
138 /// process.
139 ///
140 /// When [allowReentrantFlutter] is set to `true`, the child process is
141 /// permitted to call the Flutter tool. By default it is not.
142 ///
143 /// When [environment] is supplied, it is used as the environment for the child
144 /// process.
145 ///
146 /// When [timeout] is supplied, [runAsync] will kill the child process and
147 /// throw a [ProcessException] when it doesn't finish in time.
148 ///
149 /// If [timeout] is supplied, the command will be retried [timeoutRetries] times
150 /// if it times out.
151 Future<RunResult> run(
152 List<String> cmd, {
153 bool throwOnError = false,
154 RunResultChecker? allowedFailures,
155 String? workingDirectory,
156 bool allowReentrantFlutter = false,
157 Map<String, String>? environment,
158 Duration? timeout,
159 int timeoutRetries = 0,
160 });
161
162 /// Run the command and block waiting for its result.
163 RunResult runSync(
164 List<String> cmd, {
165 bool throwOnError = false,
166 bool verboseExceptions = false,
167 RunResultChecker? allowedFailures,
168 bool hideStdout = false,
169 String? workingDirectory,
170 Map<String, String>? environment,
171 bool allowReentrantFlutter = false,
172 Encoding encoding = systemEncoding,
173 });
174
175 /// This runs the command in the background from the specified working
176 /// directory. Completes when the process has been started.
177 Future<Process> start(
178 List<String> cmd, {
179 String? workingDirectory,
180 bool allowReentrantFlutter = false,
181 Map<String, String>? environment,
182 ProcessStartMode mode = ProcessStartMode.normal,
183 });
184
185 /// This runs the command and streams stdout/stderr from the child process to
186 /// this process' stdout/stderr. Completes with the process's exit code.
187 ///
188 /// If [filter] is null, no lines are removed.
189 ///
190 /// If [filter] is non-null, all lines that do not match it are removed. If
191 /// [mapFunction] is present, all lines that match [filter] are also forwarded
192 /// to [mapFunction] for further processing.
193 ///
194 /// If [stdoutErrorMatcher] is non-null, matching lines from stdout will be
195 /// treated as errors, just as if they had been logged to stderr instead.
196 Future<int> stream(
197 List<String> cmd, {
198 String? workingDirectory,
199 bool allowReentrantFlutter = false,
200 String prefix = '',
201 bool trace = false,
202 RegExp? filter,
203 RegExp? stdoutErrorMatcher,
204 StringConverter? mapFunction,
205 Map<String, String>? environment,
206 });
207
208 bool exitsHappySync(List<String> cli, {Map<String, String>? environment});
209
210 Future<bool> exitsHappy(List<String> cli, {Map<String, String>? environment});
211
212 /// Write [line] to [stdin] and catch any errors with [onError].
213 ///
214 /// Specifically with [Process] file descriptors, an exception that is
215 /// thrown as part of a write can be most reliably caught with a
216 /// [ZoneSpecification] error handler.
217 ///
218 /// On some platforms, the following code appears to work:
219 ///
220 /// ```dart
221 /// stdin.writeln(line);
222 /// try {
223 /// await stdin.flush(line);
224 /// } catch (err) {
225 /// // handle error
226 /// }
227 /// ```
228 ///
229 /// However it did not catch a [SocketException] on Linux.
230 ///
231 /// As part of making sure errors are caught, this function will call [flush]
232 /// on [stdin] to ensure that [line] is written to the pipe before this
233 /// function returns. This means completion will be blocked if the kernel
234 /// buffer of the pipe is full.
235 static Future<void> writelnToStdinGuarded({
236 required IOSink stdin,
237 required String line,
238 required void Function(Object, StackTrace) onError,
239 }) async {
240 await _writeToStdinGuarded(stdin: stdin, content: line, onError: onError, isLine: true);
241 }
242
243 /// Please see [writelnToStdinGuarded].
244 ///
245 /// This calls `stdin.write` instead of `stdin.writeln`.
246 static Future<void> writeToStdinGuarded({
247 required IOSink stdin,
248 required String content,
249 required void Function(Object, StackTrace) onError,
250 }) async {
251 await _writeToStdinGuarded(stdin: stdin, content: content, onError: onError, isLine: false);
252 }
253
254 static Future<void> writelnToStdinUnsafe({required IOSink stdin, required String line}) async {
255 await _writeToStdinUnsafe(stdin: stdin, content: line, isLine: true);
256 }
257
258 static Future<void> writeToStdinUnsafe({required IOSink stdin, required String content}) async {
259 await _writeToStdinUnsafe(stdin: stdin, content: content, isLine: false);
260 }
261
262 static Future<void> _writeToStdinGuarded({
263 required IOSink stdin,
264 required String content,
265 required void Function(Object, StackTrace) onError,
266 required bool isLine,
267 }) async {
268 try {
269 await _writeToStdinUnsafe(stdin: stdin, content: content, isLine: isLine);
270 } on Exception catch (error, stackTrace) {
271 onError(error, stackTrace);
272 }
273 }
274
275 static Future<void> _writeToStdinUnsafe({
276 required IOSink stdin,
277 required String content,
278 required bool isLine,
279 }) {
280 final Completer<void> completer = Completer<void>();
281
282 void handleError(Object error, StackTrace stackTrace) {
283 completer.completeError(error, stackTrace);
284 }
285
286 void writeFlushAndComplete() {
287 if (isLine) {
288 stdin.writeln(content);
289 } else {
290 stdin.write(content);
291 }
292 stdin.flush().then((_) {
293 completer.complete();
294 }, onError: handleError);
295 }
296
297 runZonedGuarded(writeFlushAndComplete, handleError);
298
299 return completer.future;
300 }
301}
302
303class _DefaultProcessUtils implements ProcessUtils {
304 _DefaultProcessUtils({required ProcessManager processManager, required Logger logger})
305 : _processManager = processManager,
306 _logger = logger;
307
308 final ProcessManager _processManager;
309
310 final Logger _logger;
311
312 @override
313 Future<RunResult> run(
314 List<String> cmd, {
315 bool throwOnError = false,
316 RunResultChecker? allowedFailures,
317 String? workingDirectory,
318 bool allowReentrantFlutter = false,
319 Map<String, String>? environment,
320 Duration? timeout,
321 int timeoutRetries = 0,
322 }) async {
323 if (cmd.isEmpty) {
324 throw ArgumentError('cmd must be a non-empty list');
325 }
326 if (timeoutRetries < 0) {
327 throw ArgumentError('timeoutRetries must be non-negative');
328 }
329 _traceCommand(cmd, workingDirectory: workingDirectory);
330
331 // When there is no timeout, there's no need to kill a running process, so
332 // we can just use _processManager.run().
333 if (timeout == null) {
334 final ProcessResult results = await _processManager.run(
335 cmd,
336 workingDirectory: workingDirectory,
337 environment: _environment(allowReentrantFlutter, environment),
338 );
339 final RunResult runResult = RunResult(results, cmd);
340 _logger.printTrace(runResult.toString());
341 if (throwOnError &&
342 runResult.exitCode != 0 &&
343 (allowedFailures == null || !allowedFailures(runResult.exitCode))) {
344 runResult.throwException(
345 'Process exited abnormally with exit code ${runResult.exitCode}:\n$runResult',
346 );
347 }
348 return runResult;
349 }
350
351 // When there is a timeout, we have to kill the running process, so we have
352 // to use _processManager.start() through _runCommand() above.
353 while (true) {
354 assert(timeoutRetries >= 0);
355 timeoutRetries = timeoutRetries - 1;
356
357 final Process process = await start(
358 cmd,
359 workingDirectory: workingDirectory,
360 allowReentrantFlutter: allowReentrantFlutter,
361 environment: environment,
362 );
363
364 final StringBuffer stdoutBuffer = StringBuffer();
365 final StringBuffer stderrBuffer = StringBuffer();
366 final Future<void> stdoutFuture =
367 process.stdout
368 .transform<String>(const Utf8Decoder(reportErrors: false))
369 .listen(stdoutBuffer.write)
370 .asFuture<void>();
371 final Future<void> stderrFuture =
372 process.stderr
373 .transform<String>(const Utf8Decoder(reportErrors: false))
374 .listen(stderrBuffer.write)
375 .asFuture<void>();
376
377 int? exitCode;
378 exitCode = await process.exitCode
379 .then<int?>((int x) => x)
380 .timeout(
381 timeout,
382 onTimeout: () {
383 // The process timed out. Kill it.
384 _processManager.killPid(process.pid);
385 return null;
386 },
387 );
388
389 String stdoutString;
390 String stderrString;
391 try {
392 Future<void> stdioFuture = Future.wait<void>(<Future<void>>[stdoutFuture, stderrFuture]);
393 if (exitCode == null) {
394 // If we had to kill the process for a timeout, only wait a short time
395 // for the stdio streams to drain in case killing the process didn't
396 // work.
397 stdioFuture = stdioFuture.timeout(const Duration(seconds: 1));
398 }
399 await stdioFuture;
400 } on Exception {
401 // Ignore errors on the process' stdout and stderr streams. Just capture
402 // whatever we got, and use the exit code
403 }
404 stdoutString = stdoutBuffer.toString();
405 stderrString = stderrBuffer.toString();
406
407 final ProcessResult result = ProcessResult(
408 process.pid,
409 exitCode ?? -1,
410 stdoutString,
411 stderrString,
412 );
413 final RunResult runResult = RunResult(result, cmd);
414
415 // If the process did not timeout. We are done.
416 if (exitCode != null) {
417 _logger.printTrace(runResult.toString());
418 if (throwOnError &&
419 runResult.exitCode != 0 &&
420 (allowedFailures == null || !allowedFailures(exitCode))) {
421 runResult.throwException(
422 'Process exited abnormally with exit code $exitCode:\n$runResult',
423 );
424 }
425 return runResult;
426 }
427
428 // If we are out of timeoutRetries, throw a ProcessException.
429 if (timeoutRetries < 0) {
430 runResult.throwException('Process timed out:\n$runResult');
431 }
432
433 // Log the timeout with a trace message in verbose mode.
434 _logger.printTrace(
435 'Process "${cmd[0]}" timed out. $timeoutRetries attempts left:\n'
436 '$runResult',
437 );
438 }
439
440 // Unreachable.
441 }
442
443 @override
444 RunResult runSync(
445 List<String> cmd, {
446 bool throwOnError = false,
447 bool verboseExceptions = false,
448 RunResultChecker? allowedFailures,
449 bool hideStdout = false,
450 String? workingDirectory,
451 Map<String, String>? environment,
452 bool allowReentrantFlutter = false,
453 Encoding encoding = systemEncoding,
454 }) {
455 _traceCommand(cmd, workingDirectory: workingDirectory);
456 final ProcessResult results = _processManager.runSync(
457 cmd,
458 workingDirectory: workingDirectory,
459 environment: _environment(allowReentrantFlutter, environment),
460 stderrEncoding: encoding,
461 stdoutEncoding: encoding,
462 );
463 final RunResult runResult = RunResult(results, cmd);
464
465 _logger.printTrace('Exit code ${runResult.exitCode} from: ${cmd.join(' ')}');
466
467 bool failedExitCode = runResult.exitCode != 0;
468 if (allowedFailures != null && failedExitCode) {
469 failedExitCode = !allowedFailures(runResult.exitCode);
470 }
471
472 if (runResult.stdout.isNotEmpty && !hideStdout) {
473 if (failedExitCode && throwOnError) {
474 _logger.printStatus(runResult.stdout.trim());
475 } else {
476 _logger.printTrace(runResult.stdout.trim());
477 }
478 }
479
480 if (runResult.stderr.isNotEmpty) {
481 if (failedExitCode && throwOnError) {
482 _logger.printError(runResult.stderr.trim());
483 } else {
484 _logger.printTrace(runResult.stderr.trim());
485 }
486 }
487
488 if (failedExitCode && throwOnError) {
489 String message = 'The command failed with exit code ${runResult.exitCode}';
490 if (verboseExceptions) {
491 message =
492 'The command failed\nStdout:\n${runResult.stdout}\n'
493 'Stderr:\n${runResult.stderr}';
494 }
495 runResult.throwException(message);
496 }
497
498 return runResult;
499 }
500
501 @override
502 Future<Process> start(
503 List<String> cmd, {
504 String? workingDirectory,
505 bool allowReentrantFlutter = false,
506 Map<String, String>? environment,
507 ProcessStartMode mode = ProcessStartMode.normal,
508 }) {
509 _traceCommand(cmd, workingDirectory: workingDirectory);
510 return _processManager.start(
511 cmd,
512 workingDirectory: workingDirectory,
513 environment: _environment(allowReentrantFlutter, environment),
514 mode: mode,
515 );
516 }
517
518 @override
519 Future<int> stream(
520 List<String> cmd, {
521 String? workingDirectory,
522 bool allowReentrantFlutter = false,
523 String prefix = '',
524 bool trace = false,
525 RegExp? filter,
526 RegExp? stdoutErrorMatcher,
527 StringConverter? mapFunction,
528 Map<String, String>? environment,
529 }) async {
530 final Process process = await start(
531 cmd,
532 workingDirectory: workingDirectory,
533 allowReentrantFlutter: allowReentrantFlutter,
534 environment: environment,
535 );
536 final StreamSubscription<String> stdoutSubscription = process.stdout
537 .transform<String>(utf8.decoder)
538 .transform<String>(const LineSplitter())
539 .where((String line) => filter == null || filter.hasMatch(line))
540 .listen((String line) {
541 String? mappedLine = line;
542 if (mapFunction != null) {
543 mappedLine = mapFunction(line);
544 }
545 if (mappedLine != null) {
546 final String message = '$prefix$mappedLine';
547 if (stdoutErrorMatcher?.hasMatch(mappedLine) ?? false) {
548 _logger.printError(message, wrap: false);
549 } else if (trace) {
550 _logger.printTrace(message);
551 } else {
552 _logger.printStatus(message, wrap: false);
553 }
554 }
555 });
556 final StreamSubscription<String> stderrSubscription = process.stderr
557 .transform<String>(utf8.decoder)
558 .transform<String>(const LineSplitter())
559 .where((String line) => filter == null || filter.hasMatch(line))
560 .listen((String line) {
561 String? mappedLine = line;
562 if (mapFunction != null) {
563 mappedLine = mapFunction(line);
564 }
565 if (mappedLine != null) {
566 _logger.printError('$prefix$mappedLine', wrap: false);
567 }
568 });
569
570 // Wait for stdout to be fully processed
571 // because process.exitCode may complete first causing flaky tests.
572 await Future.wait<void>(<Future<void>>[
573 stdoutSubscription.asFuture<void>(),
574 stderrSubscription.asFuture<void>(),
575 ]);
576
577 // The streams as futures have already completed, so waiting for the
578 // potentially async stream cancellation to complete likely has no benefit.
579 // Further, some Stream implementations commonly used in tests don't
580 // complete the Future returned here, which causes tests using
581 // mocks/FakeAsync to fail when these Futures are awaited.
582 unawaited(stdoutSubscription.cancel());
583 unawaited(stderrSubscription.cancel());
584
585 return process.exitCode;
586 }
587
588 @override
589 bool exitsHappySync(List<String> cli, {Map<String, String>? environment}) {
590 _traceCommand(cli);
591 if (!_processManager.canRun(cli.first)) {
592 _logger.printTrace('$cli either does not exist or is not executable.');
593 return false;
594 }
595
596 try {
597 return _processManager.runSync(cli, environment: environment).exitCode == 0;
598 } on Exception catch (error) {
599 _logger.printTrace('$cli failed with $error');
600 return false;
601 }
602 }
603
604 @override
605 Future<bool> exitsHappy(List<String> cli, {Map<String, String>? environment}) async {
606 _traceCommand(cli);
607 if (!_processManager.canRun(cli.first)) {
608 _logger.printTrace('$cli either does not exist or is not executable.');
609 return false;
610 }
611
612 try {
613 return (await _processManager.run(cli, environment: environment)).exitCode == 0;
614 } on Exception catch (error) {
615 _logger.printTrace('$cli failed with $error');
616 return false;
617 }
618 }
619
620 Map<String, String>? _environment(
621 bool allowReentrantFlutter, [
622 Map<String, String>? environment,
623 ]) {
624 if (allowReentrantFlutter) {
625 if (environment == null) {
626 environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'};
627 } else {
628 environment['FLUTTER_ALREADY_LOCKED'] = 'true';
629 }
630 }
631
632 return environment;
633 }
634
635 void _traceCommand(List<String> args, {String? workingDirectory}) {
636 final String argsText = args.join(' ');
637 if (workingDirectory == null) {
638 _logger.printTrace('executing: $argsText');
639 } else {
640 _logger.printTrace('executing: [$workingDirectory/] $argsText');
641 }
642 }
643}
644
645Future<int> exitWithHooks(int code, {required ShutdownHooks shutdownHooks}) async {
646 if (globals.analytics.shouldShowMessage) {
647 globals.logger.printStatus(globals.analytics.getConsentMessage);
648 globals.analytics.clientShowedMessage();
649
650 // This trace is searched for in tests.
651 globals.logger.printTrace('Showed analytics consent message.');
652 }
653
654 // Run shutdown hooks before flushing logs
655 await shutdownHooks.runShutdownHooks(globals.logger);
656
657 final Completer<void> completer = Completer<void>();
658
659 await globals.analytics.close();
660
661 // Give the task / timer queue one cycle through before we hard exit.
662 Timer.run(() {
663 try {
664 globals.printTrace('exiting with code $code');
665 exit(code);
666 completer.complete();
667 // This catches all exceptions because the error is propagated on the
668 // completer.
669 } catch (error, stackTrace) {
670 completer.completeError(error, stackTrace);
671 }
672 });
673
674 await completer.future;
675 return code;
676}
677

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com