Skip to main content

rightkit_daemon/
server.rs

1use crate::contract::{new_context, Caller};
2pub use crate::contract::{CallContext, Emit, Handler};
3use crate::discovery::{record_process_matches, try_lock_exclusive};
4use crate::error::{ServiceError, ERROR_RESPONSE_TOO_LARGE};
5use crate::record::*;
6use crate::suite::{make_private_dir, valid_service_name, Suite, SuiteError};
7use crate::takeover_watch::{self, Bell, Wake, TAKEOVER_CHECK_INTERVAL};
8use crate::transport::{
9    self, pid_alive, probe_endpoint, try_endpoint_for, Accepted, Conn, Listener,
10    DEFAULT_MAX_PIPE_INSTANCES,
11};
12use rightkit_sidecar::{read_json_line, write_json_line, FrameError};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15use std::io::{BufReader, Write};
16use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
17use std::sync::{Arc, Condvar, Mutex};
18use std::time::{Duration, Instant, SystemTime};
19
20pub const DEFAULT_FRAME_LIMIT: usize = 16 * 1024 * 1024;
21
22/// Default for [`HostOptions::write_timeout`]: how long one frame write plus flush to a client may
23/// take before the host closes that connection. Matches the client's default call deadline.
24pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(120);
25
26/// Content-free lifecycle events for the host's journal.
27#[derive(Debug, Clone, PartialEq)]
28pub enum ServiceEvent {
29    Started {
30        service: String,
31        endpoint: String,
32        rank: u8,
33    },
34    ConnectionOpened {
35        conn: u64,
36        app: String,
37    },
38    ConnectionRejected {
39        app: String,
40        reason: String,
41    },
42    /// A connection was closed unread because the OS reported its peer is not the current
43    /// user (or could not be identified). `reason` is the typed peer error: a uid or pid,
44    /// never request content.
45    PeerRejected {
46        reason: String,
47    },
48    Denied {
49        conn: u64,
50        app: String,
51        method: String,
52    },
53    HandoffRequested {
54        by_role: String,
55        by_rank: u8,
56    },
57    /// A handler's result (`kind` "response") or an emitted event (`kind` "event") was larger
58    /// than the frame limit and was not sent. For a response the client received the typed
59    /// [`ERROR_RESPONSE_TOO_LARGE`](crate::ERROR_RESPONSE_TOO_LARGE) error instead.
60    FrameTooLarge {
61        conn: u64,
62        method: String,
63        kind: &'static str,
64        limit: usize,
65    },
66    Released,
67}
68
69/// Callback for [`ServiceEvent`]s.
70pub type EventFn = Arc<dyn Fn(&ServiceEvent) + Send + Sync>;
71
72pub struct ServiceConfig {
73    pub service: String,
74    pub role: String,
75    pub rank: u8,
76    pub version: String,
77    pub allowlist: Allowlist,
78    pub frame_limit: usize,
79    /// Deadline for election, endpoint publication, or a higher-ranked takeover.
80    pub takeover_timeout: Duration,
81    /// How long the incumbent lets in-flight requests reach a boundary.
82    pub handoff_grace: Duration,
83    pub on_event: Option<EventFn>,
84    /// Role -> rank for peers that publish a `role` but no `rank` (HeardRight's
85    /// `service.json` and `takeover.json`). A peer whose rank cannot be
86    /// resolved is never taken over from and never obliges a handoff.
87    pub known_roles: std::collections::BTreeMap<String, u8>,
88    /// Explicit endpoint (a compatibility convention such as HeardRight's
89    /// `<service_dir>/speech.sock`); default is [`crate::endpoint_for`].
90    pub endpoint: Option<String>,
91    /// A per-run secret every frame must carry; see [`ServiceConfig::private_secret`].
92    pub private_secret: Option<crate::private::PrivateSecret>,
93}
94
95impl ServiceConfig {
96    pub fn new(service: &str, role: &str, rank: u8, version: &str, allowlist: Allowlist) -> Self {
97        Self {
98            service: service.into(),
99            role: role.into(),
100            rank,
101            version: version.into(),
102            allowlist,
103            frame_limit: DEFAULT_FRAME_LIMIT,
104            takeover_timeout: Duration::from_secs(60),
105            handoff_grace: Duration::from_secs(30),
106            on_event: None,
107            known_roles: Default::default(),
108            endpoint: None,
109            private_secret: None,
110        }
111    }
112
113    /// Require `secret` on the `hello` frame and on every `request` frame (a top-level `secret`
114    /// field, checked in constant time before any handler runs). A frame without it, or with a
115    /// wrong one, is answered `unauthenticated` and the connection is closed. This is the host
116    /// half of the private per-run child service pattern; see the `private` module.
117    pub fn private_secret(mut self, secret: crate::private::PrivateSecret) -> Self {
118        self.private_secret = Some(secret);
119        self
120    }
121
122    /// Take the allowlist from a [`MethodTable`](crate::contract::MethodTable), the single source
123    /// of method policy.
124    pub fn with_methods(mut self, table: &crate::contract::MethodTable) -> Self {
125        self.allowlist = table.allowlist();
126        self
127    }
128}
129
130/// Host-side transport options, passed to [`host_with_options`]. `HostOptions::default()`
131/// is what [`host`] uses.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct HostOptions {
134    max_pipe_instances: u32,
135    write_timeout: Option<Duration>,
136    max_message: usize,
137    max_connections: usize,
138}
139
140/// Default for [`HostOptions::max_connections`].
141pub const DEFAULT_MAX_CONNECTIONS: usize = 64;
142
143impl Default for HostOptions {
144    fn default() -> Self {
145        Self {
146            max_pipe_instances: DEFAULT_MAX_PIPE_INSTANCES,
147            write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
148            max_message: crate::chunk::DEFAULT_MAX_MESSAGE,
149            max_connections: DEFAULT_MAX_CONNECTIONS,
150        }
151    }
152}
153
154impl HostOptions {
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Cap on simultaneous instances of the Windows named pipe: live connections plus the
160    /// one instance waiting for the next client. A client that arrives when the cap is
161    /// reached gets "pipe busy" and retries, so a flood of connections cannot grow the
162    /// host without bound. Default [`DEFAULT_MAX_PIPE_INSTANCES`] (16); clamped to
163    /// `1..=254` (255 is "unlimited" to Win32 and is never used). Ignored on Unix, where
164    /// there is no pipe.
165    #[must_use]
166    pub fn max_pipe_instances(mut self, n: u32) -> Self {
167        self.max_pipe_instances = n.clamp(1, 254);
168        self
169    }
170
171    /// The effective (clamped) cap.
172    pub fn get_max_pipe_instances(&self) -> u32 {
173        self.max_pipe_instances
174    }
175
176    /// Longest one frame (hello, event, or response) may take to write and flush to a client.
177    /// A client that stops reading fills the transport buffer and would otherwise block the
178    /// writing thread, and the connection with it, forever; when the bound passes the host closes
179    /// that connection (the client sees `connection_closed`, never a partial frame it could
180    /// mistake for a reply), and the connection's `connection_closed` handler hook runs. The
181    /// bound is per frame, so a slow but progressing reader of a stream of events is not cut off
182    /// unless a single frame stalls. Default [`DEFAULT_WRITE_TIMEOUT`] (120 s); `None` restores
183    /// the earlier unbounded write. A zero duration is raised to 1 ms.
184    #[must_use]
185    pub fn write_timeout(mut self, bound: Option<Duration>) -> Self {
186        self.write_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
187        self
188    }
189
190    /// The effective write bound (`None` = unbounded).
191    pub fn get_write_timeout(&self) -> Option<Duration> {
192        self.write_timeout
193    }
194
195    /// Largest message the host assembles from chunks, or hands out in chunks (see the `chunk`
196    /// module). The frame limit ([`ServiceConfig::frame_limit`]) stays the cap on any one frame;
197    /// this is the second, separate cap on a whole chunked request or result, held in memory per
198    /// connection. Default `0`: chunking is **off**, the host refuses `rk.upload` and never
199    /// announces a chunked result (it was 256 MiB earlier in the unpublished 0.2.7; see issue #323).
200    /// A service that takes large calls opts in with a size (see
201    /// [`SUGGESTED_MAX_MESSAGE`](crate::chunk::SUGGESTED_MAX_MESSAGE)). A connection holds at most one
202    /// assembly of at most this many bytes; the first piece is authorised before it is buffered,
203    /// and the assembled text is bounded in nesting and value count before it is parsed.
204    #[must_use]
205    pub fn max_message(mut self, bytes: usize) -> Self {
206        self.max_message = bytes;
207        self
208    }
209
210    /// The effective message bound.
211    pub fn get_max_message(&self) -> usize {
212        self.max_message
213    }
214
215    /// Cap on simultaneous connections on Unix (the counterpart of the Windows pipe-instance
216    /// cap, which this does not replace there): a connection that arrives over the cap is closed
217    /// at once, before any frame is read, so a flood cannot grow the host's threads and buffers
218    /// without bound. Default [`DEFAULT_MAX_CONNECTIONS`] (64); at least 1. Unlike Windows "pipe
219    /// busy", the refused client sees the connection close rather than a retryable busy status.
220    /// Ignored on Windows.
221    #[must_use]
222    pub fn max_connections(mut self, n: usize) -> Self {
223        self.max_connections = n.max(1);
224        self
225    }
226
227    /// The effective connection cap.
228    pub fn get_max_connections(&self) -> usize {
229        self.max_connections
230    }
231}
232
233#[derive(Debug)]
234pub enum HostError {
235    /// A live host of equal or better rank already serves this service.
236    Held(ServiceRecord),
237    /// The incumbent did not release within `takeover_timeout`.
238    TakeoverTimeout,
239    InvalidName(String),
240    Suite(SuiteError),
241    Io(std::io::Error),
242    /// No socket path within the OS limit can name this service (typed; carries path, length, limit).
243    Endpoint(crate::transport::EndpointError),
244}
245impl std::fmt::Display for HostError {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            HostError::Held(r) => write!(
249                f,
250                "{} is already hosted by {} (rank {}, pid {})",
251                r.service, r.role, r.rank, r.pid
252            ),
253            HostError::TakeoverTimeout => write!(f, "incumbent host did not release in time"),
254            HostError::InvalidName(s) => write!(f, "invalid service name {s:?}"),
255            HostError::Suite(e) => write!(f, "{e}"),
256            HostError::Io(e) => write!(f, "{e}"),
257            HostError::Endpoint(e) => write!(f, "{e}"),
258        }
259    }
260}
261impl std::error::Error for HostError {}
262impl From<std::io::Error> for HostError {
263    fn from(e: std::io::Error) -> Self {
264        HostError::Io(e)
265    }
266}
267impl From<SuiteError> for HostError {
268    fn from(e: SuiteError) -> Self {
269        HostError::Suite(e)
270    }
271}
272
273fn resolve_rank(cfg: &ServiceConfig, role: &str, rank: u8) -> u8 {
274    crate::discovery::resolve_rank(role, rank, &cfg.known_roles).unwrap_or(RANK_UNKNOWN)
275}
276
277struct Shared {
278    suite: Suite,
279    cfg: ServiceConfig,
280    handler: Arc<dyn Handler>,
281    listener: Listener,
282    // Held until endpoint cleanup completes; kernel releases it on process death.
283    election: Mutex<Option<std::fs::File>>,
284    record: ServiceRecord,
285    stop: AtomicBool,
286    handoff: Arc<AtomicBool>,
287    released: Mutex<bool>,
288    released_cv: Condvar,
289    inflight: AtomicUsize,
290    next_conn: AtomicU64,
291    conns: Mutex<HashMap<u64, Arc<dyn Fn() + Send + Sync>>>,
292    /// What the takeover monitor blocks on; `release` stops it.
293    bell: Arc<Bell>,
294    watch: WatchTuning,
295    write_timeout: Option<Duration>,
296    max_message: usize,
297    max_connections: usize,
298    /// Connections being served (Unix cap).
299    live_conns: Arc<AtomicUsize>,
300    /// Failed-authentication limiter (issue #504), shared by every connection.
301    fails: Mutex<crate::fail_limit::FailLimiter>,
302}
303
304/// Forgets a connection's failure-limiter entry when its thread ends, however it ends.
305struct ForgetFails {
306    s: Arc<Shared>,
307    conn: u64,
308}
309
310impl Drop for ForgetFails {
311    fn drop(&mut self) {
312        self.s
313            .fails
314            .lock()
315            .unwrap_or_else(|e| e.into_inner())
316            .forget(self.conn, Instant::now());
317    }
318}
319
320/// Decrements the live-connection count when a connection thread ends, however it ends.
321struct ConnSlot(Arc<AtomicUsize>);
322
323impl Drop for ConnSlot {
324    fn drop(&mut self) {
325        self.0.fetch_sub(1, Ordering::SeqCst);
326    }
327}
328
329/// Bounds each frame write on one connection. The writer calls [`begin`](Self::begin) before and
330/// [`end`](Self::end) after a write+flush; a watcher thread closes the connection through the
331/// transport `shutdown` (which wakes a blocked Unix write and cancels pending Windows pipe I/O)
332/// when one write outlasts `bound`. Only the oldest unfinished write is timed, so a stream of
333/// writes that each finish in time is never cut.
334struct WriteBound {
335    state: Mutex<WriteState>,
336    cv: Condvar,
337}
338
339struct WriteState {
340    /// Deadline of the write in progress.
341    until: Option<Instant>,
342    done: bool,
343}
344
345struct FinishOnDrop(Arc<WriteBound>);
346impl Drop for FinishOnDrop {
347    fn drop(&mut self) {
348        self.0.finish();
349    }
350}
351
352impl WriteBound {
353    fn start(shutdown: Arc<dyn Fn() + Send + Sync>) -> Arc<WriteBound> {
354        let w = Arc::new(WriteBound {
355            state: Mutex::new(WriteState {
356                until: None,
357                done: false,
358            }),
359            cv: Condvar::new(),
360        });
361        let watcher = w.clone();
362        std::thread::spawn(move || {
363            let mut g = watcher.state.lock().unwrap_or_else(|e| e.into_inner());
364            loop {
365                if g.done {
366                    return;
367                }
368                match g.until {
369                    None => g = watcher.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
370                    Some(t) => {
371                        let left = t.saturating_duration_since(Instant::now());
372                        if left.is_zero() {
373                            g.until = None;
374                            drop(g);
375                            shutdown();
376                            return;
377                        }
378                        g = watcher
379                            .cv
380                            .wait_timeout(g, left)
381                            .unwrap_or_else(|e| e.into_inner())
382                            .0;
383                    }
384                }
385            }
386        });
387        w
388    }
389    fn set(&self, until: Option<Instant>) {
390        self.state.lock().unwrap_or_else(|e| e.into_inner()).until = until;
391        self.cv.notify_all();
392    }
393    fn finish(&self) {
394        self.state.lock().unwrap_or_else(|e| e.into_inner()).done = true;
395        self.cv.notify_all();
396    }
397}
398
399/// How the takeover monitor waits and how it tells that a requester is alive. Production uses
400/// [`WatchTuning::PRODUCTION`]; the tests lengthen or shorten the interval and fake the probe.
401#[derive(Clone, Copy)]
402struct WatchTuning {
403    /// How often a request written by another process is looked for.
404    interval: Duration,
405    /// Is the process that wrote a takeover request still running?
406    alive: fn(u32) -> bool,
407}
408
409impl WatchTuning {
410    const PRODUCTION: WatchTuning = WatchTuning {
411        interval: TAKEOVER_CHECK_INTERVAL,
412        alive: pid_alive,
413    };
414}
415
416impl Shared {
417    fn event(&self, e: ServiceEvent) {
418        if let Some(f) = &self.cfg.on_event {
419            f(&e);
420        }
421    }
422
423    fn release(&self) {
424        {
425            let mut r = self.released.lock().unwrap_or_else(|e| e.into_inner());
426            if *r {
427                return;
428            }
429            *r = true;
430        }
431        self.stop.store(true, Ordering::SeqCst);
432        // The takeover monitor is blocked on its bell, not sleeping: wake it so it leaves now.
433        self.bell.stop();
434        // Remove the record first so clients stop discovering this host, but only if it is ours.
435        if read_record(&self.suite, &self.cfg.service).is_some_and(|r| r.pid == self.record.pid) {
436            let _ = std::fs::remove_file(record_path(&self.suite, &self.cfg.service));
437        }
438        self.listener.wake();
439        self.listener.cleanup();
440        for (_, close) in self.conns.lock().unwrap_or_else(|e| e.into_inner()).drain() {
441            close();
442        }
443        self.election
444            .lock()
445            .unwrap_or_else(|e| e.into_inner())
446            .take();
447        self.event(ServiceEvent::Released);
448        self.released_cv.notify_all();
449    }
450}
451
452/// Runtime figures of a running host, computed on call by [`Hosted::status`].
453#[derive(Debug, Clone, Copy, PartialEq)]
454pub struct ServiceStatus {
455    /// Connections open now.
456    pub connections: usize,
457    /// Requests being served now (the queue the handoff waits on).
458    pub inflight: usize,
459    /// A takeover was accepted and the host is draining.
460    pub handoff_requested: bool,
461    /// The host has released the service.
462    pub released: bool,
463    /// Resident memory, CPU time and I/O counters of the whole process (the host runs in it), or
464    /// `None` when the host cannot report them. See `rightkit_child_process::ProcessUsage`.
465    pub process: Option<rightkit_child_process::ProcessUsage>,
466}
467
468/// A running host. Dropping it releases the service.
469pub struct Hosted {
470    shared: Arc<Shared>,
471}
472
473impl Hosted {
474    pub fn record(&self) -> &ServiceRecord {
475        &self.shared.record
476    }
477    pub fn handoff_requested(&self) -> bool {
478        self.shared.handoff.load(Ordering::SeqCst)
479    }
480    pub fn inflight(&self) -> usize {
481        self.shared.inflight.load(Ordering::SeqCst)
482    }
483    /// Connections, in-flight requests and process resource usage, read now. No polling thread
484    /// runs for this.
485    pub fn status(&self) -> ServiceStatus {
486        ServiceStatus {
487            connections: self
488                .shared
489                .conns
490                .lock()
491                .unwrap_or_else(|e| e.into_inner())
492                .len(),
493            inflight: self.shared.inflight.load(Ordering::SeqCst),
494            handoff_requested: self.shared.handoff.load(Ordering::SeqCst),
495            released: *self
496                .shared
497                .released
498                .lock()
499                .unwrap_or_else(|e| e.into_inner()),
500            process: rightkit_child_process::process_usage().ok(),
501        }
502    }
503    /// Block until the host has released (clean shutdown or takeover).
504    pub fn wait_released(&self, timeout: Duration) -> bool {
505        let g = self
506            .shared
507            .released
508            .lock()
509            .unwrap_or_else(|e| e.into_inner());
510        let (g, _) = self
511            .shared
512            .released_cv
513            .wait_timeout_while(g, timeout, |r| !*r)
514            .unwrap_or_else(|e| e.into_inner());
515        *g
516    }
517    pub fn shutdown(&self) {
518        self.shared.release();
519    }
520}
521
522impl Drop for Hosted {
523    fn drop(&mut self) {
524        self.shared.release();
525    }
526}
527
528/// Host `cfg.service` on `suite`: discover, rank, take over if outranking,
529/// bind the current-user-only endpoint, publish `service.json`. Chunked calls (`rk.upload`) are
530/// off unless the options set [`HostOptions::max_message`].
531// `HostError::Held` carries the incumbent's record by value so callers can match on it directly;
532// this runs once per host start, so the larger Err is not a hot-path cost.
533#[allow(clippy::result_large_err)]
534pub fn host(
535    suite: &Suite,
536    cfg: ServiceConfig,
537    handler: Arc<dyn Handler>,
538) -> Result<Hosted, HostError> {
539    host_with_options(suite, cfg, handler, HostOptions::default())
540}
541
542/// [`host`] with explicit [`HostOptions`].
543#[allow(clippy::result_large_err)]
544pub fn host_with_options(
545    suite: &Suite,
546    cfg: ServiceConfig,
547    handler: Arc<dyn Handler>,
548    options: HostOptions,
549) -> Result<Hosted, HostError> {
550    start_host(suite, cfg, handler, options, WatchTuning::PRODUCTION)
551}
552
553#[allow(clippy::result_large_err)]
554fn start_host(
555    suite: &Suite,
556    cfg: ServiceConfig,
557    handler: Arc<dyn Handler>,
558    options: HostOptions,
559    watch: WatchTuning,
560) -> Result<Hosted, HostError> {
561    if !valid_service_name(&cfg.service) {
562        return Err(HostError::InvalidName(cfg.service.clone()));
563    }
564    let dir = suite.service_dir(&cfg.service);
565    make_private_dir(&dir)?;
566    let me = std::process::id();
567    let deadline = Instant::now() + cfg.takeover_timeout;
568    let mut requested = false;
569    let lock_path = dir.join("election.lock");
570    let endpoint = match cfg.endpoint.clone() {
571        Some(e) => e,
572        None => try_endpoint_for(suite, &cfg.service).map_err(HostError::Endpoint)?,
573    };
574    let mut backoff = Duration::from_millis(5);
575    let (listener, election) = loop {
576        // Never unlink the lock file: every contender must lock the same inode.
577        // A loser can observe the incumbent, but only the owner can discard stale
578        // discovery or bind. Retain ownership through publication and shutdown.
579        let election = try_lock_exclusive(&lock_path)?;
580        // `Err` is "unknown", not "no record": it falls to the arm that waits, so a record that
581        // cannot be read for an instant is never removed and no listener is bound beside it.
582        let current = try_read_record(suite, &cfg.service);
583        let unreadable = current
584            .as_ref()
585            .err()
586            .map(|e| std::io::Error::new(e.kind(), e.to_string()));
587        match current {
588            Ok(Some(rec)) if record_process_matches(&rec) && probe_endpoint(&rec.endpoint) => {
589                let their = resolve_rank(&cfg, &rec.role, rec.rank);
590                if their == RANK_UNKNOWN || cfg.rank >= their {
591                    return Err(HostError::Held(rec));
592                }
593                if !requested {
594                    let t = TakeoverRequest {
595                        role: cfg.role.clone(),
596                        rank: cfg.rank,
597                        pid: me,
598                        requested_at_ms: now_ms(),
599                    };
600                    let request_path = takeover_path(suite, &cfg.service);
601                    write_atomic(&request_path, &serde_json::to_vec(&t).unwrap_or_default())?;
602                    // A monitor in this process (another host of this service) looks now.
603                    takeover_watch::poke(&request_path);
604                    requested = true;
605                }
606            }
607            Ok(_) if election.is_some() => {
608                // PID generation and endpoint checks rejected the old record.
609                // Only the elected process may remove it or a dead Unix socket.
610                let _ = std::fs::remove_file(record_path(suite, &cfg.service));
611                match Listener::bind(&endpoint, options.max_pipe_instances) {
612                    Ok(listener) => break (listener, election.unwrap()),
613                    // A legacy host may have bound without publishing yet, or a
614                    // Windows incumbent may still be closing its pipe handles.
615                    Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {}
616                    Err(e) => return Err(HostError::Io(e)),
617                }
618            }
619            // Winner is between lock acquisition and record publication, or the record could
620            // not be read (see above).
621            _ => {}
622        }
623        drop(election);
624        let remaining = deadline.saturating_duration_since(Instant::now());
625        if remaining.is_zero() {
626            clear_takeover_if_owner(suite, &cfg.service, me);
627            // Still unable to read the record at the deadline: say so rather than report a
628            // timeout that blames the incumbent.
629            return Err(match unreadable {
630                Some(e) => HostError::Io(e),
631                None => HostError::TakeoverTimeout,
632            });
633        }
634        std::thread::sleep(backoff.min(remaining));
635        backoff = (backoff * 2).min(Duration::from_millis(50));
636    };
637    let record = ServiceRecord {
638        protocol_major: PROTOCOL_MAJOR,
639        protocol_minor: PROTOCOL_MINOR,
640        service: cfg.service.clone(),
641        role: cfg.role.clone(),
642        rank: cfg.rank,
643        pid: me,
644        endpoint: endpoint.clone(),
645        version: cfg.version.clone(),
646        engine_version: cfg.version.clone(),
647        started_ms: now_ms(),
648        process_started_ms: crate::discovery::own_start_ms().unwrap_or(0),
649    };
650    if let Err(e) = write_atomic(
651        &record_path(suite, &cfg.service),
652        &serde_json::to_vec_pretty(&record).unwrap_or_default(),
653    ) {
654        listener.cleanup();
655        return Err(HostError::Io(e));
656    }
657    if read_takeover(suite, &cfg.service).is_some_and(|t| t.pid == me) {
658        let _ = std::fs::remove_file(takeover_path(suite, &cfg.service));
659    }
660    let shared = Arc::new(Shared {
661        suite: suite.clone(),
662        cfg,
663        handler,
664        listener,
665        election: Mutex::new(Some(election)),
666        record,
667        stop: AtomicBool::new(false),
668        handoff: Arc::new(AtomicBool::new(false)),
669        released: Mutex::new(false),
670        released_cv: Condvar::new(),
671        inflight: AtomicUsize::new(0),
672        next_conn: AtomicU64::new(1),
673        conns: Mutex::new(HashMap::new()),
674        bell: Arc::new(Bell::new()),
675        watch,
676        write_timeout: options.write_timeout,
677        max_message: options.max_message,
678        max_connections: options.max_connections,
679        live_conns: Arc::new(AtomicUsize::new(0)),
680        fails: Mutex::new(crate::fail_limit::FailLimiter::new()),
681    });
682    takeover_watch::register(
683        takeover_path(&shared.suite, &shared.cfg.service),
684        &shared.bell,
685    );
686    shared.event(ServiceEvent::Started {
687        service: shared.cfg.service.clone(),
688        endpoint,
689        rank: shared.cfg.rank,
690    });
691    let s = shared.clone();
692    if let Err(e) = std::thread::Builder::new()
693        .name("rk-svc-accept".into())
694        .spawn(move || accept_loop(s))
695    {
696        shared.release();
697        return Err(HostError::Io(e));
698    }
699    let s = shared.clone();
700    if let Err(e) = std::thread::Builder::new()
701        .name("rk-svc-takeover".into())
702        .spawn(move || takeover_monitor(s))
703    {
704        shared.release();
705        return Err(HostError::Io(e));
706    }
707    Ok(Hosted { shared })
708}
709
710pub(crate) fn now_ms() -> u64 {
711    std::time::SystemTime::now()
712        .duration_since(std::time::UNIX_EPOCH)
713        .map(|d| d.as_millis() as u64)
714        .unwrap_or(0)
715}
716
717fn accept_loop(s: Arc<Shared>) {
718    while !s.stop.load(Ordering::SeqCst) {
719        match s.listener.accept() {
720            Ok(Accepted::Conn(conn)) => {
721                if s.stop.load(Ordering::SeqCst) {
722                    break;
723                }
724                // Count before spawning so a burst cannot overshoot the cap. Windows is bounded
725                // by the pipe-instance cap, so only Unix refuses here.
726                let slot = ConnSlot(s.live_conns.clone());
727                let live = s.live_conns.fetch_add(1, Ordering::SeqCst) + 1;
728                if cfg!(unix) && live > s.max_connections {
729                    (conn.shutdown)();
730                    drop(slot);
731                    s.event(ServiceEvent::PeerRejected {
732                        reason: format!("connection limit of {} reached", s.max_connections),
733                    });
734                    continue;
735                }
736                let s2 = s.clone();
737                let _ = std::thread::Builder::new()
738                    .name("rk-svc-conn".into())
739                    .spawn(move || {
740                        let _slot = slot;
741                        serve_conn(s2, conn)
742                    });
743            }
744            // Wrong user (or unidentifiable): already closed unread. Log it, except for the
745            // shutdown wake-up, which is our own connection.
746            Ok(Accepted::Rejected(reason)) => {
747                if !s.stop.load(Ordering::SeqCst) {
748                    s.event(ServiceEvent::PeerRejected { reason });
749                }
750            }
751            Ok(Accepted::Vanished) => {}
752            Err(_) => {
753                if s.stop.load(Ordering::SeqCst) {
754                    break;
755                }
756                std::thread::sleep(Duration::from_millis(20));
757            }
758        }
759    }
760}
761
762/// What `takeover.json` looked like when it was last judged: modification time and length.
763type FileStamp = (Option<SystemTime>, u64);
764
765fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
766    let md = std::fs::metadata(path).ok()?;
767    Some((md.modified().ok(), md.len()))
768}
769
770/// The requester's rank when `t` obliges a host of `cfg.rank` (running as `own_pid`) to hand
771/// off: a known rank that beats ours, from another live process, and fresh. `None` otherwise.
772fn obliging_rank(
773    cfg: &ServiceConfig,
774    own_pid: u32,
775    alive: fn(u32) -> bool,
776    t: &TakeoverRequest,
777    now: u64,
778) -> Option<u8> {
779    let max_age = cfg.takeover_timeout.as_millis() as u64;
780    let fresh = now.saturating_sub(t.requested_at_ms) <= max_age + 5_000;
781    let their = resolve_rank(cfg, &t.role, t.rank);
782    let obliges =
783        their != RANK_UNKNOWN && their < cfg.rank && t.pid != own_pid && alive(t.pid) && fresh;
784    obliges.then_some(their)
785}
786
787#[cfg(test)]
788struct ExitMark<'a>(&'a Bell);
789
790#[cfg(test)]
791impl Drop for ExitMark<'_> {
792    fn drop(&mut self) {
793        self.0.mark_exited();
794    }
795}
796
797/// Watch for a takeover request from a higher-ranked live host.
798///
799/// The thread blocks on `s.bell`: it wakes when the host releases (leave), when this process wrote
800/// a takeover request for the service (look now), and otherwise once per `s.watch.interval` to
801/// stat `takeover.json`, which is how a request written by another process is found. The file is
802/// read only when its modification time or length differs from the one already judged (or on an
803/// in-process poke), and a request that was judged not to oblige is not judged again until the
804/// file changes. That is sound because the verdict can only turn from "obliges" to "does not" with
805/// time (a request goes stale, a requester exits), never the other way.
806fn takeover_monitor(s: Arc<Shared>) {
807    #[cfg(test)]
808    let _exit = ExitMark(&s.bell);
809    let path = takeover_path(&s.suite, &s.cfg.service);
810    let mut judged: Option<FileStamp> = None;
811    let mut look_now = false;
812    loop {
813        if s.stop.load(Ordering::SeqCst) {
814            return;
815        }
816        let force = std::mem::take(&mut look_now);
817        match file_stamp(&path) {
818            None => judged = None,
819            Some(stamp) if force || judged != Some(stamp) => {
820                judged = Some(stamp);
821                #[cfg(test)]
822                s.bell.note_read();
823                match try_read_takeover(&s.suite, &s.cfg.service) {
824                    Ok(Some(t)) => {
825                        let now = now_ms();
826                        let obliging = obliging_rank(&s.cfg, s.record.pid, s.watch.alive, &t, now);
827                        if let Some(their) = obliging {
828                            hand_off(&s, t, their);
829                            return;
830                        }
831                    }
832                    Ok(None) => {}
833                    // Could not be read: whether a request exists is unknown, not "none". Do not
834                    // mark this stamp as judged, so the next pass (one interval on, no faster)
835                    // reads the file again.
836                    Err(_) => judged = None,
837                }
838            }
839            Some(_) => {}
840        }
841        #[cfg(test)]
842        s.bell.note_pass();
843        match s.bell.wait(s.watch.interval) {
844            Wake::Stop => return,
845            Wake::Poke => look_now = true,
846            Wake::Timeout => {}
847        }
848    }
849}
850
851/// Accept a takeover: stop admitting work, let in-flight requests reach a boundary (up to
852/// `handoff_grace`), then release.
853fn hand_off(s: &Shared, t: TakeoverRequest, their: u8) {
854    s.handoff.store(true, Ordering::SeqCst);
855    s.event(ServiceEvent::HandoffRequested {
856        by_role: t.role,
857        by_rank: their,
858    });
859    s.handler.on_handoff();
860    let until = Instant::now() + s.cfg.handoff_grace;
861    while s.inflight.load(Ordering::SeqCst) > 0
862        && Instant::now() < until
863        && !s.stop.load(Ordering::SeqCst)
864    {
865        std::thread::sleep(Duration::from_millis(10));
866    }
867    s.release();
868}
869
870fn frame_err(e: &FrameError) -> ServiceError {
871    ServiceError::new("bad_frame", e.to_string())
872}
873
874fn serve_conn(s: Arc<Shared>, conn: Conn) {
875    let Conn {
876        reader,
877        writer,
878        peer_closed,
879        shutdown,
880        peer,
881    } = conn;
882    let mut reader = BufReader::new(reader);
883    let writer = Arc::new(Mutex::new(writer));
884    let limit = s.cfg.frame_limit;
885    let bound = s
886        .write_timeout
887        .map(|d| (d, WriteBound::start(shutdown.clone())));
888    // Ends the watcher on every way out of this function, early returns included.
889    let _watcher_ends = bound.as_ref().map(|(_, b)| FinishOnDrop(b.clone()));
890    // Write one frame. `false` only when the frame is larger than the limit: nothing was
891    // written, so the stream is still on a frame boundary. An I/O failure means the peer is
892    // gone, which the read loop and the peer watch already report.
893    let write = {
894        let w = writer.clone();
895        let bound = bound.clone();
896        move |v: &Value| -> bool {
897            let mut w = w.lock().unwrap_or_else(|e| e.into_inner());
898            if let Some((d, b)) = &bound {
899                b.set(Some(Instant::now() + *d));
900            }
901            let r = match write_json_line(&mut *w, v, limit) {
902                Ok(()) => {
903                    let _ = w.flush();
904                    true
905                }
906                Err(FrameError::TooLarge { .. }) => false,
907                Err(_) => true,
908            };
909            if let Some((_, b)) = &bound {
910                b.set(None);
911            }
912            r
913        }
914    };
915    let send = {
916        let write = write.clone();
917        move |v: &Value| {
918            write(v);
919        }
920    };
921    let mut buf = Vec::new();
922    // The connection's key for the failure limiter and its events; the same number is its id.
923    let conn_key = s.next_conn.fetch_add(1, Ordering::SeqCst);
924    let _forget_fails = ForgetFails {
925        s: s.clone(),
926        conn: conn_key,
927    };
928    // ---- hello
929    let hello: Value = match read_json_line(&mut reader, &mut buf, 64 * 1024) {
930        Ok(Some(v)) => v,
931        _ => return, // probe or garbage
932    };
933    let app = hello["app"].as_str().unwrap_or("").to_string();
934    let reject = |reason: &str, code: &str, retryable: bool| {
935        let e = ServiceError {
936            code: code.into(),
937            message: reason.into(),
938            retryable,
939        };
940        send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
941        s.event(ServiceEvent::ConnectionRejected {
942            app: app.clone(),
943            reason: reason.into(),
944        });
945        shutdown();
946    };
947    if hello["type"] != "hello" || app.is_empty() {
948        return reject(
949            "first frame must be hello with an app label",
950            "bad_hello",
951            false,
952        );
953    }
954    if let Some(secret) = &s.cfg.private_secret {
955        // The limiter sees only a secret that failed to verify: a correct hello is never refused
956        // because other callers failed (issue #504).
957        if !secret.verify(hello["secret"].as_str().unwrap_or("")) {
958            let now = Instant::now();
959            let limited = s
960                .fails
961                .lock()
962                .unwrap_or_else(|e| e.into_inner())
963                .refuses_unverified(conn_key, now);
964            if limited {
965                return reject(
966                    "too many failed authentications; wait before reconnecting",
967                    crate::error::ERROR_RATE_LIMITED,
968                    false,
969                );
970            }
971            s.fails
972                .lock()
973                .unwrap_or_else(|e| e.into_inner())
974                .record_failure(conn_key, now);
975            return reject(
976                "missing or wrong secret",
977                crate::error::ERROR_UNAUTHENTICATED,
978                false,
979            );
980        }
981    }
982    if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
983        return reject("protocol major mismatch", "protocol_mismatch", false);
984    }
985    if s.handoff.load(Ordering::SeqCst) {
986        return reject(
987            "host is handing off",
988            crate::error::ERROR_HOST_HANDOFF,
989            true,
990        );
991    }
992    if !s.cfg.allowlist.app_known(&app) {
993        return reject(
994            "app is not on the service allowlist",
995            crate::error::ERROR_NOT_ALLOWED,
996            false,
997        );
998    }
999    let id = conn_key;
1000    s.conns
1001        .lock()
1002        .unwrap_or_else(|e| e.into_inner())
1003        .insert(id, shutdown.clone());
1004    s.event(ServiceEvent::ConnectionOpened {
1005        conn: id,
1006        app: app.clone(),
1007    });
1008    send(&json!({
1009        "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
1010        "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
1011        "methods": s.cfg.allowlist.methods_for(&app),
1012        "frame_limit": limit, "max_message": s.max_message,
1013        "chunk_bytes": crate::chunk::chunk_bytes_for(limit),
1014    }));
1015
1016    let caller = Caller::local_app(app.clone(), peer.clone());
1017    let closed_notified = Arc::new(AtomicBool::new(false));
1018    let notify_closed = {
1019        let (s, flag) = (s.clone(), closed_notified.clone());
1020        move || {
1021            if !flag.swap(true, Ordering::SeqCst) {
1022                s.handler.connection_closed(id);
1023            }
1024        }
1025    };
1026    // Chunked transfers (see the `chunk` module): at most one of each per connection, dropped
1027    // with it.
1028    let chunk_bytes = crate::chunk::chunk_bytes_for(limit);
1029    let mut uploads = crate::chunk::Uploads::default();
1030    let mut download: Option<crate::chunk::Download> = None;
1031    let mut downloads_made: u64 = 0;
1032    // ---- requests, lock-step: one in flight per connection
1033    loop {
1034        let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
1035            Ok(Some(v)) => v,
1036            Ok(None) | Err(FrameError::Io(_)) => break,
1037            Err(e) => {
1038                send(
1039                    &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
1040                );
1041                continue;
1042            }
1043        };
1044        if let Some(secret) = &s.cfg.private_secret {
1045            // A verified request is never refused by the limiter, so a block caused by other
1046            // callers cannot end an authenticated connection (issue #504).
1047            if !secret.verify(req["secret"].as_str().unwrap_or("")) {
1048                let now = Instant::now();
1049                let limited = s
1050                    .fails
1051                    .lock()
1052                    .unwrap_or_else(|e| e.into_inner())
1053                    .refuses_unverified(conn_key, now);
1054                if limited {
1055                    s.event(ServiceEvent::ConnectionRejected {
1056                        app: app.clone(),
1057                        reason: "too many failed authentications".into(),
1058                    });
1059                    send(
1060                        &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1061                        "error": ServiceError::new(crate::error::ERROR_RATE_LIMITED, "too many failed authentications; wait before reconnecting").to_value()}),
1062                    );
1063                    break;
1064                }
1065                s.fails
1066                    .lock()
1067                    .unwrap_or_else(|e| e.into_inner())
1068                    .record_failure(conn_key, now);
1069                s.event(ServiceEvent::ConnectionRejected {
1070                    app: app.clone(),
1071                    reason: "missing or wrong secret".into(),
1072                });
1073                send(
1074                    &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1075                    "error": ServiceError::new(crate::error::ERROR_UNAUTHENTICATED, "missing or wrong secret").to_value()}),
1076                );
1077                break;
1078            }
1079        }
1080        let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
1081            send(
1082                &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
1083            );
1084            continue;
1085        };
1086        let respond_err = |e: ServiceError| {
1087            send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
1088        };
1089        // Any frame is a chance to drop an assembly that went idle.
1090        uploads.expire();
1091        let mut method = req["method"].as_str().unwrap_or("").to_string();
1092        let chunked_ok = req["chunked_ok"] == true && s.max_message > 0;
1093        if s.handoff.load(Ordering::SeqCst) {
1094            respond_err(ServiceError::handoff());
1095            continue;
1096        }
1097        // The reserved chunk methods carry other calls: the carried call is authorised below on
1098        // its own method id, so these need no allowlist entry of their own.
1099        let mut assembled: Option<Value> = None;
1100        if method == crate::chunk::METHOD_FETCH {
1101            let answer = match &download {
1102                Some(d) => d.fetch(req.get("params").unwrap_or(&Value::Null)),
1103                None => Err(ServiceError::new(
1104                    crate::error::ERROR_BAD_CHUNK,
1105                    "no chunked result is held on this connection",
1106                )),
1107            };
1108            match answer {
1109                Ok(v) => send(&json!({"type": "response", "id": rid, "ok": true, "result": v})),
1110                Err(e) => respond_err(e),
1111            }
1112            continue;
1113        }
1114        if method == crate::chunk::METHOD_UPLOAD {
1115            if s.max_message == 0 {
1116                respond_err(ServiceError::new(
1117                    crate::error::ERROR_REQUEST_TOO_LARGE,
1118                    "this host does not accept chunked requests",
1119                ));
1120                continue;
1121            }
1122            // The caller's allowlist runs on the first piece, before a byte is buffered; the
1123            // final dispatch below checks again.
1124            let authorize = |m: &str| {
1125                crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, m).inspect_err(
1126                    |_| {
1127                        s.event(ServiceEvent::Denied {
1128                            conn: id,
1129                            app: app.clone(),
1130                            method: m.to_string(),
1131                        });
1132                    },
1133                )
1134            };
1135            match uploads.accept_with(
1136                req.get("params").unwrap_or(&Value::Null),
1137                s.max_message,
1138                &authorize,
1139            ) {
1140                Ok(crate::chunk::UploadStep::More { received }) => {
1141                    send(&json!({"type": "response", "id": rid, "ok": true,
1142                        "result": {"received": received}}));
1143                    continue;
1144                }
1145                Ok(crate::chunk::UploadStep::Done { method: m, params }) => {
1146                    method = m;
1147                    assembled = Some(params);
1148                }
1149                Err(e) => {
1150                    respond_err(e);
1151                    continue;
1152                }
1153            }
1154        }
1155        if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
1156            s.event(ServiceEvent::Denied {
1157                conn: id,
1158                app: app.clone(),
1159                method: method.clone(),
1160            });
1161            respond_err(e);
1162            continue;
1163        }
1164        if method == "ping" {
1165            send(
1166                &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
1167            );
1168            continue;
1169        }
1170        let params =
1171            assembled.unwrap_or_else(|| req.get("params").cloned().unwrap_or_else(|| json!({})));
1172        let cx = new_context(
1173            id,
1174            caller.clone(),
1175            Arc::new(AtomicBool::new(false)),
1176            s.handoff.clone(),
1177        );
1178        let emit: Emit = {
1179            let (write, s, method) = (write.clone(), s.clone(), method.clone());
1180            Arc::new(move |ev| {
1181                if !write(&json!({"type": "event", "id": rid, "event": ev})) {
1182                    s.event(ServiceEvent::FrameTooLarge {
1183                        conn: id,
1184                        method: method.clone(),
1185                        kind: "event",
1186                        limit,
1187                    });
1188                }
1189            })
1190        };
1191        s.inflight.fetch_add(1, Ordering::SeqCst);
1192        let done = AtomicBool::new(false);
1193        let result = std::thread::scope(|sc| {
1194            // Parked, not slept: the scope joins this thread, so a plain 40 ms sleep added up to
1195            // 40 ms to every request after its handler had already finished.
1196            let watcher = sc.spawn(|| {
1197                while !done.load(Ordering::SeqCst) {
1198                    if peer_closed() {
1199                        cx.closed.store(true, Ordering::SeqCst);
1200                        notify_closed();
1201                        return;
1202                    }
1203                    std::thread::park_timeout(Duration::from_millis(40));
1204                }
1205            });
1206            let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1207                s.handler.call(&cx, &method, &params, &emit)
1208            }))
1209            .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
1210            done.store(true, Ordering::SeqCst);
1211            watcher.thread().unpark();
1212            r
1213        });
1214        s.inflight.fetch_sub(1, Ordering::SeqCst);
1215        if cx.peer_closed() {
1216            break;
1217        }
1218        match result {
1219            Ok(v) => {
1220                // A result that does not fit is answered with a typed error, never dropped:
1221                // a dropped response leaves the client waiting for its whole deadline.
1222                if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
1223                    s.event(ServiceEvent::FrameTooLarge {
1224                        conn: id,
1225                        method: method.clone(),
1226                        kind: "response",
1227                        limit,
1228                    });
1229                    // A client that opted in gets the result in pieces, within the message
1230                    // bound; everyone else, and anything beyond the bound, the typed error.
1231                    let held = chunked_ok
1232                        .then(|| serde_json::to_string(&v).ok())
1233                        .flatten()
1234                        .filter(|text| text.len() <= s.max_message);
1235                    drop(v);
1236                    match held {
1237                        Some(text) => {
1238                            downloads_made += 1;
1239                            let held = crate::chunk::Download::new(
1240                                format!("d{downloads_made}"),
1241                                text,
1242                                chunk_bytes,
1243                            );
1244                            let descriptor = held.descriptor();
1245                            download = Some(held);
1246                            send(&json!({"type": "response", "id": rid, "ok": true,
1247                                "result": Value::Null, "chunked": descriptor}));
1248                        }
1249                        None => respond_err(ServiceError::new(
1250                            ERROR_RESPONSE_TOO_LARGE,
1251                            format!(
1252                                "the result of {method} is larger than the {limit}-byte frame limit"
1253                            ),
1254                        )),
1255                    }
1256                }
1257            }
1258            Err(e) => respond_err(e),
1259        }
1260    }
1261    notify_closed();
1262    s.conns
1263        .lock()
1264        .unwrap_or_else(|e| e.into_inner())
1265        .remove(&id);
1266    shutdown();
1267}
1268
1269#[allow(dead_code)]
1270fn _assert_transport_used() {
1271    let _ = transport::current_uid;
1272}
1273
1274#[cfg(test)]
1275mod options_tests {
1276    use super::*;
1277
1278    #[test]
1279    fn pipe_instance_cap_is_small_finite_and_clamped() {
1280        let d = HostOptions::default();
1281        assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1282        assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1283        assert_eq!(
1284            HostOptions::new()
1285                .max_pipe_instances(4)
1286                .get_max_pipe_instances(),
1287            4
1288        );
1289        assert_eq!(
1290            HostOptions::new()
1291                .max_pipe_instances(0)
1292                .get_max_pipe_instances(),
1293            1
1294        );
1295        // 255 is PIPE_UNLIMITED_INSTANCES to Win32; never reachable.
1296        assert_eq!(
1297            HostOptions::new()
1298                .max_pipe_instances(u32::MAX)
1299                .get_max_pipe_instances(),
1300            254
1301        );
1302    }
1303}
1304
1305#[cfg(test)]
1306mod monitor_tests {
1307    use super::*;
1308    use std::sync::atomic::AtomicUsize;
1309
1310    /// Longer than any test: a wake-up that arrives is a signal, never the interval running out.
1311    const LONG: Duration = Duration::from_secs(600);
1312
1313    struct Quiet {
1314        handoffs: AtomicUsize,
1315    }
1316
1317    impl Handler for Quiet {
1318        fn call(
1319            &self,
1320            _cx: &CallContext,
1321            _method: &str,
1322            _params: &Value,
1323            _emit: &Emit,
1324        ) -> Result<Value, ServiceError> {
1325            Ok(json!({}))
1326        }
1327        fn on_handoff(&self) {
1328            self.handoffs.fetch_add(1, Ordering::SeqCst);
1329        }
1330    }
1331
1332    fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1333        let deadline = Instant::now() + Duration::from_secs(20);
1334        while !ok() {
1335            assert!(Instant::now() < deadline, "timed out waiting for {what}");
1336            std::thread::sleep(Duration::from_millis(5));
1337        }
1338    }
1339
1340    /// A host of rank 5 on a fresh suite, in this process.
1341    fn host_in(
1342        dir: &tempfile::TempDir,
1343        interval: Duration,
1344        alive: fn(u32) -> bool,
1345    ) -> (Suite, Hosted, Arc<Quiet>) {
1346        let suite = Suite::at(dir.path().join("suite")).unwrap();
1347        let mut cfg = ServiceConfig::new(
1348            "watch",
1349            "incumbent",
1350            5,
1351            "test",
1352            Allowlist::new().allow("*", ["echo"]),
1353        );
1354        cfg.takeover_timeout = Duration::from_secs(20);
1355        cfg.handoff_grace = Duration::from_secs(1);
1356        let handler = Arc::new(Quiet {
1357            handoffs: AtomicUsize::new(0),
1358        });
1359        let hosted = start_host(
1360            &suite,
1361            cfg,
1362            handler.clone(),
1363            HostOptions::default(),
1364            WatchTuning { interval, alive },
1365        )
1366        .expect("host starts");
1367        (suite, hosted, handler)
1368    }
1369
1370    fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1371        TakeoverRequest {
1372            role: role.into(),
1373            rank,
1374            pid,
1375            requested_at_ms: now_ms(),
1376        }
1377    }
1378
1379    /// A pid that is not this process's: the monitor ignores requests from its own process.
1380    fn other_pid() -> u32 {
1381        std::process::id().wrapping_add(1)
1382    }
1383
1384    #[test]
1385    fn a_request_from_another_process_is_found_by_the_file_check() {
1386        let dir = tempfile::tempdir().unwrap();
1387        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1388        // Written the way another process writes it: nothing in this process is told.
1389        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1390        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1391        assert!(
1392            hosted.wait_released(Duration::from_secs(20)),
1393            "the incumbent must hand off"
1394        );
1395        assert!(hosted.handoff_requested());
1396        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1397    }
1398
1399    #[test]
1400    fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1401        let dir = tempfile::tempdir().unwrap();
1402        let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1403        let bell = hosted.shared.bell.clone();
1404        wait_for("the first pass", || bell.passes() >= 1);
1405        write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1406        assert!(
1407            hosted.wait_released(Duration::from_secs(20)),
1408            "a 10-minute interval must not delay a request raised in this process"
1409        );
1410        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1411    }
1412
1413    #[test]
1414    fn releasing_the_host_ends_the_monitor_at_once() {
1415        let dir = tempfile::tempdir().unwrap();
1416        let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1417        let bell = hosted.shared.bell.clone();
1418        wait_for("the first pass", || bell.passes() >= 1);
1419        assert!(!bell.exited());
1420        hosted.shutdown();
1421        wait_for("the monitor thread to return", || bell.exited());
1422    }
1423
1424    #[test]
1425    fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1426        let dir = tempfile::tempdir().unwrap();
1427        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1428        let bell = hosted.shared.bell.clone();
1429        // Equal rank: never obliges.
1430        let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1431        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1432        wait_for("the request to be judged", || bell.reads() >= 1);
1433        let before = bell.wakes();
1434        wait_for("five more checks", || bell.wakes() >= before + 5);
1435        assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1436        assert!(!hosted.handoff_requested());
1437
1438        // Replacing it is noticed, and a better-ranked request is then accepted.
1439        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1440        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1441        assert!(hosted.wait_released(Duration::from_secs(20)));
1442        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1443    }
1444
1445    #[test]
1446    fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1447        let dir = tempfile::tempdir().unwrap();
1448        let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1449        let bell = hosted.shared.bell.clone();
1450        let (start, woke_before) = (Instant::now(), bell.wakes());
1451        std::thread::sleep(Duration::from_secs(2));
1452        let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1453        // The window can stretch on a loaded machine; the allowance stretches with it. The
1454        // 25 ms poll this replaces woke about 80 times in two seconds.
1455        let allowed = 3u64.max(secs.ceil() as u64 + 1);
1456        assert!(
1457            woke <= allowed,
1458            "the monitor woke {woke} times in {secs:.1} s"
1459        );
1460        drop(hosted);
1461    }
1462
1463    /// A directory at `takeover.json` makes the read fail with an error that is not `NotFound`
1464    /// (`IsADirectory` on Unix, `PermissionDenied` on Windows) on every platform.
1465    #[test]
1466    fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1467        let dir = tempfile::tempdir().unwrap();
1468        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1469        let bell = hosted.shared.bell.clone();
1470        let path = takeover_path(&suite, "watch");
1471        std::fs::create_dir_all(&path).unwrap();
1472        // The file stat never changes, yet it is read on every pass: it was never judged.
1473        wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1474        assert!(!hosted.handoff_requested());
1475
1476        // Once it can be read, the request it holds is accepted.
1477        std::fs::remove_dir(&path).unwrap();
1478        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1479        write_atomic(&path, &body).unwrap();
1480        assert!(hosted.wait_released(Duration::from_secs(20)));
1481        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1482    }
1483
1484    #[test]
1485    fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1486        let dir = tempfile::tempdir().unwrap();
1487        let suite = Suite::at(dir.path().join("suite")).unwrap();
1488        let record = record_path(&suite, "watch");
1489        std::fs::create_dir_all(&record).unwrap();
1490        let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1491        cfg.takeover_timeout = Duration::from_millis(300);
1492        let err = start_host(
1493            &suite,
1494            cfg,
1495            Arc::new(Quiet {
1496                handoffs: AtomicUsize::new(0),
1497            }),
1498            HostOptions::default(),
1499            WatchTuning {
1500                interval: LONG,
1501                alive: |_| true,
1502            },
1503        )
1504        .err()
1505        .expect("a record that cannot be read must not be treated as no record");
1506        match err {
1507            HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1508            _ => panic!("expected the read error"),
1509        }
1510        assert!(record.is_dir(), "left as it was");
1511    }
1512
1513    #[test]
1514    fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1515        let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1516        cfg.takeover_timeout = Duration::from_secs(10);
1517        cfg.known_roles.insert("legacy".into(), 1);
1518        let now = 1_000_000_000_000u64;
1519        let live: fn(u32) -> bool = |_| true;
1520        let dead: fn(u32) -> bool = |_| false;
1521        let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1522            role: role.into(),
1523            rank,
1524            pid,
1525            requested_at_ms,
1526        };
1527
1528        assert_eq!(
1529            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1530            Some(0)
1531        );
1532        // Equal or worse rank never obliges.
1533        assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1534        assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1535        // Our own process never does.
1536        assert_eq!(
1537            obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1538            None
1539        );
1540        // A requester that has exited abandoned its request.
1541        assert_eq!(
1542            obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1543            None
1544        );
1545        // Fresh means younger than the takeover timeout plus five seconds.
1546        assert_eq!(
1547            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1548            Some(0)
1549        );
1550        assert_eq!(
1551            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1552            None
1553        );
1554        // A peer that names only a role is ranked through `known_roles`; an unknown one never wins.
1555        assert_eq!(
1556            obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1557            Some(1)
1558        );
1559        assert_eq!(
1560            obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1561            None
1562        );
1563    }
1564}