blob: c33a86db385d920f7bfe02dde509cd82c8be4639 [file] [log] [blame]
// 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',
];
final newEnvironment = {
...{
for (final winEnvKey in winEnvKeys)
winEnvKey: Platform.environment[winEnvKey]!,
},
if (environment != null) ...environment,
};
environment = newEnvironment;
}
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?.info('Running `$commandString`.');
final stdoutBuffer = StringBuffer();
final stderrBuffer = StringBuffer();
final stdoutCompleter = Completer<Object?>();
final stderrCompleter = Completer<Object?>();
final process = await Process.start(
executable.toFilePath(),
arguments,
workingDirectory: workingDirectory?.toFilePath(),
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: Platform.isWindows && !includeParentEnvironment,
);
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
(s) {
logger?.fine(s);
if (captureOutput) stdoutBuffer.writeln(s);
},
onDone: stdoutCompleter.complete,
);
process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
(s) {
logger?.severe(s);
if (captureOutput) stderrBuffer.writeln(s);
},
onDone: stderrCompleter.complete,
);
final exitCode = await process.exitCode;
await stdoutCompleter.future;
await stderrCompleter.future;
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''';
}