1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
//! The one place a client starts `trusty-analyze` on demand (#6350, ADR-0032).
//!
//! Why here, rather than in `trusty-analyze`: three crates dial that socket —
//! `trusty-review`'s report adapter, `trusty-analyze`'s own `deep` subcommand,
//! and `tctl`'s boot stage — and only `trusty-analyze` has a Cargo edge on
//! itself. `trusty-installer` deliberately has none (see `probe_http`'s note on
//! why it duplicates method-name literals rather than depending on a daemon),
//! and `trusty-review` has none either. So the description of HOW to start the
//! service — binary name, arguments, socket path, spawn budget — has to live in
//! the crate all three already depend on, next to
//! [`crate::daemon_socket_path`], which is where they already agree about
//! WHERE it answers.
//!
//! What: [`OnDemandAnalyze`], a small owned handle wrapping a
//! [`UdsServiceSupervisor`] in [`SupervisorConfig::with_detached`] mode. One
//! call — [`OnDemandAnalyze::ensure_running`] — returns the socket path with
//! something answering on it, spawning `trusty-analyze serve` only when nothing
//! is.
//!
//! **Two callers, one server, at two levels.** Within a process, the handle's
//! spawn gate serialises concurrent callers and the second one finds the socket
//! already serving. Across processes there is no shared gate, so the arbiter is
//! `bind_singleton_hardened` in the child: it takes over only a socket the
//! kernel proves nobody is serving, so the loser of a two-process race exits on
//! its bind and the winner serves both clients.
//!
//! **This module never fails open.** Every failure — no binary on `PATH`, a
//! spawn the OS refused, a socket that never appeared inside the budget —
//! reaches the caller as a [`SupervisorError`]. A client that wants to degrade
//! rather than abort makes that choice itself, visibly; nothing here decides it
//! silently on the caller's behalf.
//!
//! Test: `on_demand_tests.rs` — `analyze_spawn_spec_runs_a_bare_serve`,
//! `analyze_timeouts_leave_room_for_the_flush`,
//! `idle_timeout_parses_its_three_meanings`,
//! `external_mode_returns_the_socket_without_spawning`,
//! `the_default_handle_uses_the_shared_socket_path`. The real spawn, the idle
//! exit and the two-concurrent-callers race are proven against the built binary
//! in `trusty-analyze`'s `tests/on_demand.rs`, since only that crate has one.
use ;
use Duration;
use ;
/// Binary and member name of the analysis service.
pub const ANALYZE_SERVICE: &str = "trusty-analyze";
/// Environment variable that suppresses on-demand spawning.
///
/// Why: an operator running `trusty-analyze serve` in a terminal, or a
/// developer running one under a debugger, owns that process's lifecycle.
/// Setting this to exactly `1` makes every client dial whatever is there and
/// never start one of its own — the same opt-out `tctl`-managed services get
/// under ADR-0034 §1.
pub const ANALYZE_EXTERNAL_ENV: &str = "TRUSTY_ANALYZE_EXTERNAL";
/// Environment variable overriding the server's idle window, in seconds.
///
/// `0` disables idle exit, for an operator who wants a foreground server that
/// stays up. Read by the SERVER (`trusty-analyze serve`), not by clients — it is
/// declared here because [`analyze_idle_timeout`] is the single parser both a
/// client's documentation and the server's startup read.
pub const ANALYZE_IDLE_TIMEOUT_ENV: &str = "TRUSTY_ANALYZE_IDLE_TIMEOUT_SECS";
/// Default idle window before an on-demand analyze server exits.
///
/// Ten minutes, chosen from the 5–15 minute band the #6350 ruling set. The two
/// costs it balances: a cold start re-opens the facts redb and the SCIP overlay
/// store and re-warms per-index chunk caches, so a window shorter than an
/// operator's edit-run loop makes every `trusty-review report --analyze` pay
/// that again; and a resident process that nobody has spoken to for ten minutes
/// is exactly the thing ADR-0032 retired the launchd unit to stop having.
pub const DEFAULT_ANALYZE_IDLE_TIMEOUT: Duration = from_secs;
/// The analyze server's own shutdown budget.
///
/// Why this exists rather than a literal at the [`ServiceTimeouts`] call site:
/// that type's sourcing rule says `shutdown_flush` must be the supervised
/// binary's real budget, and `trusty-common` cannot import `trusty-analyze` to
/// read it. So the constant lives here and `trusty-analyze`'s
/// `analyze_flush_budget_matches_the_supervisor_contract` pins its own value
/// against it — the equality is checked, rather than assumed to have stayed
/// true.
///
/// 🔴 **What this actually bounds, precisely (#6601 review).** It is the child's
/// half of the `sigterm_patience > shutdown_flush` relation, and that relation
/// governs the ONE path on which this supervisor signals an analyze child: the
/// spawn-probe timeout in `UdsServiceSupervisor::ensure_running`. A child that
/// BOUND is detached (see [`SupervisorConfig::with_detached`], #6350) — it is
/// never entered in the supervisor's population, and every `terminate_child`
/// call site reads only that population or the doomed queue — so no reap path
/// can reach a serving analyze server, and this number says nothing about how
/// long one lives after SIGTERM.
///
/// It is therefore NOT the serve loop's shutdown drain. `trusty-analyze`'s
/// `service::rpc::serve_options` drains on
/// [`crate::shutdown::plannable_grace`], because the only bounded terminator of
/// a SERVING analyze process is the OS at logout or shutdown — `trusty-analyze
/// stop` sends SIGTERM, polls 5 s and then merely reports. Setting the drain to
/// this budget instead gave up the #6595 guarantee three seconds into a
/// multi-minute `analyze.review` while averting no SIGKILL at all.
///
/// Three seconds is right for what it does bound. The server holds no write
/// buffer — `redb` commits inside each handler before it answers, so a SIGTERM
/// discards nothing that was acked — and what a spawn-failure kill must leave
/// room for is signal delivery, the socket unlink and exit. Three leaves 2 s of
/// [`ANALYZE_SIGTERM_PATIENCE`]'s 5 s for exactly that.
pub const ANALYZE_SHUTDOWN_FLUSH: Duration = from_secs;
/// How long to wait for a freshly-spawned analyze server to accept.
///
/// The server opens two redb files before it binds. On a warm page cache that is
/// milliseconds; on a cold one, or a machine under load, it is not, and a budget
/// that expires produces a `SpawnTimeout` the caller reports as a failure while
/// the server binds successfully a moment later.
const ANALYZE_SPAWN_PROBE: Duration = from_secs;
/// SIGTERM-to-SIGKILL patience. Must strictly exceed [`ANALYZE_SHUTDOWN_FLUSH`].
///
/// The 2 s margin over the flush budget is what the child spends after its drain
/// ends: unlinking the socket, dropping its redb stores, and exiting. Raising it
/// costs every reap — `enforce_limits` waits this out per victim — so it is
/// sized to that margin rather than to the process grace window.
const ANALYZE_SIGTERM_PATIENCE: Duration = from_secs;
/// The analyze service's timing budget.
///
/// `const`, so the `sigterm_patience > shutdown_flush` relation is a build
/// error rather than a runtime panic — see [`ServiceTimeouts::new`].
pub const ANALYZE_TIMEOUTS: ServiceTimeouts = new;
/// Resolve the idle window a server should apply.
///
/// Why a parser rather than a bare `env::var`: the variable has three meanings —
/// unset is the default, `0` is "never exit", anything else is a second count —
/// and a caller that read it inline would have to re-derive all three. An
/// unparseable value is treated as unset rather than fatal: a typo in an
/// environment variable must not stop the service from starting.
///
/// What: `None` means never exit; `Some(d)` is the window.
/// Test: `idle_timeout_parses_its_three_meanings`.
/// Read [`ANALYZE_IDLE_TIMEOUT_ENV`] through [`analyze_idle_timeout`].
/// A handle that starts `trusty-analyze` when nothing is serving its socket.
///
/// Why an owned handle rather than a free function: the in-process half of the
/// "two callers, one server" guarantee is the supervisor's spawn gate, and a
/// free function would build a fresh supervisor — and a fresh gate — per call,
/// letting two concurrent callers in one process each spawn a server. Sharing
/// one handle (behind an `Arc` where the caller is concurrent) is what closes
/// that. It is deliberately NOT a process-wide singleton: this workspace keeps
/// no global state, and a handle costs one small struct.
///
/// What: `ensure_running` is the whole surface. The socket path is resolved once
/// at construction through [`crate::daemon_socket_path`], the same call the
/// server itself binds, so there is nothing for the two to disagree about.
///
/// Test: `the_default_handle_uses_the_shared_socket_path`,
/// `external_mode_returns_the_socket_without_spawning`.
/// The command that starts one analyze server on `socket`.
///
/// Why the socket is passed explicitly rather than left to the child's own
/// derivation: `OnDemandAnalyze::at` exists so a test can use a tempdir path,
/// and a child that derived the default would bind somewhere the parent never
/// probes — a spawn that "succeeds" against a socket nobody dials.
///
/// # Errors
///
/// When `trusty-analyze` is not on `PATH` or in a well-known bin directory.
///
/// Test: `analyze_spawn_spec_runs_a_bare_serve`.
/// `trusty-analyze` is not installed anywhere this process can see.
///
/// Why its own type rather than a bare string: this is the failure an operator
/// fixes with one action (install the binary), and it reaches them through
/// [`SupervisorError::SpawnSpec`]'s source chain, where a `&'static str` would
/// have arrived as an unattributed sentence.