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