module

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.cr serves it on the plugin side; --daemon on 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.cr one-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_user daemon is a separate keyed daemon spawned through the same sudo escalation, with the same sudo -n vs probed sudo -S -p '' choice and the password line written into the stdin pipe ahead of any frame (reusing the approach SSHManager.spawn_daemon documented 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

clear_daemon_for_spec(host_name : String, become_user : String | Nil) : Nil
Source
close_all_daemons

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.

Source
daemon_process_for_spec(host_name : String, become_user : String | Nil) : Process | Nil
Source
daemon_unavailable?(host_name : String, become_user : String | Nil) : Bool

Whether a daemon for this key is worth attempting at all. Public so PluginManager can skip the attempt before resolving a plugin path.

Source
local_daemon_argv(plugin_path : String, become_user : String | Nil, interactive : Bool) : Array(String)

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.

Source
seed_daemon_process_for_spec(host_name : String, become_user : String | Nil, process : Process) : Nil

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.

Source
send(host_name : String, module_name : String, config : JSON::Any, plugin_path : String, become_user : String | Nil = nil, become_password : String | Nil = nil, timeout : Int32 = SSHManager::DEFAULT_EXEC_TIMEOUT_SECONDS) : JSON::Any

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.

Source

Nested types