exec method

  1. @override
Future<Result<ShellExecResult, ExecutionError>> exec(
  1. String command, {
  2. ShellExecOptions? options,
})
override

Executes a shell command. Must never throw: all failures are encoded in the returned Result.

Implementation

@override
Future<Result<ShellExecResult, ExecutionError>> exec(
  String command, {
  ShellExecOptions? options,
}) async {
  final token = options?.cancelToken;
  if (token?.isCancelled ?? false) {
    return const Err(ExecutionError(ExecutionErrorCode.aborted, 'aborted'));
  }
  final executable = Platform.isWindows ? 'cmd' : 'sh';
  final args = Platform.isWindows ? ['/c', command] : ['-c', command];
  Process process;
  try {
    process = await Process.start(
      executable,
      args,
      workingDirectory: options?.cwd,
      environment: options?.env,
    );
  } on Object catch (error) {
    return Err(
      ExecutionError(
        ExecutionErrorCode.spawnError,
        error.toString(),
        cause: error,
      ),
    );
  }

  final stdout = StringBuffer();
  final stderr = StringBuffer();
  ExecutionError? callbackError;
  void collect(
    StringBuffer target,
    String chunk,
    void Function(String)? callback,
  ) {
    target.write(chunk);
    if (callback == null) return;
    try {
      callback(chunk);
    } on Object catch (error) {
      callbackError = ExecutionError(
        ExecutionErrorCode.callbackError,
        error.toString(),
        cause: error,
      );
      process.kill();
    }
  }

  final stdoutDone = process.stdout
      .transform(utf8.decoder)
      .forEach((chunk) => collect(stdout, chunk, options?.onStdout));
  final stderrDone = process.stderr
      .transform(utf8.decoder)
      .forEach((chunk) => collect(stderr, chunk, options?.onStderr));

  Timer? timer;
  var timedOut = false;
  final timeout = options?.timeout;
  if (timeout != null) {
    timer = Timer(timeout, () {
      timedOut = true;
      process.kill();
    });
  }
  void onCancel(_) {
    process.kill();
  }

  token?.onCancel.then(onCancel);

  final exitCode = await process.exitCode;
  timer?.cancel();
  await Future.wait([stdoutDone, stderrDone]);

  if (callbackError != null) return Err(callbackError!);
  if (timedOut) {
    return Err(
      ExecutionError(ExecutionErrorCode.timeout, 'timeout: $timeout'),
    );
  }
  if (token?.isCancelled ?? false) {
    return const Err(ExecutionError(ExecutionErrorCode.aborted, 'aborted'));
  }
  return Ok(
    ShellExecResult(
      stdout: stdout.toString(),
      stderr: stderr.toString(),
      exitCode: exitCode,
    ),
  );
}