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