Skip to main content

omni_dev/daemon/
service.rs

1//! The [`DaemonService`] abstraction: a pluggable unit of work hosted by the
2//! daemon supervisor.
3//!
4//! A service has a stable [`name`](DaemonService::name), answers operations
5//! routed to it over the control socket ([`handle`](DaemonService::handle)),
6//! contributes a tray submenu ([`menu`](DaemonService::menu) /
7//! [`menu_action`](DaemonService::menu_action)), reports structured status for
8//! `daemon status` ([`status`](DaemonService::status)), and participates in
9//! graceful shutdown ([`shutdown`](DaemonService::shutdown)). See ADR-0039.
10
11use anyhow::Result;
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16/// A long-lived unit of work supervised by the daemon.
17///
18/// Implementations are registered in a [`ServiceRegistry`](super::registry::ServiceRegistry)
19/// and are assumed live from registration until [`shutdown`](Self::shutdown).
20#[async_trait]
21pub trait DaemonService: Send + Sync {
22    /// Stable identifier used to route control-socket envelopes to this service
23    /// (the envelope's `service` field) and to label its status/menu.
24    fn name(&self) -> &'static str;
25
26    /// Handles an operation routed to this service, returning a JSON payload.
27    async fn handle(&self, op: &str, payload: Value) -> Result<Value>;
28
29    /// Opens a **push subscription** for a streaming op, or returns `None` when
30    /// `op` is not one this service streams (the default for every service).
31    ///
32    /// When the server sees a `Some`, it switches that connection to streaming
33    /// mode ([`server::run_stream`](super::server)): it sends the stream's
34    /// initial [`snapshot`](ServiceStream::snapshot), then pushes a fresh
35    /// snapshot each time [`ServiceStream::changed`] (or the server's own
36    /// periodic tick) wakes it and the payload differs from the last one sent,
37    /// until the client disconnects or the daemon shuts down. A `None` op falls
38    /// through to the normal request→one-reply [`handle`](Self::handle) path, so
39    /// the request/reply contract is unchanged for every existing service and op
40    /// (#1267).
41    ///
42    /// Kept synchronous and cheap: building a stream should only clone the
43    /// handles it needs. `payload` is borrowed so the non-streaming path retains
44    /// ownership for [`handle`](Self::handle).
45    fn subscribe(&self, _op: &str, _payload: &Value) -> Option<Box<dyn ServiceStream>> {
46        None
47    }
48
49    /// Cheap snapshot of the service's tray submenu, polled by the menu-bar
50    /// shell. Must not block.
51    fn menu(&self) -> MenuSnapshot;
52
53    /// Performs a tray menu action previously surfaced by [`menu`](Self::menu),
54    /// identified by its [`MenuAction::id`].
55    async fn menu_action(&self, action_id: &str) -> Result<()>;
56
57    /// Structured status for `daemon status` aggregation.
58    async fn status(&self) -> ServiceStatus;
59
60    /// Gracefully stops the service, draining in-flight work. Called once on
61    /// daemon shutdown.
62    async fn shutdown(&self);
63}
64
65/// A live push stream a service exposes for a subscription op.
66///
67/// See [`DaemonService::subscribe`]. The server owns the drive loop: it awaits
68/// [`changed`](Self::changed) (alongside its own periodic tick, so purely
69/// on-disk state changes are still caught), then calls
70/// [`snapshot`](Self::snapshot) and pushes the payload only when it differs from
71/// the last one sent — so the implementation never has to schedule, diff, or
72/// write anything itself (#1267).
73#[async_trait]
74pub trait ServiceStream: Send {
75    /// Resolves when the service's visible state *may* have changed since the
76    /// previous call. Collapsing a burst of changes into one wakeup (coalescing)
77    /// is the implementation's job; a spurious wakeup is harmless because the
78    /// server diffs the resulting snapshot. It must **not** resolve in a tight
79    /// loop when there is nothing to report (e.g. once the change source is gone,
80    /// park rather than return repeatedly), or it would spin the server's
81    /// `select!`.
82    async fn changed(&mut self);
83
84    /// The current snapshot payload, sent verbatim as
85    /// [`DaemonReply::ok`](super::protocol::DaemonReply::ok). May perform
86    /// blocking work (offloaded to a blocking thread); the server awaits it
87    /// between wakeups.
88    async fn snapshot(&self) -> Value;
89}
90
91/// Structured per-service status, aggregated by the built-in `status` op and
92/// surfaced by `omni-dev daemon status`.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ServiceStatus {
95    /// The reporting service's [`name`](DaemonService::name).
96    pub name: String,
97    /// Whether the service is currently operating normally.
98    pub healthy: bool,
99    /// One-line human-readable summary (e.g. `"1 tab connected"`).
100    pub summary: String,
101    /// Service-specific structured detail; `null` when there is none.
102    #[serde(default, skip_serializing_if = "Value::is_null")]
103    pub detail: Value,
104}
105
106/// A cheap snapshot of a service's tray submenu, rebuilt on each poll.
107#[derive(Debug, Clone, Default)]
108pub struct MenuSnapshot {
109    /// Submenu title (the parent menu-bar entry label).
110    pub title: String,
111    /// Ordered submenu entries.
112    pub items: Vec<MenuItem>,
113}
114
115/// A single entry in a [`MenuSnapshot`].
116#[derive(Debug, Clone)]
117pub enum MenuItem {
118    /// A non-interactive status line.
119    Label(String),
120    /// A horizontal separator.
121    Separator,
122    /// A clickable action dispatched via [`DaemonService::menu_action`].
123    Action(MenuAction),
124}
125
126/// A clickable tray action.
127#[derive(Debug, Clone)]
128pub struct MenuAction {
129    /// Stable identifier passed back to [`DaemonService::menu_action`].
130    pub id: String,
131    /// Human-readable menu label.
132    pub label: String,
133    /// Whether the action is currently selectable.
134    pub enabled: bool,
135}