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}
92
93impl ServiceConfig {
94    pub fn new(service: &str, role: &str, rank: u8, version: &str, allowlist: Allowlist) -> Self {
95        Self {
96            service: service.into(),
97            role: role.into(),
98            rank,
99            version: version.into(),
100            allowlist,
101            frame_limit: DEFAULT_FRAME_LIMIT,
102            takeover_timeout: Duration::from_secs(60),
103            handoff_grace: Duration::from_secs(30),
104            on_event: None,
105            known_roles: Default::default(),
106            endpoint: None,
107        }
108    }
109
110    /// Take the allowlist from a [`MethodTable`](crate::contract::MethodTable), the single source
111    /// of method policy.
112    pub fn with_methods(mut self, table: &crate::contract::MethodTable) -> Self {
113        self.allowlist = table.allowlist();
114        self
115    }
116}
117
118/// Host-side transport options, passed to [`host_with_options`]. `HostOptions::default()`
119/// is what [`host`] uses.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct HostOptions {
122    max_pipe_instances: u32,
123    write_timeout: Option<Duration>,
124}
125
126impl Default for HostOptions {
127    fn default() -> Self {
128        Self {
129            max_pipe_instances: DEFAULT_MAX_PIPE_INSTANCES,
130            write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
131        }
132    }
133}
134
135impl HostOptions {
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Cap on simultaneous instances of the Windows named pipe: live connections plus the
141    /// one instance waiting for the next client. A client that arrives when the cap is
142    /// reached gets "pipe busy" and retries, so a flood of connections cannot grow the
143    /// host without bound. Default [`DEFAULT_MAX_PIPE_INSTANCES`] (16); clamped to
144    /// `1..=254` (255 is "unlimited" to Win32 and is never used). Ignored on Unix, where
145    /// there is no pipe.
146    #[must_use]
147    pub fn max_pipe_instances(mut self, n: u32) -> Self {
148        self.max_pipe_instances = n.clamp(1, 254);
149        self
150    }
151
152    /// The effective (clamped) cap.
153    pub fn get_max_pipe_instances(&self) -> u32 {
154        self.max_pipe_instances
155    }
156
157    /// Longest one frame (hello, event, or response) may take to write and flush to a client.
158    /// A client that stops reading fills the transport buffer and would otherwise block the
159    /// writing thread, and the connection with it, forever; when the bound passes the host closes
160    /// that connection (the client sees `connection_closed`, never a partial frame it could
161    /// mistake for a reply), and the connection's `connection_closed` handler hook runs. The
162    /// bound is per frame, so a slow but progressing reader of a stream of events is not cut off
163    /// unless a single frame stalls. Default [`DEFAULT_WRITE_TIMEOUT`] (120 s); `None` restores
164    /// the earlier unbounded write. A zero duration is raised to 1 ms.
165    #[must_use]
166    pub fn write_timeout(mut self, bound: Option<Duration>) -> Self {
167        self.write_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
168        self
169    }
170
171    /// The effective write bound (`None` = unbounded).
172    pub fn get_write_timeout(&self) -> Option<Duration> {
173        self.write_timeout
174    }
175}
176
177#[derive(Debug)]
178pub enum HostError {
179    /// A live host of equal or better rank already serves this service.
180    Held(ServiceRecord),
181    /// The incumbent did not release within `takeover_timeout`.
182    TakeoverTimeout,
183    InvalidName(String),
184    Suite(SuiteError),
185    Io(std::io::Error),
186    /// No socket path within the OS limit can name this service (typed; carries path, length, limit).
187    Endpoint(crate::transport::EndpointError),
188}
189impl std::fmt::Display for HostError {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        match self {
192            HostError::Held(r) => write!(
193                f,
194                "{} is already hosted by {} (rank {}, pid {})",
195                r.service, r.role, r.rank, r.pid
196            ),
197            HostError::TakeoverTimeout => write!(f, "incumbent host did not release in time"),
198            HostError::InvalidName(s) => write!(f, "invalid service name {s:?}"),
199            HostError::Suite(e) => write!(f, "{e}"),
200            HostError::Io(e) => write!(f, "{e}"),
201            HostError::Endpoint(e) => write!(f, "{e}"),
202        }
203    }
204}
205impl std::error::Error for HostError {}
206impl From<std::io::Error> for HostError {
207    fn from(e: std::io::Error) -> Self {
208        HostError::Io(e)
209    }
210}
211impl From<SuiteError> for HostError {
212    fn from(e: SuiteError) -> Self {
213        HostError::Suite(e)
214    }
215}
216
217fn resolve_rank(cfg: &ServiceConfig, role: &str, rank: u8) -> u8 {
218    crate::discovery::resolve_rank(role, rank, &cfg.known_roles).unwrap_or(RANK_UNKNOWN)
219}
220
221struct Shared {
222    suite: Suite,
223    cfg: ServiceConfig,
224    handler: Arc<dyn Handler>,
225    listener: Listener,
226    // Held until endpoint cleanup completes; kernel releases it on process death.
227    election: Mutex<Option<std::fs::File>>,
228    record: ServiceRecord,
229    stop: AtomicBool,
230    handoff: Arc<AtomicBool>,
231    released: Mutex<bool>,
232    released_cv: Condvar,
233    inflight: AtomicUsize,
234    next_conn: AtomicU64,
235    conns: Mutex<HashMap<u64, Arc<dyn Fn() + Send + Sync>>>,
236    /// What the takeover monitor blocks on; `release` stops it.
237    bell: Arc<Bell>,
238    watch: WatchTuning,
239    write_timeout: Option<Duration>,
240}
241
242/// Bounds each frame write on one connection. The writer calls [`begin`](Self::begin) before and
243/// [`end`](Self::end) after a write+flush; a watcher thread closes the connection through the
244/// transport `shutdown` (which wakes a blocked Unix write and cancels pending Windows pipe I/O)
245/// when one write outlasts `bound`. Only the oldest unfinished write is timed, so a stream of
246/// writes that each finish in time is never cut.
247struct WriteBound {
248    state: Mutex<WriteState>,
249    cv: Condvar,
250}
251
252struct WriteState {
253    /// Deadline of the write in progress.
254    until: Option<Instant>,
255    done: bool,
256}
257
258struct FinishOnDrop(Arc<WriteBound>);
259impl Drop for FinishOnDrop {
260    fn drop(&mut self) {
261        self.0.finish();
262    }
263}
264
265impl WriteBound {
266    fn start(shutdown: Arc<dyn Fn() + Send + Sync>) -> Arc<WriteBound> {
267        let w = Arc::new(WriteBound {
268            state: Mutex::new(WriteState {
269                until: None,
270                done: false,
271            }),
272            cv: Condvar::new(),
273        });
274        let watcher = w.clone();
275        std::thread::spawn(move || {
276            let mut g = watcher.state.lock().unwrap_or_else(|e| e.into_inner());
277            loop {
278                if g.done {
279                    return;
280                }
281                match g.until {
282                    None => g = watcher.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
283                    Some(t) => {
284                        let left = t.saturating_duration_since(Instant::now());
285                        if left.is_zero() {
286                            g.until = None;
287                            drop(g);
288                            shutdown();
289                            return;
290                        }
291                        g = watcher
292                            .cv
293                            .wait_timeout(g, left)
294                            .unwrap_or_else(|e| e.into_inner())
295                            .0;
296                    }
297                }
298            }
299        });
300        w
301    }
302    fn set(&self, until: Option<Instant>) {
303        self.state.lock().unwrap_or_else(|e| e.into_inner()).until = until;
304        self.cv.notify_all();
305    }
306    fn finish(&self) {
307        self.state.lock().unwrap_or_else(|e| e.into_inner()).done = true;
308        self.cv.notify_all();
309    }
310}
311
312/// How the takeover monitor waits and how it tells that a requester is alive. Production uses
313/// [`WatchTuning::PRODUCTION`]; the tests lengthen or shorten the interval and fake the probe.
314#[derive(Clone, Copy)]
315struct WatchTuning {
316    /// How often a request written by another process is looked for.
317    interval: Duration,
318    /// Is the process that wrote a takeover request still running?
319    alive: fn(u32) -> bool,
320}
321
322impl WatchTuning {
323    const PRODUCTION: WatchTuning = WatchTuning {
324        interval: TAKEOVER_CHECK_INTERVAL,
325        alive: pid_alive,
326    };
327}
328
329impl Shared {
330    fn event(&self, e: ServiceEvent) {
331        if let Some(f) = &self.cfg.on_event {
332            f(&e);
333        }
334    }
335
336    fn release(&self) {
337        {
338            let mut r = self.released.lock().unwrap_or_else(|e| e.into_inner());
339            if *r {
340                return;
341            }
342            *r = true;
343        }
344        self.stop.store(true, Ordering::SeqCst);
345        // The takeover monitor is blocked on its bell, not sleeping: wake it so it leaves now.
346        self.bell.stop();
347        // Remove the record first so clients stop discovering this host, but only if it is ours.
348        if read_record(&self.suite, &self.cfg.service).is_some_and(|r| r.pid == self.record.pid) {
349            let _ = std::fs::remove_file(record_path(&self.suite, &self.cfg.service));
350        }
351        self.listener.wake();
352        self.listener.cleanup();
353        for (_, close) in self.conns.lock().unwrap_or_else(|e| e.into_inner()).drain() {
354            close();
355        }
356        self.election
357            .lock()
358            .unwrap_or_else(|e| e.into_inner())
359            .take();
360        self.event(ServiceEvent::Released);
361        self.released_cv.notify_all();
362    }
363}
364
365/// Runtime figures of a running host, computed on call by [`Hosted::status`].
366#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct ServiceStatus {
368    /// Connections open now.
369    pub connections: usize,
370    /// Requests being served now (the queue the handoff waits on).
371    pub inflight: usize,
372    /// A takeover was accepted and the host is draining.
373    pub handoff_requested: bool,
374    /// The host has released the service.
375    pub released: bool,
376    /// Resident memory, CPU time and I/O counters of the whole process (the host runs in it), or
377    /// `None` when the host cannot report them. See `rightkit_child_process::ProcessUsage`.
378    pub process: Option<rightkit_child_process::ProcessUsage>,
379}
380
381/// A running host. Dropping it releases the service.
382pub struct Hosted {
383    shared: Arc<Shared>,
384}
385
386impl Hosted {
387    pub fn record(&self) -> &ServiceRecord {
388        &self.shared.record
389    }
390    pub fn handoff_requested(&self) -> bool {
391        self.shared.handoff.load(Ordering::SeqCst)
392    }
393    pub fn inflight(&self) -> usize {
394        self.shared.inflight.load(Ordering::SeqCst)
395    }
396    /// Connections, in-flight requests and process resource usage, read now. No polling thread
397    /// runs for this.
398    pub fn status(&self) -> ServiceStatus {
399        ServiceStatus {
400            connections: self
401                .shared
402                .conns
403                .lock()
404                .unwrap_or_else(|e| e.into_inner())
405                .len(),
406            inflight: self.shared.inflight.load(Ordering::SeqCst),
407            handoff_requested: self.shared.handoff.load(Ordering::SeqCst),
408            released: *self
409                .shared
410                .released
411                .lock()
412                .unwrap_or_else(|e| e.into_inner()),
413            process: rightkit_child_process::process_usage().ok(),
414        }
415    }
416    /// Block until the host has released (clean shutdown or takeover).
417    pub fn wait_released(&self, timeout: Duration) -> bool {
418        let g = self
419            .shared
420            .released
421            .lock()
422            .unwrap_or_else(|e| e.into_inner());
423        let (g, _) = self
424            .shared
425            .released_cv
426            .wait_timeout_while(g, timeout, |r| !*r)
427            .unwrap_or_else(|e| e.into_inner());
428        *g
429    }
430    pub fn shutdown(&self) {
431        self.shared.release();
432    }
433}
434
435impl Drop for Hosted {
436    fn drop(&mut self) {
437        self.shared.release();
438    }
439}
440
441/// Host `cfg.service` on `suite`: discover, rank, take over if outranking,
442/// bind the current-user-only endpoint, publish `service.json`.
443// `HostError::Held` carries the incumbent's record by value so callers can match on it directly;
444// this runs once per host start, so the larger Err is not a hot-path cost.
445#[allow(clippy::result_large_err)]
446pub fn host(
447    suite: &Suite,
448    cfg: ServiceConfig,
449    handler: Arc<dyn Handler>,
450) -> Result<Hosted, HostError> {
451    host_with_options(suite, cfg, handler, HostOptions::default())
452}
453
454/// [`host`] with explicit [`HostOptions`].
455#[allow(clippy::result_large_err)]
456pub fn host_with_options(
457    suite: &Suite,
458    cfg: ServiceConfig,
459    handler: Arc<dyn Handler>,
460    options: HostOptions,
461) -> Result<Hosted, HostError> {
462    start_host(suite, cfg, handler, options, WatchTuning::PRODUCTION)
463}
464
465#[allow(clippy::result_large_err)]
466fn start_host(
467    suite: &Suite,
468    cfg: ServiceConfig,
469    handler: Arc<dyn Handler>,
470    options: HostOptions,
471    watch: WatchTuning,
472) -> Result<Hosted, HostError> {
473    if !valid_service_name(&cfg.service) {
474        return Err(HostError::InvalidName(cfg.service.clone()));
475    }
476    let dir = suite.service_dir(&cfg.service);
477    make_private_dir(&dir)?;
478    let me = std::process::id();
479    let deadline = Instant::now() + cfg.takeover_timeout;
480    let mut requested = false;
481    let lock_path = dir.join("election.lock");
482    let endpoint = match cfg.endpoint.clone() {
483        Some(e) => e,
484        None => try_endpoint_for(suite, &cfg.service).map_err(HostError::Endpoint)?,
485    };
486    let mut backoff = Duration::from_millis(5);
487    let (listener, election) = loop {
488        // Never unlink the lock file: every contender must lock the same inode.
489        // A loser can observe the incumbent, but only the owner can discard stale
490        // discovery or bind. Retain ownership through publication and shutdown.
491        let election = try_lock_exclusive(&lock_path)?;
492        // `Err` is "unknown", not "no record": it falls to the arm that waits, so a record that
493        // cannot be read for an instant is never removed and no listener is bound beside it.
494        let current = try_read_record(suite, &cfg.service);
495        let unreadable = current
496            .as_ref()
497            .err()
498            .map(|e| std::io::Error::new(e.kind(), e.to_string()));
499        match current {
500            Ok(Some(rec)) if record_process_matches(&rec) && probe_endpoint(&rec.endpoint) => {
501                let their = resolve_rank(&cfg, &rec.role, rec.rank);
502                if their == RANK_UNKNOWN || cfg.rank >= their {
503                    return Err(HostError::Held(rec));
504                }
505                if !requested {
506                    let t = TakeoverRequest {
507                        role: cfg.role.clone(),
508                        rank: cfg.rank,
509                        pid: me,
510                        requested_at_ms: now_ms(),
511                    };
512                    let request_path = takeover_path(suite, &cfg.service);
513                    write_atomic(&request_path, &serde_json::to_vec(&t).unwrap_or_default())?;
514                    // A monitor in this process (another host of this service) looks now.
515                    takeover_watch::poke(&request_path);
516                    requested = true;
517                }
518            }
519            Ok(_) if election.is_some() => {
520                // PID generation and endpoint checks rejected the old record.
521                // Only the elected process may remove it or a dead Unix socket.
522                let _ = std::fs::remove_file(record_path(suite, &cfg.service));
523                match Listener::bind(&endpoint, options.max_pipe_instances) {
524                    Ok(listener) => break (listener, election.unwrap()),
525                    // A legacy host may have bound without publishing yet, or a
526                    // Windows incumbent may still be closing its pipe handles.
527                    Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {}
528                    Err(e) => return Err(HostError::Io(e)),
529                }
530            }
531            // Winner is between lock acquisition and record publication, or the record could
532            // not be read (see above).
533            _ => {}
534        }
535        drop(election);
536        let remaining = deadline.saturating_duration_since(Instant::now());
537        if remaining.is_zero() {
538            clear_takeover_if_owner(suite, &cfg.service, me);
539            // Still unable to read the record at the deadline: say so rather than report a
540            // timeout that blames the incumbent.
541            return Err(match unreadable {
542                Some(e) => HostError::Io(e),
543                None => HostError::TakeoverTimeout,
544            });
545        }
546        std::thread::sleep(backoff.min(remaining));
547        backoff = (backoff * 2).min(Duration::from_millis(50));
548    };
549    let record = ServiceRecord {
550        protocol_major: PROTOCOL_MAJOR,
551        protocol_minor: PROTOCOL_MINOR,
552        service: cfg.service.clone(),
553        role: cfg.role.clone(),
554        rank: cfg.rank,
555        pid: me,
556        endpoint: endpoint.clone(),
557        version: cfg.version.clone(),
558        engine_version: cfg.version.clone(),
559        started_ms: now_ms(),
560        process_started_ms: crate::discovery::own_start_ms().unwrap_or(0),
561    };
562    if let Err(e) = write_atomic(
563        &record_path(suite, &cfg.service),
564        &serde_json::to_vec_pretty(&record).unwrap_or_default(),
565    ) {
566        listener.cleanup();
567        return Err(HostError::Io(e));
568    }
569    if read_takeover(suite, &cfg.service).is_some_and(|t| t.pid == me) {
570        let _ = std::fs::remove_file(takeover_path(suite, &cfg.service));
571    }
572    let shared = Arc::new(Shared {
573        suite: suite.clone(),
574        cfg,
575        handler,
576        listener,
577        election: Mutex::new(Some(election)),
578        record,
579        stop: AtomicBool::new(false),
580        handoff: Arc::new(AtomicBool::new(false)),
581        released: Mutex::new(false),
582        released_cv: Condvar::new(),
583        inflight: AtomicUsize::new(0),
584        next_conn: AtomicU64::new(1),
585        conns: Mutex::new(HashMap::new()),
586        bell: Arc::new(Bell::new()),
587        watch,
588        write_timeout: options.write_timeout,
589    });
590    takeover_watch::register(
591        takeover_path(&shared.suite, &shared.cfg.service),
592        &shared.bell,
593    );
594    shared.event(ServiceEvent::Started {
595        service: shared.cfg.service.clone(),
596        endpoint,
597        rank: shared.cfg.rank,
598    });
599    let s = shared.clone();
600    if let Err(e) = std::thread::Builder::new()
601        .name("rk-svc-accept".into())
602        .spawn(move || accept_loop(s))
603    {
604        shared.release();
605        return Err(HostError::Io(e));
606    }
607    let s = shared.clone();
608    if let Err(e) = std::thread::Builder::new()
609        .name("rk-svc-takeover".into())
610        .spawn(move || takeover_monitor(s))
611    {
612        shared.release();
613        return Err(HostError::Io(e));
614    }
615    Ok(Hosted { shared })
616}
617
618pub(crate) fn now_ms() -> u64 {
619    std::time::SystemTime::now()
620        .duration_since(std::time::UNIX_EPOCH)
621        .map(|d| d.as_millis() as u64)
622        .unwrap_or(0)
623}
624
625fn accept_loop(s: Arc<Shared>) {
626    while !s.stop.load(Ordering::SeqCst) {
627        match s.listener.accept() {
628            Ok(Accepted::Conn(conn)) => {
629                if s.stop.load(Ordering::SeqCst) {
630                    break;
631                }
632                let s2 = s.clone();
633                let _ = std::thread::Builder::new()
634                    .name("rk-svc-conn".into())
635                    .spawn(move || serve_conn(s2, conn));
636            }
637            // Wrong user (or unidentifiable): already closed unread. Log it, except for the
638            // shutdown wake-up, which is our own connection.
639            Ok(Accepted::Rejected(reason)) => {
640                if !s.stop.load(Ordering::SeqCst) {
641                    s.event(ServiceEvent::PeerRejected { reason });
642                }
643            }
644            Ok(Accepted::Vanished) => {}
645            Err(_) => {
646                if s.stop.load(Ordering::SeqCst) {
647                    break;
648                }
649                std::thread::sleep(Duration::from_millis(20));
650            }
651        }
652    }
653}
654
655/// What `takeover.json` looked like when it was last judged: modification time and length.
656type FileStamp = (Option<SystemTime>, u64);
657
658fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
659    let md = std::fs::metadata(path).ok()?;
660    Some((md.modified().ok(), md.len()))
661}
662
663/// The requester's rank when `t` obliges a host of `cfg.rank` (running as `own_pid`) to hand
664/// off: a known rank that beats ours, from another live process, and fresh. `None` otherwise.
665fn obliging_rank(
666    cfg: &ServiceConfig,
667    own_pid: u32,
668    alive: fn(u32) -> bool,
669    t: &TakeoverRequest,
670    now: u64,
671) -> Option<u8> {
672    let max_age = cfg.takeover_timeout.as_millis() as u64;
673    let fresh = now.saturating_sub(t.requested_at_ms) <= max_age + 5_000;
674    let their = resolve_rank(cfg, &t.role, t.rank);
675    let obliges =
676        their != RANK_UNKNOWN && their < cfg.rank && t.pid != own_pid && alive(t.pid) && fresh;
677    obliges.then_some(their)
678}
679
680#[cfg(test)]
681struct ExitMark<'a>(&'a Bell);
682
683#[cfg(test)]
684impl Drop for ExitMark<'_> {
685    fn drop(&mut self) {
686        self.0.mark_exited();
687    }
688}
689
690/// Watch for a takeover request from a higher-ranked live host.
691///
692/// The thread blocks on `s.bell`: it wakes when the host releases (leave), when this process wrote
693/// a takeover request for the service (look now), and otherwise once per `s.watch.interval` to
694/// stat `takeover.json`, which is how a request written by another process is found. The file is
695/// read only when its modification time or length differs from the one already judged (or on an
696/// in-process poke), and a request that was judged not to oblige is not judged again until the
697/// file changes. That is sound because the verdict can only turn from "obliges" to "does not" with
698/// time (a request goes stale, a requester exits), never the other way.
699fn takeover_monitor(s: Arc<Shared>) {
700    #[cfg(test)]
701    let _exit = ExitMark(&s.bell);
702    let path = takeover_path(&s.suite, &s.cfg.service);
703    let mut judged: Option<FileStamp> = None;
704    let mut look_now = false;
705    loop {
706        if s.stop.load(Ordering::SeqCst) {
707            return;
708        }
709        let force = std::mem::take(&mut look_now);
710        match file_stamp(&path) {
711            None => judged = None,
712            Some(stamp) if force || judged != Some(stamp) => {
713                judged = Some(stamp);
714                #[cfg(test)]
715                s.bell.note_read();
716                match try_read_takeover(&s.suite, &s.cfg.service) {
717                    Ok(Some(t)) => {
718                        let now = now_ms();
719                        let obliging = obliging_rank(&s.cfg, s.record.pid, s.watch.alive, &t, now);
720                        if let Some(their) = obliging {
721                            hand_off(&s, t, their);
722                            return;
723                        }
724                    }
725                    Ok(None) => {}
726                    // Could not be read: whether a request exists is unknown, not "none". Do not
727                    // mark this stamp as judged, so the next pass (one interval on, no faster)
728                    // reads the file again.
729                    Err(_) => judged = None,
730                }
731            }
732            Some(_) => {}
733        }
734        #[cfg(test)]
735        s.bell.note_pass();
736        match s.bell.wait(s.watch.interval) {
737            Wake::Stop => return,
738            Wake::Poke => look_now = true,
739            Wake::Timeout => {}
740        }
741    }
742}
743
744/// Accept a takeover: stop admitting work, let in-flight requests reach a boundary (up to
745/// `handoff_grace`), then release.
746fn hand_off(s: &Shared, t: TakeoverRequest, their: u8) {
747    s.handoff.store(true, Ordering::SeqCst);
748    s.event(ServiceEvent::HandoffRequested {
749        by_role: t.role,
750        by_rank: their,
751    });
752    s.handler.on_handoff();
753    let until = Instant::now() + s.cfg.handoff_grace;
754    while s.inflight.load(Ordering::SeqCst) > 0
755        && Instant::now() < until
756        && !s.stop.load(Ordering::SeqCst)
757    {
758        std::thread::sleep(Duration::from_millis(10));
759    }
760    s.release();
761}
762
763fn frame_err(e: &FrameError) -> ServiceError {
764    ServiceError::new("bad_frame", e.to_string())
765}
766
767fn serve_conn(s: Arc<Shared>, conn: Conn) {
768    let Conn {
769        reader,
770        writer,
771        peer_closed,
772        shutdown,
773        peer,
774    } = conn;
775    let mut reader = BufReader::new(reader);
776    let writer = Arc::new(Mutex::new(writer));
777    let limit = s.cfg.frame_limit;
778    let bound = s
779        .write_timeout
780        .map(|d| (d, WriteBound::start(shutdown.clone())));
781    // Ends the watcher on every way out of this function, early returns included.
782    let _watcher_ends = bound.as_ref().map(|(_, b)| FinishOnDrop(b.clone()));
783    // Write one frame. `false` only when the frame is larger than the limit: nothing was
784    // written, so the stream is still on a frame boundary. An I/O failure means the peer is
785    // gone, which the read loop and the peer watch already report.
786    let write = {
787        let w = writer.clone();
788        let bound = bound.clone();
789        move |v: &Value| -> bool {
790            let mut w = w.lock().unwrap_or_else(|e| e.into_inner());
791            if let Some((d, b)) = &bound {
792                b.set(Some(Instant::now() + *d));
793            }
794            let r = match write_json_line(&mut *w, v, limit) {
795                Ok(()) => {
796                    let _ = w.flush();
797                    true
798                }
799                Err(FrameError::TooLarge { .. }) => false,
800                Err(_) => true,
801            };
802            if let Some((_, b)) = &bound {
803                b.set(None);
804            }
805            r
806        }
807    };
808    let send = {
809        let write = write.clone();
810        move |v: &Value| {
811            write(v);
812        }
813    };
814    let mut buf = Vec::new();
815    // ---- hello
816    let hello: Value = match read_json_line(&mut reader, &mut buf, 64 * 1024) {
817        Ok(Some(v)) => v,
818        _ => return, // probe or garbage
819    };
820    let app = hello["app"].as_str().unwrap_or("").to_string();
821    let reject = |reason: &str, code: &str, retryable: bool| {
822        let e = ServiceError {
823            code: code.into(),
824            message: reason.into(),
825            retryable,
826        };
827        send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
828        s.event(ServiceEvent::ConnectionRejected {
829            app: app.clone(),
830            reason: reason.into(),
831        });
832        shutdown();
833    };
834    if hello["type"] != "hello" || app.is_empty() {
835        return reject(
836            "first frame must be hello with an app label",
837            "bad_hello",
838            false,
839        );
840    }
841    if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
842        return reject("protocol major mismatch", "protocol_mismatch", false);
843    }
844    if s.handoff.load(Ordering::SeqCst) {
845        return reject(
846            "host is handing off",
847            crate::error::ERROR_HOST_HANDOFF,
848            true,
849        );
850    }
851    if !s.cfg.allowlist.app_known(&app) {
852        return reject(
853            "app is not on the service allowlist",
854            crate::error::ERROR_NOT_ALLOWED,
855            false,
856        );
857    }
858    let id = s.next_conn.fetch_add(1, Ordering::SeqCst);
859    s.conns
860        .lock()
861        .unwrap_or_else(|e| e.into_inner())
862        .insert(id, shutdown.clone());
863    s.event(ServiceEvent::ConnectionOpened {
864        conn: id,
865        app: app.clone(),
866    });
867    send(&json!({
868        "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
869        "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
870        "methods": s.cfg.allowlist.methods_for(&app),
871    }));
872
873    let caller = Caller::local_app(app.clone(), peer.clone());
874    let closed_notified = Arc::new(AtomicBool::new(false));
875    let notify_closed = {
876        let (s, flag) = (s.clone(), closed_notified.clone());
877        move || {
878            if !flag.swap(true, Ordering::SeqCst) {
879                s.handler.connection_closed(id);
880            }
881        }
882    };
883    // ---- requests, lock-step: one in flight per connection
884    loop {
885        let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
886            Ok(Some(v)) => v,
887            Ok(None) | Err(FrameError::Io(_)) => break,
888            Err(e) => {
889                send(
890                    &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
891                );
892                continue;
893            }
894        };
895        let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
896            send(
897                &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
898            );
899            continue;
900        };
901        let respond_err = |e: ServiceError| {
902            send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
903        };
904        let method = req["method"].as_str().unwrap_or("").to_string();
905        if s.handoff.load(Ordering::SeqCst) {
906            respond_err(ServiceError::handoff());
907            continue;
908        }
909        if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
910            s.event(ServiceEvent::Denied {
911                conn: id,
912                app: app.clone(),
913                method: method.clone(),
914            });
915            respond_err(e);
916            continue;
917        }
918        if method == "ping" {
919            send(
920                &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
921            );
922            continue;
923        }
924        let params = req.get("params").cloned().unwrap_or_else(|| json!({}));
925        let cx = new_context(
926            id,
927            caller.clone(),
928            Arc::new(AtomicBool::new(false)),
929            s.handoff.clone(),
930        );
931        let emit: Emit = {
932            let (write, s, method) = (write.clone(), s.clone(), method.clone());
933            Arc::new(move |ev| {
934                if !write(&json!({"type": "event", "id": rid, "event": ev})) {
935                    s.event(ServiceEvent::FrameTooLarge {
936                        conn: id,
937                        method: method.clone(),
938                        kind: "event",
939                        limit,
940                    });
941                }
942            })
943        };
944        s.inflight.fetch_add(1, Ordering::SeqCst);
945        let done = AtomicBool::new(false);
946        let result = std::thread::scope(|sc| {
947            sc.spawn(|| {
948                while !done.load(Ordering::SeqCst) {
949                    if peer_closed() {
950                        cx.closed.store(true, Ordering::SeqCst);
951                        notify_closed();
952                        return;
953                    }
954                    std::thread::sleep(Duration::from_millis(40));
955                }
956            });
957            let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
958                s.handler.call(&cx, &method, &params, &emit)
959            }))
960            .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
961            done.store(true, Ordering::SeqCst);
962            r
963        });
964        s.inflight.fetch_sub(1, Ordering::SeqCst);
965        if cx.peer_closed() {
966            break;
967        }
968        match result {
969            Ok(v) => {
970                // A result that does not fit is answered with a typed error, never dropped:
971                // a dropped response leaves the client waiting for its whole deadline.
972                if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
973                    s.event(ServiceEvent::FrameTooLarge {
974                        conn: id,
975                        method: method.clone(),
976                        kind: "response",
977                        limit,
978                    });
979                    respond_err(ServiceError::new(
980                        ERROR_RESPONSE_TOO_LARGE,
981                        format!(
982                            "the result of {method} is larger than the {limit}-byte frame limit"
983                        ),
984                    ));
985                }
986            }
987            Err(e) => respond_err(e),
988        }
989    }
990    notify_closed();
991    s.conns
992        .lock()
993        .unwrap_or_else(|e| e.into_inner())
994        .remove(&id);
995    shutdown();
996}
997
998#[allow(dead_code)]
999fn _assert_transport_used() {
1000    let _ = transport::current_uid;
1001}
1002
1003#[cfg(test)]
1004mod options_tests {
1005    use super::*;
1006
1007    #[test]
1008    fn pipe_instance_cap_is_small_finite_and_clamped() {
1009        let d = HostOptions::default();
1010        assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1011        assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1012        assert_eq!(
1013            HostOptions::new()
1014                .max_pipe_instances(4)
1015                .get_max_pipe_instances(),
1016            4
1017        );
1018        assert_eq!(
1019            HostOptions::new()
1020                .max_pipe_instances(0)
1021                .get_max_pipe_instances(),
1022            1
1023        );
1024        // 255 is PIPE_UNLIMITED_INSTANCES to Win32; never reachable.
1025        assert_eq!(
1026            HostOptions::new()
1027                .max_pipe_instances(u32::MAX)
1028                .get_max_pipe_instances(),
1029            254
1030        );
1031    }
1032}
1033
1034#[cfg(test)]
1035mod monitor_tests {
1036    use super::*;
1037    use std::sync::atomic::AtomicUsize;
1038
1039    /// Longer than any test: a wake-up that arrives is a signal, never the interval running out.
1040    const LONG: Duration = Duration::from_secs(600);
1041
1042    struct Quiet {
1043        handoffs: AtomicUsize,
1044    }
1045
1046    impl Handler for Quiet {
1047        fn call(
1048            &self,
1049            _cx: &CallContext,
1050            _method: &str,
1051            _params: &Value,
1052            _emit: &Emit,
1053        ) -> Result<Value, ServiceError> {
1054            Ok(json!({}))
1055        }
1056        fn on_handoff(&self) {
1057            self.handoffs.fetch_add(1, Ordering::SeqCst);
1058        }
1059    }
1060
1061    fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1062        let deadline = Instant::now() + Duration::from_secs(20);
1063        while !ok() {
1064            assert!(Instant::now() < deadline, "timed out waiting for {what}");
1065            std::thread::sleep(Duration::from_millis(5));
1066        }
1067    }
1068
1069    /// A host of rank 5 on a fresh suite, in this process.
1070    fn host_in(
1071        dir: &tempfile::TempDir,
1072        interval: Duration,
1073        alive: fn(u32) -> bool,
1074    ) -> (Suite, Hosted, Arc<Quiet>) {
1075        let suite = Suite::at(dir.path().join("suite")).unwrap();
1076        let mut cfg = ServiceConfig::new(
1077            "watch",
1078            "incumbent",
1079            5,
1080            "test",
1081            Allowlist::new().allow("*", ["echo"]),
1082        );
1083        cfg.takeover_timeout = Duration::from_secs(20);
1084        cfg.handoff_grace = Duration::from_secs(1);
1085        let handler = Arc::new(Quiet {
1086            handoffs: AtomicUsize::new(0),
1087        });
1088        let hosted = start_host(
1089            &suite,
1090            cfg,
1091            handler.clone(),
1092            HostOptions::default(),
1093            WatchTuning { interval, alive },
1094        )
1095        .expect("host starts");
1096        (suite, hosted, handler)
1097    }
1098
1099    fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1100        TakeoverRequest {
1101            role: role.into(),
1102            rank,
1103            pid,
1104            requested_at_ms: now_ms(),
1105        }
1106    }
1107
1108    /// A pid that is not this process's: the monitor ignores requests from its own process.
1109    fn other_pid() -> u32 {
1110        std::process::id().wrapping_add(1)
1111    }
1112
1113    #[test]
1114    fn a_request_from_another_process_is_found_by_the_file_check() {
1115        let dir = tempfile::tempdir().unwrap();
1116        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1117        // Written the way another process writes it: nothing in this process is told.
1118        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1119        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1120        assert!(
1121            hosted.wait_released(Duration::from_secs(20)),
1122            "the incumbent must hand off"
1123        );
1124        assert!(hosted.handoff_requested());
1125        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1126    }
1127
1128    #[test]
1129    fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1130        let dir = tempfile::tempdir().unwrap();
1131        let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1132        let bell = hosted.shared.bell.clone();
1133        wait_for("the first pass", || bell.passes() >= 1);
1134        write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1135        assert!(
1136            hosted.wait_released(Duration::from_secs(20)),
1137            "a 10-minute interval must not delay a request raised in this process"
1138        );
1139        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1140    }
1141
1142    #[test]
1143    fn releasing_the_host_ends_the_monitor_at_once() {
1144        let dir = tempfile::tempdir().unwrap();
1145        let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1146        let bell = hosted.shared.bell.clone();
1147        wait_for("the first pass", || bell.passes() >= 1);
1148        assert!(!bell.exited());
1149        hosted.shutdown();
1150        wait_for("the monitor thread to return", || bell.exited());
1151    }
1152
1153    #[test]
1154    fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1155        let dir = tempfile::tempdir().unwrap();
1156        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1157        let bell = hosted.shared.bell.clone();
1158        // Equal rank: never obliges.
1159        let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1160        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1161        wait_for("the request to be judged", || bell.reads() >= 1);
1162        let before = bell.wakes();
1163        wait_for("five more checks", || bell.wakes() >= before + 5);
1164        assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1165        assert!(!hosted.handoff_requested());
1166
1167        // Replacing it is noticed, and a better-ranked request is then accepted.
1168        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1169        write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1170        assert!(hosted.wait_released(Duration::from_secs(20)));
1171        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1172    }
1173
1174    #[test]
1175    fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1176        let dir = tempfile::tempdir().unwrap();
1177        let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1178        let bell = hosted.shared.bell.clone();
1179        let (start, woke_before) = (Instant::now(), bell.wakes());
1180        std::thread::sleep(Duration::from_secs(2));
1181        let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1182        // The window can stretch on a loaded machine; the allowance stretches with it. The
1183        // 25 ms poll this replaces woke about 80 times in two seconds.
1184        let allowed = 3u64.max(secs.ceil() as u64 + 1);
1185        assert!(
1186            woke <= allowed,
1187            "the monitor woke {woke} times in {secs:.1} s"
1188        );
1189        drop(hosted);
1190    }
1191
1192    /// A directory at `takeover.json` makes the read fail with an error that is not `NotFound`
1193    /// (`IsADirectory` on Unix, `PermissionDenied` on Windows) on every platform.
1194    #[test]
1195    fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1196        let dir = tempfile::tempdir().unwrap();
1197        let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1198        let bell = hosted.shared.bell.clone();
1199        let path = takeover_path(&suite, "watch");
1200        std::fs::create_dir_all(&path).unwrap();
1201        // The file stat never changes, yet it is read on every pass: it was never judged.
1202        wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1203        assert!(!hosted.handoff_requested());
1204
1205        // Once it can be read, the request it holds is accepted.
1206        std::fs::remove_dir(&path).unwrap();
1207        let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1208        write_atomic(&path, &body).unwrap();
1209        assert!(hosted.wait_released(Duration::from_secs(20)));
1210        assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1211    }
1212
1213    #[test]
1214    fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1215        let dir = tempfile::tempdir().unwrap();
1216        let suite = Suite::at(dir.path().join("suite")).unwrap();
1217        let record = record_path(&suite, "watch");
1218        std::fs::create_dir_all(&record).unwrap();
1219        let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1220        cfg.takeover_timeout = Duration::from_millis(300);
1221        let err = start_host(
1222            &suite,
1223            cfg,
1224            Arc::new(Quiet {
1225                handoffs: AtomicUsize::new(0),
1226            }),
1227            HostOptions::default(),
1228            WatchTuning {
1229                interval: LONG,
1230                alive: |_| true,
1231            },
1232        )
1233        .err()
1234        .expect("a record that cannot be read must not be treated as no record");
1235        match err {
1236            HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1237            _ => panic!("expected the read error"),
1238        }
1239        assert!(record.is_dir(), "left as it was");
1240    }
1241
1242    #[test]
1243    fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1244        let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1245        cfg.takeover_timeout = Duration::from_secs(10);
1246        cfg.known_roles.insert("legacy".into(), 1);
1247        let now = 1_000_000_000_000u64;
1248        let live: fn(u32) -> bool = |_| true;
1249        let dead: fn(u32) -> bool = |_| false;
1250        let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1251            role: role.into(),
1252            rank,
1253            pid,
1254            requested_at_ms,
1255        };
1256
1257        assert_eq!(
1258            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1259            Some(0)
1260        );
1261        // Equal or worse rank never obliges.
1262        assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1263        assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1264        // Our own process never does.
1265        assert_eq!(
1266            obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1267            None
1268        );
1269        // A requester that has exited abandoned its request.
1270        assert_eq!(
1271            obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1272            None
1273        );
1274        // Fresh means younger than the takeover timeout plus five seconds.
1275        assert_eq!(
1276            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1277            Some(0)
1278        );
1279        assert_eq!(
1280            obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1281            None
1282        );
1283        // A peer that names only a role is ranked through `known_roles`; an unknown one never wins.
1284        assert_eq!(
1285            obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1286            Some(1)
1287        );
1288        assert_eq!(
1289            obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1290            None
1291        );
1292    }
1293}