Script.capture constructor
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).
-
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 thisscript
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);
});
}