Krikri::LocalPluginDaemon
The LOCAL half of the persistent plugin daemon - the exact analogue of
SSHManager's daemon half for ansible_connection=local tasks, where
the "transport" being optimized away is not an ssh client fork but the
fat plugin binary's own ~13 ms exec floor (14.8 MB static, measured in
~/scratch/perf-profile-report.md: 30 one-shot local execs were 80% of a
737 ms local-probe wall).
Everything here mirrors the ssh daemon's design deliberately, point for
point - see ssh_manager.cr's daemon block and plugin_daemon.cr for
the rationale of each piece; only the mechanics of "spawn the resident
process" differ (a direct child of this controller process instead of a
remote command over an ssh session):
- Same framed stdin/stdout wire protocol (
plugin_daemon.crserves it on the plugin side;--daemonon the fat binary). - Same per-connection keying and one-fiber-per-host safety argument:
a local daemon is keyed (host_name, become_user) and only ever
touched by the single fiber that runs that host's tasks - the same
executor.crone-fiber-per-host model the ssh daemon's comment documents. Unlike ssh there is no (user, port) dimension: every local connection talks to this controller itself. - Same fallback contract: on any spawn/write/read failure the caller (PluginManager#try_local_daemon) falls through to the one-fork-per- task path for THAT task; consecutive failures per key eventually stop attempting the daemon at all (same MAX_DAEMON_FAILURES=3 threshold, shared from SSHManager); any success resets the count so a daemon that died once is lazily respawned.
- Same dispatch-vs-response-lost classification (the request WAS fully written, so the module may already have run inside the daemon and must NOT be re-executed over the fallback - a non-idempotent module would apply twice): a lost response on a daemon this call spawned itself is treated as "never came up" and stays fallback-safe, while one on an already-cached daemon raises SSHManager::DaemonDispatchUnknownError, which the caller turns into a failed task via the SAME daemon_dispatch_unknown_result it uses for the ssh daemon.
- Same become handling as the one-shot local path
(#execute_local_plugin_impl): a
become_userdaemon is a separate keyed daemon spawned through the samesudoescalation, with the samesudo -nvs probedsudo -S -p ''choice and the password line written into the stdin pipe ahead of any frame (reusing the approachSSHManager.spawn_daemondocumented for the remote side). - Same shutdown at run end:
close_all_daemons(called from krikri-playbook.cr right next to SSHManager's) closes each daemon's stdin - the EOF is the plugin-side clean-shutdown signal - then polls briefly and force-kills stragglers.
Per-request isolation is inherited from the module implementations themselves, exactly as on the ssh daemon: a request's environment: overlay, chdir: and umask are applied by the module code to its own SUBprocesses (Process.new env:/chdir:) or saved/restored around the operation (base_plugin.cr's owner-setting helpers, the openssl_* family) - never mutated on the plugin process itself, which is what makes one long-lived process safe to serve many tasks. The one process-level property the daemon changes vs the one-fork path is therefore only lifetime, not state.
Class methods
Graceful shutdown for every still-open local daemon, called once at
the end of a run right after SSHManager.close_all_daemons. Same
shape as the ssh side's: close every daemon's stdin first (each one
sees EOF and exits cleanly via plugin_daemon.cr's own rescue IO::EOFError), then poll for the whole batch with the same 1ms ->
20ms doubling backoff inside a 1s hard deadline before force-killing
stragglers. The backoff numbers matter for the same measured reason
the ssh side's comment records: a daemon sees EOF and exits in about
a millisecond, so a flat sleep would burn whole ticks of every run's
exit path.
sudo argv for the LOCAL daemon spawn - the same flags the one-shot
path's #local_sudo_argv builds (-n when passwordless, -S -p ''
when a password must be read off stdin), plus the trailing
--daemon that selects the resident mode. No shell is involved
(real argv array), so become_user needs no shell-escaping here, same
as the one-shot path. Public as a spec seam: pinned directly in the
unit tests.
Spec seams, mirroring SSHManager's: install/remove a Process as the cached daemon for a key so the failure-classification logic can be driven against fake processes with no real plugin binary involved.
Sends one request and returns the plugin's own JSON result, unwrapped - same contract as SSHManager.daemon_send (the daemon's response IS the plugin's real output; no exit-code arbitration exists on this transport). Failure classification is the ssh daemon's, split by HOW FAR the request got; see its comment for the full rationale - the short version is that the caller's fallback re-EXECUTES the module, so only a provably-undispatched failure may fall through.