module

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

clear_current_fiber_redirect
Source
current_io
Source
exit_quietly_if_broken_pipe(ex : IO::Error) : Nil

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.

Source
normalize_for_display(text : String) : String

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).

Source
normalize_for_display(object : T) : T forall T
Source
redirect_current_fiber_to(io : IO) : Nil
Source