Krikri::OutputRouting
Per-fiber stdout/print redirection, used only by TaskExecutor's --forks fan-out (run_task_for_hosts_in_parallel) to buffer each host's output separately, so concurrent hosts' lines never interleave - they're flushed in host order once every fiber for a given task has finished.
Safe with no lock: only one fiber ever executes Crystal code at any instant (cooperative scheduling - fibers only yield at explicit I/O waits), so this Hash's mutation and lookup can never actually race, even though several hosts' fibers are "concurrent" from the task executor's point of view. A fiber with no entry here writes straight to the real STDOUT, unchanged from today - this is the path every fiber outside of a --forks fan-out (and the main fiber itself) takes.
Class methods
Broken-pipe writes must surface as IO::Error for this rescue to
work. The entrypoints guarantee that (krikri-playbook.cr and
base_plugin.cr for the plugin binaries): Crystal 1.22 moved the
stdlib's default signal handlers (Signal::PIPE.ignore included)
inside Crystal::System::Signal.setup_default_handlers instead of
guaranteeing them before user code, AND made the stdio streams call
LibC.exit(141) themselves on an EPIPE write (IO::FileDescriptor's
exit_on_broken_pipe, set by from_stdio) - the latter is what made
krikri-playbook playbook.yml | head -2 / --help | head -1 /
quitting out of a pager die silently with 141 on 1.22 instead of
raising the catchable IO::Error that, with nothing rescuing it,
used to dump a full Crystal stack trace and exit 1.
ansible-playbook is silent here and exits 0 (verified against
ansible-playbook/ansible: --help | head -1 and
--version | head -1 both produce no stderr and PIPESTATUS[0]=0),
so match that. Note this is deliberately NOT fixed by restoring the
default SIGPIPE disposition (Signal::PIPE.reset): that would let
the kernel kill the whole process on ANY EPIPE, including a write
to a subprocess stdin that closed early, turning a currently
catchable error deep in the SSH/plugin paths into an abrupt death.
Scoping it to this program's own stdout writes keeps that behavior
unchanged.
Every output path in this codebase goes through the puts/print below (there is exactly one other STDOUT reference in the whole tree - current_io above), so this is the complete set of places a stdout EPIPE can originate.
Ansible's Display.display rewrites every CRLF in a message to a
bare LF before writing it ("Some environments, such as Azure
Pipelines, render \r as an additional \n" - ansible-core's own
comment for it), and display() is the one choke point every
banner, task line, result, warning and [ERROR]: block passes
through on its way to the terminal. krikri wrote such text
verbatim, so a message whose own PAYLOAD carries CRLF came out one
byte longer than real's: an unreachable report's ssh stderr
(Warning: Permanently added '[host]:2222' (ED25519) to the list of known hosts.\r\nuser@host: Permission denied (publickey, password).) rendered as \r\r\n on a tty against real's \r\n
(issue #975, live-verified vs ansible-core 2.21.6).