//! The supervision abstraction. //! //! A `Supervisor` owns the lifecycle of one game instance. Different games are //! managed in fundamentally different ways — Rust/Conan/Soulmask are spawned OS //! processes ([`crate::process::ProcessSupervisor`]); Dune is a docker-compose //! stack ([`crate::docker_compose::DockerComposeSupervisor`]); future planes //! (kubectl, AMP/podman, SSH) will be their own impls. The instance command //! dispatch (`instancecmd::dispatch`) talks only to this trait, so it never //! learns which management model is behind a given instance. //! //! Trait objects (`Arc`) need object-safe, dynamically //! dispatchable async methods; native `async fn` in traits is not yet //! dyn-compatible, so we use `#[async_trait]` (the battle-tested ecosystem //! standard) to box the returned futures. The cost — one heap alloc per //! lifecycle call — is irrelevant for start/stop/restart, which happen seconds //! to minutes apart. use std::sync::Arc; use anyhow::Result; use serde::Serialize; use tokio::sync::watch; /// Observable lifecycle state of one instance. Shared vocabulary across every /// supervisor impl; serialized verbatim into heartbeats and status events /// (`{"state":"running", ...}`). #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "snake_case", tag = "state")] pub enum InstanceState { /// Not lifecycle-managed (a process instance with no executable, etc.). Unmanaged, Stopped, Starting, Running, Stopping, /// Exited/died without a stop request. Crashed { #[serde(skip_serializing_if = "Option::is_none")] exit_code: Option, }, } impl InstanceState { pub fn as_label(&self) -> &'static str { match self { InstanceState::Unmanaged => "unmanaged", InstanceState::Stopped => "stopped", InstanceState::Starting => "starting", InstanceState::Running => "running", InstanceState::Stopping => "stopping", InstanceState::Crashed { .. } => "crashed", } } } /// Lifecycle control + state observation for one instance. /// /// `start`/`stop`/`restart` take `self: Arc` so an impl can hand a clone /// to a spawned monitor task; callers hold an `Arc` and /// `clone()` before each call. `watch_state` exposes the same channel the /// status-event publisher drains, so panel push events stay decoupled from the /// heartbeat cadence. #[async_trait::async_trait] pub trait Supervisor: Send + Sync { /// The instance slug (a NATS subject segment). fn instance_id(&self) -> &str; /// Current cached state (cheap; no I/O). fn state(&self) -> InstanceState; /// Subscribe to state transitions. fn watch_state(&self) -> watch::Receiver; /// Seconds since the instance entered `Running` (0 otherwise). async fn uptime_seconds(&self) -> u64; async fn start(self: Arc) -> Result<()>; async fn stop(self: Arc) -> Result<()>; async fn restart(self: Arc) -> Result<()>; }