| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. 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'; |
| import 'dart:io'; |
| |
| import 'package:logging/logging.dart'; |
| |
| /// Runs a [Process]. |
| /// |
| /// If [logger] is provided, stream stdout and stderr to it. |
| /// |
| /// If [captureOutput], captures stdout and stderr. |
| Future<RunProcessResult> runProcess({ |
| required Uri executable, |
| List<String> arguments = const [], |
| Uri? workingDirectory, |
| Map<String, String>? environment, |
| bool includeParentEnvironment = true, |
| required Logger? logger, |
| bool captureOutput = true, |
| int expectedExitCode = 0, |
| bool throwOnUnexpectedExitCode = false, |
| }) async { |
| if (Platform.isWindows && !includeParentEnvironment) { |
| const winEnvKeys = [ |
| 'SYSTEMROOT', |
| ]; |
| environment = { |
| for (final winEnvKey in winEnvKeys) |
| winEnvKey: Platform.environment[winEnvKey]!, |
| ...?environment, |
| }; |
| } |
| |
| final printWorkingDir = |
| workingDirectory != null && workingDirectory != Directory.current.uri; |
| final commandString = [ |
| if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};', |
| ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'), |
| executable.toFilePath(), |
| ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), |
| if (printWorkingDir) ')', |
| ].join(' '); |
| logger?.config('Running `$commandString`.'); |
| |
| final stdoutBuffer = StringBuffer(); |
| final stderrBuffer = StringBuffer(); |
| final process = await Process.start( |
| executable.toFilePath(), |
| arguments, |
| workingDirectory: workingDirectory?.toFilePath(), |
| environment: environment, |
| includeParentEnvironment: includeParentEnvironment, |
| runInShell: Platform.isWindows && !includeParentEnvironment, |
| ); |
| |
| final stdoutSub = process.stdout |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .listen(captureOutput |
| ? (s) { |
| logger?.fine(s); |
| stdoutBuffer.writeln(s); |
| } |
| : logger?.fine); |
| final stderrSub = process.stderr |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .listen(captureOutput |
| ? (s) { |
| logger?.config(s); |
| stderrBuffer.writeln(s); |
| } |
| : logger?.config); |
| |
| final (exitCode, _, _) = await ( |
| process.exitCode, |
| stdoutSub.asFuture<void>(), |
| stderrSub.asFuture<void>() |
| ).wait; |
| final result = RunProcessResult( |
| pid: process.pid, |
| command: commandString, |
| exitCode: exitCode, |
| stdout: stdoutBuffer.toString(), |
| stderr: stderrBuffer.toString(), |
| ); |
| if (throwOnUnexpectedExitCode && expectedExitCode != exitCode) { |
| throw ProcessException( |
| executable.toFilePath(), |
| arguments, |
| "Full command string: '$commandString'.\n" |
| "Exit code: '$exitCode'.\n" |
| 'For the output of the process check the logger output.', |
| ); |
| } |
| return result; |
| } |
| |
| /// Drop in replacement of [ProcessResult]. |
| class RunProcessResult { |
| final int pid; |
| |
| final String command; |
| |
| final int exitCode; |
| |
| final String stderr; |
| |
| final String stdout; |
| |
| RunProcessResult({ |
| required this.pid, |
| required this.command, |
| required this.exitCode, |
| required this.stderr, |
| required this.stdout, |
| }); |
| |
| @override |
| String toString() => '''command: $command |
| exitCode: $exitCode |
| stdout: $stdout |
| stderr: $stderr'''; |
| } |