Script.capture constructor

Script.capture(
  1. FutureOr<void> callback(
    1. Stream<List<int>> stdin
    ), {
  2. String? name,
  3. bool onSignal(
    1. ProcessSignal signal
    )?,
})

Runs callback and captures the output of Scripts created within it.

As much as possible, callback is treated as its own isolated process. In particular:

  • The stdout and stderr streams of any child Scripts are forwarded to this script's stdout and stderr, respectively, unless the getters are accessed within the span of a macrotask after being created (that is, before a Timer.run event fires).

  • Calls to print are also forwarded to stdout.

  • Any unhandled ScriptExceptions within callback, such as from child Scripts failing unexpectedly, cause this Script to fail with the same exit code.

  • Any unhandled Dart exceptions within callback cause this Script to print the error message and stack trace to stderr and exit with code 257.

  • Any errors within callback that go unhandled after this script has already exited are silently ignored, as is any additional output.

  • Creating a Script within callback after this Script has exited will throw a StateError (which will then be silently ignored unless otherwise handled).

The returned Script exits automatically as soon as the Future returned by callback has completed and all child Scripts created within callback have completed. It will exit early if an error goes unhandled, including from a child Script failing unexpectedly.

The name is a human-readable name for the script, used for debugging and error messages. It defaults to "capture".

The callback can't be interrupted by calling kill, but the onSignal callback allows capturing those signals so the callback may react appropriately. When no onSignal handler was set, calling kill will do nothing and return false.

Implementation

factory Script.capture(
    FutureOr<void> Function(Stream<List<int>> stdin) callback,
    {String? name,
    bool onSignal(ProcessSignal signal)?}) {
  _checkCapture();

  var scriptName = name ?? "capture";
  var childScripts = FutureGroup<void>();
  var stdinController = StreamController<List<int>>();
  var groups = StdioGroup.entangled();
  var stdoutGroup = groups.item1;
  var stderrGroup = groups.item2;
  var exitCodeCompleter = Completer<int>();

  runZonedGuarded(
      () async {
        if (onSignal != null) {
          onSignal = Zone.current.bindUnaryCallback(onSignal!);
        }

        await callback(stdinController.stream);

        // Once there are no child scripts still spawning or running, mark
        // this script as done.
        void checkIdle() {
          if (childScripts.isIdle && !exitCodeCompleter.isCompleted) {
            stdoutGroup.close();
            stderrGroup.close();
            childScripts.close();
            exitCodeCompleter.complete(0);
          }
        }

        checkIdle();
        childScripts.onIdle.listen((_) => Timer.run(checkIdle));
      },
      (error, stackTrace) {
        if (!exitCodeCompleter.isCompleted) {
          stdoutGroup.close();
          stderrGroup.close();
          childScripts.close();
          exitCodeCompleter.completeError(error, stackTrace);
        }
      },
      zoneValues: {
        #_childScripts: childScripts,
        scriptNameKey: scriptName,
        stdoutKey: stdoutGroup,
        stderrKey: stderrGroup
      },
      zoneSpecification: ZoneSpecification(print: (_, parent, zone, line) {
        if (!exitCodeCompleter.isCompleted) stdoutGroup.writeln(line);
      }));

  return Script._(scriptName, stdinController.sink, stdoutGroup.stream,
      stderrGroup.stream, exitCodeCompleter.future, (signal) {
    if (onSignal == null) return false;
    return onSignal!(signal);
  });
}