diff --git a/shorebird/ci/shard_runner/lib/gcs.dart b/shorebird/ci/shard_runner/lib/gcs.dart index c4263658bc275..18c622c1481e3 100644 --- a/shorebird/ci/shard_runner/lib/gcs.dart +++ b/shorebird/ci/shard_runner/lib/gcs.dart @@ -72,17 +72,16 @@ Future downloadFromStaging({ print('[GCS] Downloading from $stagingRoot'); - // List files in the staging location - final ProcessResult lsResult = await runChecked( + // List files in the staging location. We capture stdout because we need + // to parse the file list; everything else uses runChecked to stream. + final String lsOutput = await runCapturingStdout( 'gsutil', ['ls', stagingRoot], description: 'gsutil ls $stagingRoot', ); - final List files = (lsResult.stdout as String) - .split('\n') - .where((String f) => f.endsWith('.tar.gz')) - .toList(); + final List files = + lsOutput.split('\n').where((String f) => f.endsWith('.tar.gz')).toList(); for (final String file in files) { final String fileName = p.basename(file); diff --git a/shorebird/ci/shard_runner/lib/process.dart b/shorebird/ci/shard_runner/lib/process.dart index 1fb251b609d97..5c3a0a6adaf22 100644 --- a/shorebird/ci/shard_runner/lib/process.dart +++ b/shorebird/ci/shard_runner/lib/process.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; /// Resolves an executable name for Windows, appending .cmd if needed. @@ -28,10 +30,13 @@ String _resolveExecutable(String executable) { return executable; } -/// Runs a process and throws if it exits with a non-zero code. +/// Runs a process, streaming stdout/stderr to the parent's stdio so the +/// caller (and CI logs) see output live. Throws if the process exits +/// non-zero. /// -/// Returns the [ProcessResult] for callers that need stdout/stderr. -Future runChecked( +/// Use this for everything except the rare case where you need to parse +/// the child's stdout — for that, see [runCapturingStdout]. +Future runChecked( String executable, List arguments, { String? workingDirectory, @@ -40,27 +45,59 @@ Future runChecked( }) async { final String resolvedExecutable = _resolveExecutable(executable); - final ProcessResult result = await Process.run( + final Process process = await Process.start( resolvedExecutable, arguments, workingDirectory: workingDirectory, environment: environment, + mode: ProcessStartMode.inheritStdio, ); - if (result.exitCode != 0) { + final int exitCode = await process.exitCode; + if (exitCode != 0) { final String desc = description ?? '$executable ${arguments.join(' ')}'; - final String stderr = (result.stderr as String).trim(); - final String stdout = (result.stdout as String).trim(); - final StringBuffer message = - StringBuffer('$desc failed (exit ${result.exitCode})'); - if (stdout.isNotEmpty) { - message.write('\nSTDOUT: $stdout'); - } - if (stderr.isNotEmpty) { - message.write('\nSTDERR: $stderr'); - } - throw Exception(message.toString()); + throw Exception('$desc failed (exit $exitCode)'); } +} + +/// Runs a process, capturing stdout into the returned string while still +/// streaming stderr to the parent's stderr. Throws if the process exits +/// non-zero. +/// +/// Use this when you need to parse the child's stdout (e.g. `gsutil ls`). +/// For everything else use [runChecked] so output reaches the CI log live. +Future runCapturingStdout( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + String? description, +}) async { + final String resolvedExecutable = _resolveExecutable(executable); - return result; + final Process process = await Process.start( + resolvedExecutable, + arguments, + workingDirectory: workingDirectory, + environment: environment, + ); + + final Future stdoutFuture = + process.stdout.transform(utf8.decoder).join(); + final Future stderrFuture = + process.stderr.transform(utf8.decoder).forEach(stderr.write); + + final List results = await Future.wait(>[ + stdoutFuture, + stderrFuture, + process.exitCode, + ]); + final String capturedStdout = results[0] as String; + final int exitCode = results[2] as int; + + if (exitCode != 0) { + final String desc = description ?? '$executable ${arguments.join(' ')}'; + throw Exception('$desc failed (exit $exitCode)'); + } + return capturedStdout; }