1use crate::contract::{new_context, Caller};
2pub use crate::contract::{CallContext, Emit, Handler};
3use crate::discovery::{record_process_matches, try_lock_exclusive};
4use crate::error::{ServiceError, ERROR_RESPONSE_TOO_LARGE};
5use crate::record::*;
6use crate::suite::{make_private_dir, valid_service_name, Suite, SuiteError};
7use crate::takeover_watch::{self, Bell, Wake, TAKEOVER_CHECK_INTERVAL};
8use crate::transport::{
9 self, pid_alive, probe_endpoint, try_endpoint_for, Accepted, Conn, Listener,
10 DEFAULT_MAX_PIPE_INSTANCES,
11};
12use rightkit_sidecar::{read_json_line, write_json_line, FrameError};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15use std::io::{BufReader, Write};
16use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
17use std::sync::{Arc, Condvar, Mutex};
18use std::time::{Duration, Instant, SystemTime};
19
20pub const DEFAULT_FRAME_LIMIT: usize = 16 * 1024 * 1024;
21
22pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(120);
25
26#[derive(Debug, Clone, PartialEq)]
28pub enum ServiceEvent {
29 Started {
30 service: String,
31 endpoint: String,
32 rank: u8,
33 },
34 ConnectionOpened {
35 conn: u64,
36 app: String,
37 },
38 ConnectionRejected {
39 app: String,
40 reason: String,
41 },
42 PeerRejected {
46 reason: String,
47 },
48 Denied {
49 conn: u64,
50 app: String,
51 method: String,
52 },
53 HandoffRequested {
54 by_role: String,
55 by_rank: u8,
56 },
57 FrameTooLarge {
61 conn: u64,
62 method: String,
63 kind: &'static str,
64 limit: usize,
65 },
66 Released,
67}
68
69pub type EventFn = Arc<dyn Fn(&ServiceEvent) + Send + Sync>;
71
72pub struct ServiceConfig {
73 pub service: String,
74 pub role: String,
75 pub rank: u8,
76 pub version: String,
77 pub allowlist: Allowlist,
78 pub frame_limit: usize,
79 pub takeover_timeout: Duration,
81 pub handoff_grace: Duration,
83 pub on_event: Option<EventFn>,
84 pub known_roles: std::collections::BTreeMap<String, u8>,
88 pub endpoint: Option<String>,
91}
92
93impl ServiceConfig {
94 pub fn new(service: &str, role: &str, rank: u8, version: &str, allowlist: Allowlist) -> Self {
95 Self {
96 service: service.into(),
97 role: role.into(),
98 rank,
99 version: version.into(),
100 allowlist,
101 frame_limit: DEFAULT_FRAME_LIMIT,
102 takeover_timeout: Duration::from_secs(60),
103 handoff_grace: Duration::from_secs(30),
104 on_event: None,
105 known_roles: Default::default(),
106 endpoint: None,
107 }
108 }
109
110 pub fn with_methods(mut self, table: &crate::contract::MethodTable) -> Self {
113 self.allowlist = table.allowlist();
114 self
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct HostOptions {
122 max_pipe_instances: u32,
123 write_timeout: Option<Duration>,
124}
125
126impl Default for HostOptions {
127 fn default() -> Self {
128 Self {
129 max_pipe_instances: DEFAULT_MAX_PIPE_INSTANCES,
130 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
131 }
132 }
133}
134
135impl HostOptions {
136 pub fn new() -> Self {
137 Self::default()
138 }
139
140 #[must_use]
147 pub fn max_pipe_instances(mut self, n: u32) -> Self {
148 self.max_pipe_instances = n.clamp(1, 254);
149 self
150 }
151
152 pub fn get_max_pipe_instances(&self) -> u32 {
154 self.max_pipe_instances
155 }
156
157 #[must_use]
166 pub fn write_timeout(mut self, bound: Option<Duration>) -> Self {
167 self.write_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
168 self
169 }
170
171 pub fn get_write_timeout(&self) -> Option<Duration> {
173 self.write_timeout
174 }
175}
176
177#[derive(Debug)]
178pub enum HostError {
179 Held(ServiceRecord),
181 TakeoverTimeout,
183 InvalidName(String),
184 Suite(SuiteError),
185 Io(std::io::Error),
186 Endpoint(crate::transport::EndpointError),
188}
189impl std::fmt::Display for HostError {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 match self {
192 HostError::Held(r) => write!(
193 f,
194 "{} is already hosted by {} (rank {}, pid {})",
195 r.service, r.role, r.rank, r.pid
196 ),
197 HostError::TakeoverTimeout => write!(f, "incumbent host did not release in time"),
198 HostError::InvalidName(s) => write!(f, "invalid service name {s:?}"),
199 HostError::Suite(e) => write!(f, "{e}"),
200 HostError::Io(e) => write!(f, "{e}"),
201 HostError::Endpoint(e) => write!(f, "{e}"),
202 }
203 }
204}
205impl std::error::Error for HostError {}
206impl From<std::io::Error> for HostError {
207 fn from(e: std::io::Error) -> Self {
208 HostError::Io(e)
209 }
210}
211impl From<SuiteError> for HostError {
212 fn from(e: SuiteError) -> Self {
213 HostError::Suite(e)
214 }
215}
216
217fn resolve_rank(cfg: &ServiceConfig, role: &str, rank: u8) -> u8 {
218 crate::discovery::resolve_rank(role, rank, &cfg.known_roles).unwrap_or(RANK_UNKNOWN)
219}
220
221struct Shared {
222 suite: Suite,
223 cfg: ServiceConfig,
224 handler: Arc<dyn Handler>,
225 listener: Listener,
226 election: Mutex<Option<std::fs::File>>,
228 record: ServiceRecord,
229 stop: AtomicBool,
230 handoff: Arc<AtomicBool>,
231 released: Mutex<bool>,
232 released_cv: Condvar,
233 inflight: AtomicUsize,
234 next_conn: AtomicU64,
235 conns: Mutex<HashMap<u64, Arc<dyn Fn() + Send + Sync>>>,
236 bell: Arc<Bell>,
238 watch: WatchTuning,
239 write_timeout: Option<Duration>,
240}
241
242struct WriteBound {
248 state: Mutex<WriteState>,
249 cv: Condvar,
250}
251
252struct WriteState {
253 until: Option<Instant>,
255 done: bool,
256}
257
258struct FinishOnDrop(Arc<WriteBound>);
259impl Drop for FinishOnDrop {
260 fn drop(&mut self) {
261 self.0.finish();
262 }
263}
264
265impl WriteBound {
266 fn start(shutdown: Arc<dyn Fn() + Send + Sync>) -> Arc<WriteBound> {
267 let w = Arc::new(WriteBound {
268 state: Mutex::new(WriteState {
269 until: None,
270 done: false,
271 }),
272 cv: Condvar::new(),
273 });
274 let watcher = w.clone();
275 std::thread::spawn(move || {
276 let mut g = watcher.state.lock().unwrap_or_else(|e| e.into_inner());
277 loop {
278 if g.done {
279 return;
280 }
281 match g.until {
282 None => g = watcher.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
283 Some(t) => {
284 let left = t.saturating_duration_since(Instant::now());
285 if left.is_zero() {
286 g.until = None;
287 drop(g);
288 shutdown();
289 return;
290 }
291 g = watcher
292 .cv
293 .wait_timeout(g, left)
294 .unwrap_or_else(|e| e.into_inner())
295 .0;
296 }
297 }
298 }
299 });
300 w
301 }
302 fn set(&self, until: Option<Instant>) {
303 self.state.lock().unwrap_or_else(|e| e.into_inner()).until = until;
304 self.cv.notify_all();
305 }
306 fn finish(&self) {
307 self.state.lock().unwrap_or_else(|e| e.into_inner()).done = true;
308 self.cv.notify_all();
309 }
310}
311
312#[derive(Clone, Copy)]
315struct WatchTuning {
316 interval: Duration,
318 alive: fn(u32) -> bool,
320}
321
322impl WatchTuning {
323 const PRODUCTION: WatchTuning = WatchTuning {
324 interval: TAKEOVER_CHECK_INTERVAL,
325 alive: pid_alive,
326 };
327}
328
329impl Shared {
330 fn event(&self, e: ServiceEvent) {
331 if let Some(f) = &self.cfg.on_event {
332 f(&e);
333 }
334 }
335
336 fn release(&self) {
337 {
338 let mut r = self.released.lock().unwrap_or_else(|e| e.into_inner());
339 if *r {
340 return;
341 }
342 *r = true;
343 }
344 self.stop.store(true, Ordering::SeqCst);
345 self.bell.stop();
347 if read_record(&self.suite, &self.cfg.service).is_some_and(|r| r.pid == self.record.pid) {
349 let _ = std::fs::remove_file(record_path(&self.suite, &self.cfg.service));
350 }
351 self.listener.wake();
352 self.listener.cleanup();
353 for (_, close) in self.conns.lock().unwrap_or_else(|e| e.into_inner()).drain() {
354 close();
355 }
356 self.election
357 .lock()
358 .unwrap_or_else(|e| e.into_inner())
359 .take();
360 self.event(ServiceEvent::Released);
361 self.released_cv.notify_all();
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct ServiceStatus {
368 pub connections: usize,
370 pub inflight: usize,
372 pub handoff_requested: bool,
374 pub released: bool,
376 pub process: Option<rightkit_child_process::ProcessUsage>,
379}
380
381pub struct Hosted {
383 shared: Arc<Shared>,
384}
385
386impl Hosted {
387 pub fn record(&self) -> &ServiceRecord {
388 &self.shared.record
389 }
390 pub fn handoff_requested(&self) -> bool {
391 self.shared.handoff.load(Ordering::SeqCst)
392 }
393 pub fn inflight(&self) -> usize {
394 self.shared.inflight.load(Ordering::SeqCst)
395 }
396 pub fn status(&self) -> ServiceStatus {
399 ServiceStatus {
400 connections: self
401 .shared
402 .conns
403 .lock()
404 .unwrap_or_else(|e| e.into_inner())
405 .len(),
406 inflight: self.shared.inflight.load(Ordering::SeqCst),
407 handoff_requested: self.shared.handoff.load(Ordering::SeqCst),
408 released: *self
409 .shared
410 .released
411 .lock()
412 .unwrap_or_else(|e| e.into_inner()),
413 process: rightkit_child_process::process_usage().ok(),
414 }
415 }
416 pub fn wait_released(&self, timeout: Duration) -> bool {
418 let g = self
419 .shared
420 .released
421 .lock()
422 .unwrap_or_else(|e| e.into_inner());
423 let (g, _) = self
424 .shared
425 .released_cv
426 .wait_timeout_while(g, timeout, |r| !*r)
427 .unwrap_or_else(|e| e.into_inner());
428 *g
429 }
430 pub fn shutdown(&self) {
431 self.shared.release();
432 }
433}
434
435impl Drop for Hosted {
436 fn drop(&mut self) {
437 self.shared.release();
438 }
439}
440
441#[allow(clippy::result_large_err)]
446pub fn host(
447 suite: &Suite,
448 cfg: ServiceConfig,
449 handler: Arc<dyn Handler>,
450) -> Result<Hosted, HostError> {
451 host_with_options(suite, cfg, handler, HostOptions::default())
452}
453
454#[allow(clippy::result_large_err)]
456pub fn host_with_options(
457 suite: &Suite,
458 cfg: ServiceConfig,
459 handler: Arc<dyn Handler>,
460 options: HostOptions,
461) -> Result<Hosted, HostError> {
462 start_host(suite, cfg, handler, options, WatchTuning::PRODUCTION)
463}
464
465#[allow(clippy::result_large_err)]
466fn start_host(
467 suite: &Suite,
468 cfg: ServiceConfig,
469 handler: Arc<dyn Handler>,
470 options: HostOptions,
471 watch: WatchTuning,
472) -> Result<Hosted, HostError> {
473 if !valid_service_name(&cfg.service) {
474 return Err(HostError::InvalidName(cfg.service.clone()));
475 }
476 let dir = suite.service_dir(&cfg.service);
477 make_private_dir(&dir)?;
478 let me = std::process::id();
479 let deadline = Instant::now() + cfg.takeover_timeout;
480 let mut requested = false;
481 let lock_path = dir.join("election.lock");
482 let endpoint = match cfg.endpoint.clone() {
483 Some(e) => e,
484 None => try_endpoint_for(suite, &cfg.service).map_err(HostError::Endpoint)?,
485 };
486 let mut backoff = Duration::from_millis(5);
487 let (listener, election) = loop {
488 let election = try_lock_exclusive(&lock_path)?;
492 let current = try_read_record(suite, &cfg.service);
495 let unreadable = current
496 .as_ref()
497 .err()
498 .map(|e| std::io::Error::new(e.kind(), e.to_string()));
499 match current {
500 Ok(Some(rec)) if record_process_matches(&rec) && probe_endpoint(&rec.endpoint) => {
501 let their = resolve_rank(&cfg, &rec.role, rec.rank);
502 if their == RANK_UNKNOWN || cfg.rank >= their {
503 return Err(HostError::Held(rec));
504 }
505 if !requested {
506 let t = TakeoverRequest {
507 role: cfg.role.clone(),
508 rank: cfg.rank,
509 pid: me,
510 requested_at_ms: now_ms(),
511 };
512 let request_path = takeover_path(suite, &cfg.service);
513 write_atomic(&request_path, &serde_json::to_vec(&t).unwrap_or_default())?;
514 takeover_watch::poke(&request_path);
516 requested = true;
517 }
518 }
519 Ok(_) if election.is_some() => {
520 let _ = std::fs::remove_file(record_path(suite, &cfg.service));
523 match Listener::bind(&endpoint, options.max_pipe_instances) {
524 Ok(listener) => break (listener, election.unwrap()),
525 Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {}
528 Err(e) => return Err(HostError::Io(e)),
529 }
530 }
531 _ => {}
534 }
535 drop(election);
536 let remaining = deadline.saturating_duration_since(Instant::now());
537 if remaining.is_zero() {
538 clear_takeover_if_owner(suite, &cfg.service, me);
539 return Err(match unreadable {
542 Some(e) => HostError::Io(e),
543 None => HostError::TakeoverTimeout,
544 });
545 }
546 std::thread::sleep(backoff.min(remaining));
547 backoff = (backoff * 2).min(Duration::from_millis(50));
548 };
549 let record = ServiceRecord {
550 protocol_major: PROTOCOL_MAJOR,
551 protocol_minor: PROTOCOL_MINOR,
552 service: cfg.service.clone(),
553 role: cfg.role.clone(),
554 rank: cfg.rank,
555 pid: me,
556 endpoint: endpoint.clone(),
557 version: cfg.version.clone(),
558 engine_version: cfg.version.clone(),
559 started_ms: now_ms(),
560 process_started_ms: crate::discovery::own_start_ms().unwrap_or(0),
561 };
562 if let Err(e) = write_atomic(
563 &record_path(suite, &cfg.service),
564 &serde_json::to_vec_pretty(&record).unwrap_or_default(),
565 ) {
566 listener.cleanup();
567 return Err(HostError::Io(e));
568 }
569 if read_takeover(suite, &cfg.service).is_some_and(|t| t.pid == me) {
570 let _ = std::fs::remove_file(takeover_path(suite, &cfg.service));
571 }
572 let shared = Arc::new(Shared {
573 suite: suite.clone(),
574 cfg,
575 handler,
576 listener,
577 election: Mutex::new(Some(election)),
578 record,
579 stop: AtomicBool::new(false),
580 handoff: Arc::new(AtomicBool::new(false)),
581 released: Mutex::new(false),
582 released_cv: Condvar::new(),
583 inflight: AtomicUsize::new(0),
584 next_conn: AtomicU64::new(1),
585 conns: Mutex::new(HashMap::new()),
586 bell: Arc::new(Bell::new()),
587 watch,
588 write_timeout: options.write_timeout,
589 });
590 takeover_watch::register(
591 takeover_path(&shared.suite, &shared.cfg.service),
592 &shared.bell,
593 );
594 shared.event(ServiceEvent::Started {
595 service: shared.cfg.service.clone(),
596 endpoint,
597 rank: shared.cfg.rank,
598 });
599 let s = shared.clone();
600 if let Err(e) = std::thread::Builder::new()
601 .name("rk-svc-accept".into())
602 .spawn(move || accept_loop(s))
603 {
604 shared.release();
605 return Err(HostError::Io(e));
606 }
607 let s = shared.clone();
608 if let Err(e) = std::thread::Builder::new()
609 .name("rk-svc-takeover".into())
610 .spawn(move || takeover_monitor(s))
611 {
612 shared.release();
613 return Err(HostError::Io(e));
614 }
615 Ok(Hosted { shared })
616}
617
618pub(crate) fn now_ms() -> u64 {
619 std::time::SystemTime::now()
620 .duration_since(std::time::UNIX_EPOCH)
621 .map(|d| d.as_millis() as u64)
622 .unwrap_or(0)
623}
624
625fn accept_loop(s: Arc<Shared>) {
626 while !s.stop.load(Ordering::SeqCst) {
627 match s.listener.accept() {
628 Ok(Accepted::Conn(conn)) => {
629 if s.stop.load(Ordering::SeqCst) {
630 break;
631 }
632 let s2 = s.clone();
633 let _ = std::thread::Builder::new()
634 .name("rk-svc-conn".into())
635 .spawn(move || serve_conn(s2, conn));
636 }
637 Ok(Accepted::Rejected(reason)) => {
640 if !s.stop.load(Ordering::SeqCst) {
641 s.event(ServiceEvent::PeerRejected { reason });
642 }
643 }
644 Ok(Accepted::Vanished) => {}
645 Err(_) => {
646 if s.stop.load(Ordering::SeqCst) {
647 break;
648 }
649 std::thread::sleep(Duration::from_millis(20));
650 }
651 }
652 }
653}
654
655type FileStamp = (Option<SystemTime>, u64);
657
658fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
659 let md = std::fs::metadata(path).ok()?;
660 Some((md.modified().ok(), md.len()))
661}
662
663fn obliging_rank(
666 cfg: &ServiceConfig,
667 own_pid: u32,
668 alive: fn(u32) -> bool,
669 t: &TakeoverRequest,
670 now: u64,
671) -> Option<u8> {
672 let max_age = cfg.takeover_timeout.as_millis() as u64;
673 let fresh = now.saturating_sub(t.requested_at_ms) <= max_age + 5_000;
674 let their = resolve_rank(cfg, &t.role, t.rank);
675 let obliges =
676 their != RANK_UNKNOWN && their < cfg.rank && t.pid != own_pid && alive(t.pid) && fresh;
677 obliges.then_some(their)
678}
679
680#[cfg(test)]
681struct ExitMark<'a>(&'a Bell);
682
683#[cfg(test)]
684impl Drop for ExitMark<'_> {
685 fn drop(&mut self) {
686 self.0.mark_exited();
687 }
688}
689
690fn takeover_monitor(s: Arc<Shared>) {
700 #[cfg(test)]
701 let _exit = ExitMark(&s.bell);
702 let path = takeover_path(&s.suite, &s.cfg.service);
703 let mut judged: Option<FileStamp> = None;
704 let mut look_now = false;
705 loop {
706 if s.stop.load(Ordering::SeqCst) {
707 return;
708 }
709 let force = std::mem::take(&mut look_now);
710 match file_stamp(&path) {
711 None => judged = None,
712 Some(stamp) if force || judged != Some(stamp) => {
713 judged = Some(stamp);
714 #[cfg(test)]
715 s.bell.note_read();
716 match try_read_takeover(&s.suite, &s.cfg.service) {
717 Ok(Some(t)) => {
718 let now = now_ms();
719 let obliging = obliging_rank(&s.cfg, s.record.pid, s.watch.alive, &t, now);
720 if let Some(their) = obliging {
721 hand_off(&s, t, their);
722 return;
723 }
724 }
725 Ok(None) => {}
726 Err(_) => judged = None,
730 }
731 }
732 Some(_) => {}
733 }
734 #[cfg(test)]
735 s.bell.note_pass();
736 match s.bell.wait(s.watch.interval) {
737 Wake::Stop => return,
738 Wake::Poke => look_now = true,
739 Wake::Timeout => {}
740 }
741 }
742}
743
744fn hand_off(s: &Shared, t: TakeoverRequest, their: u8) {
747 s.handoff.store(true, Ordering::SeqCst);
748 s.event(ServiceEvent::HandoffRequested {
749 by_role: t.role,
750 by_rank: their,
751 });
752 s.handler.on_handoff();
753 let until = Instant::now() + s.cfg.handoff_grace;
754 while s.inflight.load(Ordering::SeqCst) > 0
755 && Instant::now() < until
756 && !s.stop.load(Ordering::SeqCst)
757 {
758 std::thread::sleep(Duration::from_millis(10));
759 }
760 s.release();
761}
762
763fn frame_err(e: &FrameError) -> ServiceError {
764 ServiceError::new("bad_frame", e.to_string())
765}
766
767fn serve_conn(s: Arc<Shared>, conn: Conn) {
768 let Conn {
769 reader,
770 writer,
771 peer_closed,
772 shutdown,
773 peer,
774 } = conn;
775 let mut reader = BufReader::new(reader);
776 let writer = Arc::new(Mutex::new(writer));
777 let limit = s.cfg.frame_limit;
778 let bound = s
779 .write_timeout
780 .map(|d| (d, WriteBound::start(shutdown.clone())));
781 let _watcher_ends = bound.as_ref().map(|(_, b)| FinishOnDrop(b.clone()));
783 let write = {
787 let w = writer.clone();
788 let bound = bound.clone();
789 move |v: &Value| -> bool {
790 let mut w = w.lock().unwrap_or_else(|e| e.into_inner());
791 if let Some((d, b)) = &bound {
792 b.set(Some(Instant::now() + *d));
793 }
794 let r = match write_json_line(&mut *w, v, limit) {
795 Ok(()) => {
796 let _ = w.flush();
797 true
798 }
799 Err(FrameError::TooLarge { .. }) => false,
800 Err(_) => true,
801 };
802 if let Some((_, b)) = &bound {
803 b.set(None);
804 }
805 r
806 }
807 };
808 let send = {
809 let write = write.clone();
810 move |v: &Value| {
811 write(v);
812 }
813 };
814 let mut buf = Vec::new();
815 let hello: Value = match read_json_line(&mut reader, &mut buf, 64 * 1024) {
817 Ok(Some(v)) => v,
818 _ => return, };
820 let app = hello["app"].as_str().unwrap_or("").to_string();
821 let reject = |reason: &str, code: &str, retryable: bool| {
822 let e = ServiceError {
823 code: code.into(),
824 message: reason.into(),
825 retryable,
826 };
827 send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
828 s.event(ServiceEvent::ConnectionRejected {
829 app: app.clone(),
830 reason: reason.into(),
831 });
832 shutdown();
833 };
834 if hello["type"] != "hello" || app.is_empty() {
835 return reject(
836 "first frame must be hello with an app label",
837 "bad_hello",
838 false,
839 );
840 }
841 if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
842 return reject("protocol major mismatch", "protocol_mismatch", false);
843 }
844 if s.handoff.load(Ordering::SeqCst) {
845 return reject(
846 "host is handing off",
847 crate::error::ERROR_HOST_HANDOFF,
848 true,
849 );
850 }
851 if !s.cfg.allowlist.app_known(&app) {
852 return reject(
853 "app is not on the service allowlist",
854 crate::error::ERROR_NOT_ALLOWED,
855 false,
856 );
857 }
858 let id = s.next_conn.fetch_add(1, Ordering::SeqCst);
859 s.conns
860 .lock()
861 .unwrap_or_else(|e| e.into_inner())
862 .insert(id, shutdown.clone());
863 s.event(ServiceEvent::ConnectionOpened {
864 conn: id,
865 app: app.clone(),
866 });
867 send(&json!({
868 "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
869 "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
870 "methods": s.cfg.allowlist.methods_for(&app),
871 }));
872
873 let caller = Caller::local_app(app.clone(), peer.clone());
874 let closed_notified = Arc::new(AtomicBool::new(false));
875 let notify_closed = {
876 let (s, flag) = (s.clone(), closed_notified.clone());
877 move || {
878 if !flag.swap(true, Ordering::SeqCst) {
879 s.handler.connection_closed(id);
880 }
881 }
882 };
883 loop {
885 let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
886 Ok(Some(v)) => v,
887 Ok(None) | Err(FrameError::Io(_)) => break,
888 Err(e) => {
889 send(
890 &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
891 );
892 continue;
893 }
894 };
895 let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
896 send(
897 &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
898 );
899 continue;
900 };
901 let respond_err = |e: ServiceError| {
902 send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
903 };
904 let method = req["method"].as_str().unwrap_or("").to_string();
905 if s.handoff.load(Ordering::SeqCst) {
906 respond_err(ServiceError::handoff());
907 continue;
908 }
909 if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
910 s.event(ServiceEvent::Denied {
911 conn: id,
912 app: app.clone(),
913 method: method.clone(),
914 });
915 respond_err(e);
916 continue;
917 }
918 if method == "ping" {
919 send(
920 &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
921 );
922 continue;
923 }
924 let params = req.get("params").cloned().unwrap_or_else(|| json!({}));
925 let cx = new_context(
926 id,
927 caller.clone(),
928 Arc::new(AtomicBool::new(false)),
929 s.handoff.clone(),
930 );
931 let emit: Emit = {
932 let (write, s, method) = (write.clone(), s.clone(), method.clone());
933 Arc::new(move |ev| {
934 if !write(&json!({"type": "event", "id": rid, "event": ev})) {
935 s.event(ServiceEvent::FrameTooLarge {
936 conn: id,
937 method: method.clone(),
938 kind: "event",
939 limit,
940 });
941 }
942 })
943 };
944 s.inflight.fetch_add(1, Ordering::SeqCst);
945 let done = AtomicBool::new(false);
946 let result = std::thread::scope(|sc| {
947 sc.spawn(|| {
948 while !done.load(Ordering::SeqCst) {
949 if peer_closed() {
950 cx.closed.store(true, Ordering::SeqCst);
951 notify_closed();
952 return;
953 }
954 std::thread::sleep(Duration::from_millis(40));
955 }
956 });
957 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
958 s.handler.call(&cx, &method, ¶ms, &emit)
959 }))
960 .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
961 done.store(true, Ordering::SeqCst);
962 r
963 });
964 s.inflight.fetch_sub(1, Ordering::SeqCst);
965 if cx.peer_closed() {
966 break;
967 }
968 match result {
969 Ok(v) => {
970 if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
973 s.event(ServiceEvent::FrameTooLarge {
974 conn: id,
975 method: method.clone(),
976 kind: "response",
977 limit,
978 });
979 respond_err(ServiceError::new(
980 ERROR_RESPONSE_TOO_LARGE,
981 format!(
982 "the result of {method} is larger than the {limit}-byte frame limit"
983 ),
984 ));
985 }
986 }
987 Err(e) => respond_err(e),
988 }
989 }
990 notify_closed();
991 s.conns
992 .lock()
993 .unwrap_or_else(|e| e.into_inner())
994 .remove(&id);
995 shutdown();
996}
997
998#[allow(dead_code)]
999fn _assert_transport_used() {
1000 let _ = transport::current_uid;
1001}
1002
1003#[cfg(test)]
1004mod options_tests {
1005 use super::*;
1006
1007 #[test]
1008 fn pipe_instance_cap_is_small_finite_and_clamped() {
1009 let d = HostOptions::default();
1010 assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1011 assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1012 assert_eq!(
1013 HostOptions::new()
1014 .max_pipe_instances(4)
1015 .get_max_pipe_instances(),
1016 4
1017 );
1018 assert_eq!(
1019 HostOptions::new()
1020 .max_pipe_instances(0)
1021 .get_max_pipe_instances(),
1022 1
1023 );
1024 assert_eq!(
1026 HostOptions::new()
1027 .max_pipe_instances(u32::MAX)
1028 .get_max_pipe_instances(),
1029 254
1030 );
1031 }
1032}
1033
1034#[cfg(test)]
1035mod monitor_tests {
1036 use super::*;
1037 use std::sync::atomic::AtomicUsize;
1038
1039 const LONG: Duration = Duration::from_secs(600);
1041
1042 struct Quiet {
1043 handoffs: AtomicUsize,
1044 }
1045
1046 impl Handler for Quiet {
1047 fn call(
1048 &self,
1049 _cx: &CallContext,
1050 _method: &str,
1051 _params: &Value,
1052 _emit: &Emit,
1053 ) -> Result<Value, ServiceError> {
1054 Ok(json!({}))
1055 }
1056 fn on_handoff(&self) {
1057 self.handoffs.fetch_add(1, Ordering::SeqCst);
1058 }
1059 }
1060
1061 fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1062 let deadline = Instant::now() + Duration::from_secs(20);
1063 while !ok() {
1064 assert!(Instant::now() < deadline, "timed out waiting for {what}");
1065 std::thread::sleep(Duration::from_millis(5));
1066 }
1067 }
1068
1069 fn host_in(
1071 dir: &tempfile::TempDir,
1072 interval: Duration,
1073 alive: fn(u32) -> bool,
1074 ) -> (Suite, Hosted, Arc<Quiet>) {
1075 let suite = Suite::at(dir.path().join("suite")).unwrap();
1076 let mut cfg = ServiceConfig::new(
1077 "watch",
1078 "incumbent",
1079 5,
1080 "test",
1081 Allowlist::new().allow("*", ["echo"]),
1082 );
1083 cfg.takeover_timeout = Duration::from_secs(20);
1084 cfg.handoff_grace = Duration::from_secs(1);
1085 let handler = Arc::new(Quiet {
1086 handoffs: AtomicUsize::new(0),
1087 });
1088 let hosted = start_host(
1089 &suite,
1090 cfg,
1091 handler.clone(),
1092 HostOptions::default(),
1093 WatchTuning { interval, alive },
1094 )
1095 .expect("host starts");
1096 (suite, hosted, handler)
1097 }
1098
1099 fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1100 TakeoverRequest {
1101 role: role.into(),
1102 rank,
1103 pid,
1104 requested_at_ms: now_ms(),
1105 }
1106 }
1107
1108 fn other_pid() -> u32 {
1110 std::process::id().wrapping_add(1)
1111 }
1112
1113 #[test]
1114 fn a_request_from_another_process_is_found_by_the_file_check() {
1115 let dir = tempfile::tempdir().unwrap();
1116 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1117 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1119 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1120 assert!(
1121 hosted.wait_released(Duration::from_secs(20)),
1122 "the incumbent must hand off"
1123 );
1124 assert!(hosted.handoff_requested());
1125 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1126 }
1127
1128 #[test]
1129 fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1130 let dir = tempfile::tempdir().unwrap();
1131 let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1132 let bell = hosted.shared.bell.clone();
1133 wait_for("the first pass", || bell.passes() >= 1);
1134 write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1135 assert!(
1136 hosted.wait_released(Duration::from_secs(20)),
1137 "a 10-minute interval must not delay a request raised in this process"
1138 );
1139 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1140 }
1141
1142 #[test]
1143 fn releasing_the_host_ends_the_monitor_at_once() {
1144 let dir = tempfile::tempdir().unwrap();
1145 let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1146 let bell = hosted.shared.bell.clone();
1147 wait_for("the first pass", || bell.passes() >= 1);
1148 assert!(!bell.exited());
1149 hosted.shutdown();
1150 wait_for("the monitor thread to return", || bell.exited());
1151 }
1152
1153 #[test]
1154 fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1155 let dir = tempfile::tempdir().unwrap();
1156 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1157 let bell = hosted.shared.bell.clone();
1158 let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1160 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1161 wait_for("the request to be judged", || bell.reads() >= 1);
1162 let before = bell.wakes();
1163 wait_for("five more checks", || bell.wakes() >= before + 5);
1164 assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1165 assert!(!hosted.handoff_requested());
1166
1167 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1169 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1170 assert!(hosted.wait_released(Duration::from_secs(20)));
1171 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1172 }
1173
1174 #[test]
1175 fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1176 let dir = tempfile::tempdir().unwrap();
1177 let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1178 let bell = hosted.shared.bell.clone();
1179 let (start, woke_before) = (Instant::now(), bell.wakes());
1180 std::thread::sleep(Duration::from_secs(2));
1181 let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1182 let allowed = 3u64.max(secs.ceil() as u64 + 1);
1185 assert!(
1186 woke <= allowed,
1187 "the monitor woke {woke} times in {secs:.1} s"
1188 );
1189 drop(hosted);
1190 }
1191
1192 #[test]
1195 fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1196 let dir = tempfile::tempdir().unwrap();
1197 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1198 let bell = hosted.shared.bell.clone();
1199 let path = takeover_path(&suite, "watch");
1200 std::fs::create_dir_all(&path).unwrap();
1201 wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1203 assert!(!hosted.handoff_requested());
1204
1205 std::fs::remove_dir(&path).unwrap();
1207 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1208 write_atomic(&path, &body).unwrap();
1209 assert!(hosted.wait_released(Duration::from_secs(20)));
1210 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1211 }
1212
1213 #[test]
1214 fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1215 let dir = tempfile::tempdir().unwrap();
1216 let suite = Suite::at(dir.path().join("suite")).unwrap();
1217 let record = record_path(&suite, "watch");
1218 std::fs::create_dir_all(&record).unwrap();
1219 let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1220 cfg.takeover_timeout = Duration::from_millis(300);
1221 let err = start_host(
1222 &suite,
1223 cfg,
1224 Arc::new(Quiet {
1225 handoffs: AtomicUsize::new(0),
1226 }),
1227 HostOptions::default(),
1228 WatchTuning {
1229 interval: LONG,
1230 alive: |_| true,
1231 },
1232 )
1233 .err()
1234 .expect("a record that cannot be read must not be treated as no record");
1235 match err {
1236 HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1237 _ => panic!("expected the read error"),
1238 }
1239 assert!(record.is_dir(), "left as it was");
1240 }
1241
1242 #[test]
1243 fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1244 let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1245 cfg.takeover_timeout = Duration::from_secs(10);
1246 cfg.known_roles.insert("legacy".into(), 1);
1247 let now = 1_000_000_000_000u64;
1248 let live: fn(u32) -> bool = |_| true;
1249 let dead: fn(u32) -> bool = |_| false;
1250 let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1251 role: role.into(),
1252 rank,
1253 pid,
1254 requested_at_ms,
1255 };
1256
1257 assert_eq!(
1258 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1259 Some(0)
1260 );
1261 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1263 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1264 assert_eq!(
1266 obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1267 None
1268 );
1269 assert_eq!(
1271 obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1272 None
1273 );
1274 assert_eq!(
1276 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1277 Some(0)
1278 );
1279 assert_eq!(
1280 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1281 None
1282 );
1283 assert_eq!(
1285 obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1286 Some(1)
1287 );
1288 assert_eq!(
1289 obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1290 None
1291 );
1292 }
1293}