runExecutableArguments function

Future<ProcessResult> runExecutableArguments(
  1. String executable,
  2. List<String> arguments, {
  3. String? workingDirectory,
  4. Map<String, String>? environment,
  5. bool includeParentEnvironment = true,
  6. bool? runInShell,
  7. Encoding? stdoutEncoding = systemEncoding,
  8. Encoding? stderrEncoding = systemEncoding,
  9. Stream<List<int>>? stdin,
  10. StreamSink<List<int>>? stdout,
  11. StreamSink<List<int>>? stderr,
  12. bool? verbose,
  13. bool? commandVerbose,
  14. bool? noStdoutResult,
  15. bool? noStderrResult,
  16. void onProcess(
    1. Process process
    )?,
})

if commandVerbose or verbose is true, display the command. if verbose is true, stream stdout & stdin

Optional onProcess(process) is called to allow killing the process.

If noStdoutResult is true, the result will not contain the stdout. If noStderrResult is true, the result will not contain the stderr.

Don't mess-up with the input and output for now here. only use it for kill.

Implementation

Future<ProcessResult> runExecutableArguments(
    String executable, List<String> arguments,
    {String? workingDirectory,
    Map<String, String>? environment,
    bool includeParentEnvironment = true,
    bool? runInShell,
    Encoding? stdoutEncoding = systemEncoding,
    Encoding? stderrEncoding = systemEncoding,
    Stream<List<int>>? stdin,
    StreamSink<List<int>>? stdout,
    StreamSink<List<int>>? stderr,
    bool? verbose,
    bool? commandVerbose,
    bool? noStdoutResult,
    bool? noStderrResult,
    void Function(Process process)? onProcess}) async {
  if (verbose == true) {
    commandVerbose = true;
    stdout ??= io.stdout;
    stderr ??= io.stderr;
  }

  if (commandVerbose == true) {
    utils.streamSinkWriteln(stdout ?? io.stdout,
        '\$ ${executableArgumentsToString(executable, arguments)}',
        encoding: stdoutEncoding);
  }

  // Build our environment
  var shellEnvironment = ShellEnvironment.full(
      environment: environment,
      includeParentEnvironment: includeParentEnvironment);

  // Default is the full command
  var executableShortName = executable;

  // Find executable if needed, i.e. if it is only a name
  if (basename(executable) == executable) {
    // Try to find it in path or use it as is
    executable = utils.findExecutableSync(executable, shellEnvironment.paths) ??
        executable;
  } else {
    // resolve locally
    executable = utils.findExecutableSync(basename(executable), [
          join(workingDirectory ?? Directory.current.path, dirname(executable))
        ]) ??
        executable;
  }

  // Fix runInShell on windows (force run in shell for non-.exe)
  runInShell = utils.fixRunInShell(runInShell, executable);

  Process process;
  try {
    process = await Process.start(executable, arguments,
        workingDirectory: workingDirectory,
        environment: shellEnvironment,
        includeParentEnvironment: false,
        runInShell: runInShell);
    if (shellDebug) {
      print('process: ${process.pid}');
    }
    if (onProcess != null) {
      onProcess(process);
    }
    if (shellDebug) {
      // ignore: unawaited_futures
      () async {
        try {
          var exitCode = await process.exitCode;
          print('process: ${process.pid} exitCode $exitCode');
        } catch (e) {
          print('process: ${process.pid} Error $e waiting exit code');
        }
      }();
    }
  } catch (e) {
    if (verbose == true) {
      io.stderr.writeln(e);
      io.stderr.writeln(
          '\$ ${executableArgumentsToString(executableShortName, arguments)}');
      io.stderr.writeln(
          'workingDirectory: ${workingDirectory ?? Directory.current.path}');
    }
    rethrow;
  }

  final outCtlr = StreamController<List<int>>(sync: true);
  final errCtlr = StreamController<List<int>>(sync: true);

  // Connected stdin
  // Buggy!
  StreamSubscription? stdinSubscription;
  if (stdin != null) {
    //stdin.pipe(process.stdin); // this closes the stream...
    stdinSubscription = stdin.listen((List<int> data) {
      process.stdin.add(data);
    })
      ..onDone(() {
        process.stdin.close();
      });
    // OLD 2: process.stdin.addStream(stdin);
  } else {
    // Close the input sync, we want this not interractive
    //process.stdin.close();
  }

  Future<dynamic> streamToResult(
      Stream<List<int>> stream, Encoding? encoding) async {
    final list = <int>[];
    await for (final data in stream) {
      //devPrint('s: ${data}');
      list.addAll(data);
    }
    if (encoding != null) {
      return encoding.decode(list);
    }
    return list;
  }

  var out = (noStdoutResult ?? false)
      ? Future.value(null)
      : streamToResult(outCtlr.stream, stdoutEncoding);
  var err = (noStderrResult ?? false)
      ? Future.value(null)
      : streamToResult(errCtlr.stream, stderrEncoding);

  process.stdout.listen((List<int> d) {
    if (stdout != null) {
      stdout.add(d);
    }
    outCtlr.add(d);
  }, onDone: () {
    outCtlr.close();
  });

  process.stderr.listen((List<int> d) async {
    if (stderr != null) {
      stderr.add(d);
    }
    errCtlr.add(d);
  }, onDone: () {
    errCtlr.close();
  });

  final exitCode = await process.exitCode;

  /// Cancel input sink
  if (stdinSubscription != null) {
    await stdinSubscription.cancel();
  }

  // Notice that exitCode can complete before all of the lines of output have been
  // processed. Also note that we do not explicitly close the process. In order
  // to not leak resources we have to drain both the stderr and the stdout streams.
  // To do that we set a listener (using await for) to drain the stderr stream.
  //await process.stdout.drain();
  //await process.stderr.drain();

  final result = ProcessResult(process.pid, exitCode, await out, await err);

  if (stdin != null) {
    //process.stdin.close();
  }

  // flush for consistency
  if (stdout == io.stdout) {
    await io.stdout.safeFlush();
  }
  if (stderr == io.stderr) {
    await io.stderr.safeFlush();
  }

  return result;
}