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