use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; use hive_sh4re::{HostRequest, HostResponse}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use crate::actions; use crate::coordinator::Coordinator; use crate::lifecycle; pub async fn serve(socket: &Path, coord: Arc) -> Result<()> { // Prefer a socket passed by systemd socket-activation (LISTEN_FDS). // When running under a `.socket` unit, systemd has already created, // bound, and chmod-ed the socket for us — we just accept on it. // Fall back to the traditional bind path when not socket-activated // (direct invocation, dev, tests). let listener = { let mut listenfd = listenfd::ListenFd::from_env(); if let Some(std_listener) = listenfd .take_unix_listener(0) .context("take socket-activated unix listener")? { std_listener.set_nonblocking(true)?; UnixListener::from_std(std_listener) .context("convert socket-activated listener to tokio")? } else { // Standalone: create parent dir, remove any stale socket, bind. if let Some(parent) = socket.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("create socket parent {}", parent.display()))?; } if socket.exists() { std::fs::remove_file(socket).context("remove stale socket")?; } UnixListener::bind(socket) .with_context(|| format!("bind admin socket {}", socket.display()))? } }; tracing::info!(socket = %socket.display(), hyperhive_flake = %coord.hyperhive_flake, "hive-c0re admin listening"); loop { let (stream, _) = listener.accept().await.context("accept connection")?; let coord = coord.clone(); tokio::spawn(async move { if let Err(e) = handle(stream, coord).await { tracing::warn!(error = ?e, "connection failed"); } }); } } async fn handle(stream: UnixStream, coord: Arc) -> Result<()> { let (read, mut write) = stream.into_split(); let mut reader = BufReader::new(read); let mut line = String::new(); loop { line.clear(); let n = reader.read_line(&mut line).await?; if n == 0 { return Ok(()); } let resp = match serde_json::from_str::(line.trim()) { Ok(req) => dispatch(&req, coord.clone()).await, Err(e) => HostResponse::error(format!("parse error: {e}")), }; let mut payload = serde_json::to_string(&resp)?; payload.push('\n'); write.write_all(payload.as_bytes()).await?; write.flush().await?; } } async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { let result: anyhow::Result = async { Ok(match req { HostRequest::Spawn { name } => handle_spawn(&coord, name).await?, HostRequest::RequestSpawn { name } => { tracing::info!(%name, "request_spawn"); let id = coord .approvals .submit_kind(name, hive_sh4re::ApprovalKind::Spawn, "", None)?; tracing::info!(%id, %name, "spawn approval queued"); HostResponse::success() } HostRequest::Kill { name } => handle_kill(&coord, name).await?, HostRequest::Restart { name } => { tracing::info!(%name, "restart"); lifecycle::restart(name).await?; HostResponse::success() } HostRequest::RestartAll => handle_restart_all().await?, HostRequest::Destroy { name, purge } => { actions::destroy(&coord, name, *purge).await?; HostResponse::success() } HostRequest::Rebuild { name } => handle_rebuild(&coord, name).await?, HostRequest::List => HostResponse::list(lifecycle::list().await?), HostRequest::Pending => HostResponse::pending(coord.approvals.pending()?), HostRequest::Approve { id } => { actions::approve(coord.clone(), *id).await?; HostResponse::success() } HostRequest::Deny { id } => { actions::deny(&coord, *id, None).await?; HostResponse::success() } HostRequest::SetParent { child, new_parent } => { tracing::info!(%child, ?new_parent, "set_parent"); // `reparent_with_notify` wraps `topology::set_parent` // with the three notification messages + the // ContainerView rescan. Idempotent same-parent calls // skip both the messages and the disk write per the // topology fast-path. coord .reparent_with_notify(child, new_parent.as_deref()) .await .map_err(anyhow::Error::msg)?; HostResponse::success() } }) } .await; match result { Ok(r) => r, Err(e) => HostResponse::error(format!("{e:#}")), } } /// Create + start the container for `name`, rolling back socket /// registration and notifying the manager on failure. async fn handle_spawn(coord: &Arc, name: &str) -> Result { tracing::info!(%name, "spawn"); let agent_dir = coord.ensure_runtime(name)?; let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); match lifecycle::spawn(name, &hive, &paths).await { Ok(()) => { coord.notify_manager(&hive_sh4re::HelperEvent::Spawned { agent: name.to_owned(), ok: true, note: None, sha: None, }); } Err(e) => { // Roll back socket registration if container creation failed. coord.unregister_agent(name); coord.notify_manager(&hive_sh4re::HelperEvent::Spawned { agent: name.to_owned(), ok: false, note: Some(format!("{e:#}")), sha: None, }); return Err(e); } } Ok(HostResponse::success()) } /// Kill `name`'s container, unregister its socket, notify the manager. async fn handle_kill(coord: &Arc, name: &str) -> Result { tracing::info!(%name, "kill"); lifecycle::kill(name).await?; coord.unregister_agent(name); coord.notify_manager(&hive_sh4re::HelperEvent::Killed { agent: name.to_owned(), }); Ok(HostResponse::success()) } /// Restart every container, aggregating per-agent failures into one /// response rather than aborting on the first error. async fn handle_restart_all() -> Result { tracing::info!("restart-all"); let agents = lifecycle::list().await?; let mut ok_agents: Vec = Vec::new(); let mut errors: Vec = Vec::new(); for agent in &agents { if let Err(e) = lifecycle::restart(agent).await { tracing::warn!(%agent, error = ?e, "restart-all: failed to restart agent"); errors.push(format!("{agent}: {e:#}")); } else { ok_agents.push(agent.clone()); } } if errors.is_empty() { Ok(HostResponse::list(ok_agents)) } else { Ok(HostResponse { ok: false, error: Some(errors.join("; ")), agents: Some(ok_agents), approvals: None, }) } } /// Rebuild `name`'s container, notifying the manager of the outcome /// (success or failure) and kicking the agent's next turn on success. async fn handle_rebuild(coord: &Arc, name: &str) -> Result { tracing::info!(%name, "rebuild"); let agent_dir = coord.ensure_runtime(name)?; let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); let result = lifecycle::rebuild(name, &hive, &paths, &|_| (), &|_| ()).await; // Mirror auto_update::rebuild_agent — the manager wants to know // about every rebuild attempt regardless of which surface triggered // it, especially failures (build error → manager can adjust the // agent's agent.nix). Without this the admin-socket CLI was a // notify-gap. match &result { Ok(()) => { coord.notify_manager(&hive_sh4re::HelperEvent::Rebuilt { agent: name.to_owned(), ok: true, note: None, sha: None, tag: None, }); // Wake the agent's next turn with the "you were rebuilt" // hint. Same pattern as auto_update::rebuild_agent and the // dashboard rebuild path — this is the CLI's equivalent. coord.kick_agent(name, "container rebuilt"); } Err(e) => coord.notify_manager(&hive_sh4re::HelperEvent::Rebuilt { agent: name.to_owned(), ok: false, note: Some(format!("{e:#}")), sha: None, tag: None, }), } result?; Ok(HostResponse::success()) }