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';
6import 'dart:convert';
7import 'dart:io' hide Platform;
8
9import 'package:platform/platform.dart' show LocalPlatform, Platform;
10import 'package:process/process.dart';
11
12import 'common.dart';
13
14/// A helper class for classes that want to run a process.
15///
16/// The stderr and stdout can optionally be reported as the process runs, and
17/// capture the stdout properly without dropping any.
18class ProcessRunner {
19 ProcessRunner({
20 ProcessManager? processManager,
21 this.subprocessOutput = true,
22 this.defaultWorkingDirectory,
23 this.platform = const LocalPlatform(),
24 }) : processManager = processManager ?? const LocalProcessManager() {
25 environment = Map<String, String>.from(platform.environment);
26 }
27
28 /// The platform to use for a starting environment.
29 final Platform platform;
30
31 /// Set [subprocessOutput] to show output as processes run. Stdout from the
32 /// process will be printed to stdout, and stderr printed to stderr.
33 final bool subprocessOutput;
34
35 /// Set the [processManager] in order to inject a test instance to perform
36 /// testing.
37 final ProcessManager processManager;
38
39 /// Sets the default directory used when `workingDirectory` is not specified
40 /// to [runProcess].
41 final Directory? defaultWorkingDirectory;
42
43 /// The environment to run processes with.
44 late Map<String, String> environment;
45
46 /// Run the command and arguments in `commandLine` as a sub-process from
47 /// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses
48 /// [Directory.current] if [defaultWorkingDirectory] is not set.
49 ///
50 /// Set `failOk` if [runProcess] should not throw an exception when the
51 /// command completes with a non-zero exit code.
52 Future<String> runProcess(
53 List<String> commandLine, {
54 Directory? workingDirectory,
55 bool failOk = false,
56 }) async {
57 workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
58 if (subprocessOutput) {
59 stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
60 }
61 final List<int> output = <int>[];
62 final Completer<void> stdoutComplete = Completer<void>();
63 final Completer<void> stderrComplete = Completer<void>();
64 late Process process;
65 Future<int> allComplete() async {
66 await stderrComplete.future;
67 await stdoutComplete.future;
68 return process.exitCode;
69 }
70
71 try {
72 process = await processManager.start(
73 commandLine,
74 workingDirectory: workingDirectory.absolute.path,
75 environment: environment,
76 );
77 process.stdout.listen((List<int> event) {
78 output.addAll(event);
79 if (subprocessOutput) {
80 stdout.add(event);
81 }
82 }, onDone: () async => stdoutComplete.complete());
83 if (subprocessOutput) {
84 process.stderr.listen((List<int> event) {
85 stderr.add(event);
86 }, onDone: () async => stderrComplete.complete());
87 } else {
88 stderrComplete.complete();
89 }
90 } on ProcessException catch (e) {
91 final String message =
92 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
93 'failed with:\n$e';
94 throw PreparePackageException(message);
95 } on ArgumentError catch (e) {
96 final String message =
97 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
98 'failed with:\n$e';
99 throw PreparePackageException(message);
100 }
101
102 final int exitCode = await allComplete();
103 if (exitCode != 0 && !failOk) {
104 final String message =
105 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
106 throw PreparePackageException(
107 message,
108 ProcessResult(0, exitCode, null, 'returned $exitCode'),
109 );
110 }
111 return utf8.decoder.convert(output).trim();
112 }
113}
114