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 pub private_secret: Option<crate::private::PrivateSecret>,
93}
94
95impl ServiceConfig {
96 pub fn new(service: &str, role: &str, rank: u8, version: &str, allowlist: Allowlist) -> Self {
97 Self {
98 service: service.into(),
99 role: role.into(),
100 rank,
101 version: version.into(),
102 allowlist,
103 frame_limit: DEFAULT_FRAME_LIMIT,
104 takeover_timeout: Duration::from_secs(60),
105 handoff_grace: Duration::from_secs(30),
106 on_event: None,
107 known_roles: Default::default(),
108 endpoint: None,
109 private_secret: None,
110 }
111 }
112
113 pub fn private_secret(mut self, secret: crate::private::PrivateSecret) -> Self {
118 self.private_secret = Some(secret);
119 self
120 }
121
122 pub fn with_methods(mut self, table: &crate::contract::MethodTable) -> Self {
125 self.allowlist = table.allowlist();
126 self
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct HostOptions {
134 max_pipe_instances: u32,
135 write_timeout: Option<Duration>,
136 max_message: usize,
137 max_connections: usize,
138}
139
140pub const DEFAULT_MAX_CONNECTIONS: usize = 64;
142
143impl Default for HostOptions {
144 fn default() -> Self {
145 Self {
146 max_pipe_instances: DEFAULT_MAX_PIPE_INSTANCES,
147 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
148 max_message: crate::chunk::DEFAULT_MAX_MESSAGE,
149 max_connections: DEFAULT_MAX_CONNECTIONS,
150 }
151 }
152}
153
154impl HostOptions {
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 #[must_use]
166 pub fn max_pipe_instances(mut self, n: u32) -> Self {
167 self.max_pipe_instances = n.clamp(1, 254);
168 self
169 }
170
171 pub fn get_max_pipe_instances(&self) -> u32 {
173 self.max_pipe_instances
174 }
175
176 #[must_use]
185 pub fn write_timeout(mut self, bound: Option<Duration>) -> Self {
186 self.write_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
187 self
188 }
189
190 pub fn get_write_timeout(&self) -> Option<Duration> {
192 self.write_timeout
193 }
194
195 #[must_use]
205 pub fn max_message(mut self, bytes: usize) -> Self {
206 self.max_message = bytes;
207 self
208 }
209
210 pub fn get_max_message(&self) -> usize {
212 self.max_message
213 }
214
215 #[must_use]
222 pub fn max_connections(mut self, n: usize) -> Self {
223 self.max_connections = n.max(1);
224 self
225 }
226
227 pub fn get_max_connections(&self) -> usize {
229 self.max_connections
230 }
231}
232
233#[derive(Debug)]
234pub enum HostError {
235 Held(ServiceRecord),
237 TakeoverTimeout,
239 InvalidName(String),
240 Suite(SuiteError),
241 Io(std::io::Error),
242 Endpoint(crate::transport::EndpointError),
244}
245impl std::fmt::Display for HostError {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 match self {
248 HostError::Held(r) => write!(
249 f,
250 "{} is already hosted by {} (rank {}, pid {})",
251 r.service, r.role, r.rank, r.pid
252 ),
253 HostError::TakeoverTimeout => write!(f, "incumbent host did not release in time"),
254 HostError::InvalidName(s) => write!(f, "invalid service name {s:?}"),
255 HostError::Suite(e) => write!(f, "{e}"),
256 HostError::Io(e) => write!(f, "{e}"),
257 HostError::Endpoint(e) => write!(f, "{e}"),
258 }
259 }
260}
261impl std::error::Error for HostError {}
262impl From<std::io::Error> for HostError {
263 fn from(e: std::io::Error) -> Self {
264 HostError::Io(e)
265 }
266}
267impl From<SuiteError> for HostError {
268 fn from(e: SuiteError) -> Self {
269 HostError::Suite(e)
270 }
271}
272
273fn resolve_rank(cfg: &ServiceConfig, role: &str, rank: u8) -> u8 {
274 crate::discovery::resolve_rank(role, rank, &cfg.known_roles).unwrap_or(RANK_UNKNOWN)
275}
276
277struct Shared {
278 suite: Suite,
279 cfg: ServiceConfig,
280 handler: Arc<dyn Handler>,
281 listener: Listener,
282 election: Mutex<Option<std::fs::File>>,
284 record: ServiceRecord,
285 stop: AtomicBool,
286 handoff: Arc<AtomicBool>,
287 released: Mutex<bool>,
288 released_cv: Condvar,
289 inflight: AtomicUsize,
290 next_conn: AtomicU64,
291 conns: Mutex<HashMap<u64, Arc<dyn Fn() + Send + Sync>>>,
292 bell: Arc<Bell>,
294 watch: WatchTuning,
295 write_timeout: Option<Duration>,
296 max_message: usize,
297 max_connections: usize,
298 live_conns: Arc<AtomicUsize>,
300 fails: Mutex<crate::fail_limit::FailLimiter>,
302}
303
304struct ForgetFails {
306 s: Arc<Shared>,
307 conn: u64,
308}
309
310impl Drop for ForgetFails {
311 fn drop(&mut self) {
312 self.s
313 .fails
314 .lock()
315 .unwrap_or_else(|e| e.into_inner())
316 .forget(self.conn, Instant::now());
317 }
318}
319
320struct ConnSlot(Arc<AtomicUsize>);
322
323impl Drop for ConnSlot {
324 fn drop(&mut self) {
325 self.0.fetch_sub(1, Ordering::SeqCst);
326 }
327}
328
329struct WriteBound {
335 state: Mutex<WriteState>,
336 cv: Condvar,
337}
338
339struct WriteState {
340 until: Option<Instant>,
342 done: bool,
343}
344
345struct FinishOnDrop(Arc<WriteBound>);
346impl Drop for FinishOnDrop {
347 fn drop(&mut self) {
348 self.0.finish();
349 }
350}
351
352impl WriteBound {
353 fn start(shutdown: Arc<dyn Fn() + Send + Sync>) -> Arc<WriteBound> {
354 let w = Arc::new(WriteBound {
355 state: Mutex::new(WriteState {
356 until: None,
357 done: false,
358 }),
359 cv: Condvar::new(),
360 });
361 let watcher = w.clone();
362 std::thread::spawn(move || {
363 let mut g = watcher.state.lock().unwrap_or_else(|e| e.into_inner());
364 loop {
365 if g.done {
366 return;
367 }
368 match g.until {
369 None => g = watcher.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
370 Some(t) => {
371 let left = t.saturating_duration_since(Instant::now());
372 if left.is_zero() {
373 g.until = None;
374 drop(g);
375 shutdown();
376 return;
377 }
378 g = watcher
379 .cv
380 .wait_timeout(g, left)
381 .unwrap_or_else(|e| e.into_inner())
382 .0;
383 }
384 }
385 }
386 });
387 w
388 }
389 fn set(&self, until: Option<Instant>) {
390 self.state.lock().unwrap_or_else(|e| e.into_inner()).until = until;
391 self.cv.notify_all();
392 }
393 fn finish(&self) {
394 self.state.lock().unwrap_or_else(|e| e.into_inner()).done = true;
395 self.cv.notify_all();
396 }
397}
398
399#[derive(Clone, Copy)]
402struct WatchTuning {
403 interval: Duration,
405 alive: fn(u32) -> bool,
407}
408
409impl WatchTuning {
410 const PRODUCTION: WatchTuning = WatchTuning {
411 interval: TAKEOVER_CHECK_INTERVAL,
412 alive: pid_alive,
413 };
414}
415
416impl Shared {
417 fn event(&self, e: ServiceEvent) {
418 if let Some(f) = &self.cfg.on_event {
419 f(&e);
420 }
421 }
422
423 fn release(&self) {
424 {
425 let mut r = self.released.lock().unwrap_or_else(|e| e.into_inner());
426 if *r {
427 return;
428 }
429 *r = true;
430 }
431 self.stop.store(true, Ordering::SeqCst);
432 self.bell.stop();
434 if read_record(&self.suite, &self.cfg.service).is_some_and(|r| r.pid == self.record.pid) {
436 let _ = std::fs::remove_file(record_path(&self.suite, &self.cfg.service));
437 }
438 self.listener.wake();
439 self.listener.cleanup();
440 for (_, close) in self.conns.lock().unwrap_or_else(|e| e.into_inner()).drain() {
441 close();
442 }
443 self.election
444 .lock()
445 .unwrap_or_else(|e| e.into_inner())
446 .take();
447 self.event(ServiceEvent::Released);
448 self.released_cv.notify_all();
449 }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq)]
454pub struct ServiceStatus {
455 pub connections: usize,
457 pub inflight: usize,
459 pub handoff_requested: bool,
461 pub released: bool,
463 pub process: Option<rightkit_child_process::ProcessUsage>,
466}
467
468pub struct Hosted {
470 shared: Arc<Shared>,
471}
472
473impl Hosted {
474 pub fn record(&self) -> &ServiceRecord {
475 &self.shared.record
476 }
477 pub fn handoff_requested(&self) -> bool {
478 self.shared.handoff.load(Ordering::SeqCst)
479 }
480 pub fn inflight(&self) -> usize {
481 self.shared.inflight.load(Ordering::SeqCst)
482 }
483 pub fn status(&self) -> ServiceStatus {
486 ServiceStatus {
487 connections: self
488 .shared
489 .conns
490 .lock()
491 .unwrap_or_else(|e| e.into_inner())
492 .len(),
493 inflight: self.shared.inflight.load(Ordering::SeqCst),
494 handoff_requested: self.shared.handoff.load(Ordering::SeqCst),
495 released: *self
496 .shared
497 .released
498 .lock()
499 .unwrap_or_else(|e| e.into_inner()),
500 process: rightkit_child_process::process_usage().ok(),
501 }
502 }
503 pub fn wait_released(&self, timeout: Duration) -> bool {
505 let g = self
506 .shared
507 .released
508 .lock()
509 .unwrap_or_else(|e| e.into_inner());
510 let (g, _) = self
511 .shared
512 .released_cv
513 .wait_timeout_while(g, timeout, |r| !*r)
514 .unwrap_or_else(|e| e.into_inner());
515 *g
516 }
517 pub fn shutdown(&self) {
518 self.shared.release();
519 }
520}
521
522impl Drop for Hosted {
523 fn drop(&mut self) {
524 self.shared.release();
525 }
526}
527
528#[allow(clippy::result_large_err)]
534pub fn host(
535 suite: &Suite,
536 cfg: ServiceConfig,
537 handler: Arc<dyn Handler>,
538) -> Result<Hosted, HostError> {
539 host_with_options(suite, cfg, handler, HostOptions::default())
540}
541
542#[allow(clippy::result_large_err)]
544pub fn host_with_options(
545 suite: &Suite,
546 cfg: ServiceConfig,
547 handler: Arc<dyn Handler>,
548 options: HostOptions,
549) -> Result<Hosted, HostError> {
550 start_host(suite, cfg, handler, options, WatchTuning::PRODUCTION)
551}
552
553#[allow(clippy::result_large_err)]
554fn start_host(
555 suite: &Suite,
556 cfg: ServiceConfig,
557 handler: Arc<dyn Handler>,
558 options: HostOptions,
559 watch: WatchTuning,
560) -> Result<Hosted, HostError> {
561 if !valid_service_name(&cfg.service) {
562 return Err(HostError::InvalidName(cfg.service.clone()));
563 }
564 let dir = suite.service_dir(&cfg.service);
565 make_private_dir(&dir)?;
566 let me = std::process::id();
567 let deadline = Instant::now() + cfg.takeover_timeout;
568 let mut requested = false;
569 let lock_path = dir.join("election.lock");
570 let endpoint = match cfg.endpoint.clone() {
571 Some(e) => e,
572 None => try_endpoint_for(suite, &cfg.service).map_err(HostError::Endpoint)?,
573 };
574 let mut backoff = Duration::from_millis(5);
575 let (listener, election) = loop {
576 let election = try_lock_exclusive(&lock_path)?;
580 let current = try_read_record(suite, &cfg.service);
583 let unreadable = current
584 .as_ref()
585 .err()
586 .map(|e| std::io::Error::new(e.kind(), e.to_string()));
587 match current {
588 Ok(Some(rec)) if record_process_matches(&rec) && probe_endpoint(&rec.endpoint) => {
589 let their = resolve_rank(&cfg, &rec.role, rec.rank);
590 if their == RANK_UNKNOWN || cfg.rank >= their {
591 return Err(HostError::Held(rec));
592 }
593 if !requested {
594 let t = TakeoverRequest {
595 role: cfg.role.clone(),
596 rank: cfg.rank,
597 pid: me,
598 requested_at_ms: now_ms(),
599 };
600 let request_path = takeover_path(suite, &cfg.service);
601 write_atomic(&request_path, &serde_json::to_vec(&t).unwrap_or_default())?;
602 takeover_watch::poke(&request_path);
604 requested = true;
605 }
606 }
607 Ok(_) if election.is_some() => {
608 let _ = std::fs::remove_file(record_path(suite, &cfg.service));
611 match Listener::bind(&endpoint, options.max_pipe_instances) {
612 Ok(listener) => break (listener, election.unwrap()),
613 Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {}
616 Err(e) => return Err(HostError::Io(e)),
617 }
618 }
619 _ => {}
622 }
623 drop(election);
624 let remaining = deadline.saturating_duration_since(Instant::now());
625 if remaining.is_zero() {
626 clear_takeover_if_owner(suite, &cfg.service, me);
627 return Err(match unreadable {
630 Some(e) => HostError::Io(e),
631 None => HostError::TakeoverTimeout,
632 });
633 }
634 std::thread::sleep(backoff.min(remaining));
635 backoff = (backoff * 2).min(Duration::from_millis(50));
636 };
637 let record = ServiceRecord {
638 protocol_major: PROTOCOL_MAJOR,
639 protocol_minor: PROTOCOL_MINOR,
640 service: cfg.service.clone(),
641 role: cfg.role.clone(),
642 rank: cfg.rank,
643 pid: me,
644 endpoint: endpoint.clone(),
645 version: cfg.version.clone(),
646 engine_version: cfg.version.clone(),
647 started_ms: now_ms(),
648 process_started_ms: crate::discovery::own_start_ms().unwrap_or(0),
649 };
650 if let Err(e) = write_atomic(
651 &record_path(suite, &cfg.service),
652 &serde_json::to_vec_pretty(&record).unwrap_or_default(),
653 ) {
654 listener.cleanup();
655 return Err(HostError::Io(e));
656 }
657 if read_takeover(suite, &cfg.service).is_some_and(|t| t.pid == me) {
658 let _ = std::fs::remove_file(takeover_path(suite, &cfg.service));
659 }
660 let shared = Arc::new(Shared {
661 suite: suite.clone(),
662 cfg,
663 handler,
664 listener,
665 election: Mutex::new(Some(election)),
666 record,
667 stop: AtomicBool::new(false),
668 handoff: Arc::new(AtomicBool::new(false)),
669 released: Mutex::new(false),
670 released_cv: Condvar::new(),
671 inflight: AtomicUsize::new(0),
672 next_conn: AtomicU64::new(1),
673 conns: Mutex::new(HashMap::new()),
674 bell: Arc::new(Bell::new()),
675 watch,
676 write_timeout: options.write_timeout,
677 max_message: options.max_message,
678 max_connections: options.max_connections,
679 live_conns: Arc::new(AtomicUsize::new(0)),
680 fails: Mutex::new(crate::fail_limit::FailLimiter::new()),
681 });
682 takeover_watch::register(
683 takeover_path(&shared.suite, &shared.cfg.service),
684 &shared.bell,
685 );
686 shared.event(ServiceEvent::Started {
687 service: shared.cfg.service.clone(),
688 endpoint,
689 rank: shared.cfg.rank,
690 });
691 let s = shared.clone();
692 if let Err(e) = std::thread::Builder::new()
693 .name("rk-svc-accept".into())
694 .spawn(move || accept_loop(s))
695 {
696 shared.release();
697 return Err(HostError::Io(e));
698 }
699 let s = shared.clone();
700 if let Err(e) = std::thread::Builder::new()
701 .name("rk-svc-takeover".into())
702 .spawn(move || takeover_monitor(s))
703 {
704 shared.release();
705 return Err(HostError::Io(e));
706 }
707 Ok(Hosted { shared })
708}
709
710pub(crate) fn now_ms() -> u64 {
711 std::time::SystemTime::now()
712 .duration_since(std::time::UNIX_EPOCH)
713 .map(|d| d.as_millis() as u64)
714 .unwrap_or(0)
715}
716
717fn accept_loop(s: Arc<Shared>) {
718 while !s.stop.load(Ordering::SeqCst) {
719 match s.listener.accept() {
720 Ok(Accepted::Conn(conn)) => {
721 if s.stop.load(Ordering::SeqCst) {
722 break;
723 }
724 let slot = ConnSlot(s.live_conns.clone());
727 let live = s.live_conns.fetch_add(1, Ordering::SeqCst) + 1;
728 if cfg!(unix) && live > s.max_connections {
729 (conn.shutdown)();
730 drop(slot);
731 s.event(ServiceEvent::PeerRejected {
732 reason: format!("connection limit of {} reached", s.max_connections),
733 });
734 continue;
735 }
736 let s2 = s.clone();
737 let _ = std::thread::Builder::new()
738 .name("rk-svc-conn".into())
739 .spawn(move || {
740 let _slot = slot;
741 serve_conn(s2, conn)
742 });
743 }
744 Ok(Accepted::Rejected(reason)) => {
747 if !s.stop.load(Ordering::SeqCst) {
748 s.event(ServiceEvent::PeerRejected { reason });
749 }
750 }
751 Ok(Accepted::Vanished) => {}
752 Err(_) => {
753 if s.stop.load(Ordering::SeqCst) {
754 break;
755 }
756 std::thread::sleep(Duration::from_millis(20));
757 }
758 }
759 }
760}
761
762type FileStamp = (Option<SystemTime>, u64);
764
765fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
766 let md = std::fs::metadata(path).ok()?;
767 Some((md.modified().ok(), md.len()))
768}
769
770fn obliging_rank(
773 cfg: &ServiceConfig,
774 own_pid: u32,
775 alive: fn(u32) -> bool,
776 t: &TakeoverRequest,
777 now: u64,
778) -> Option<u8> {
779 let max_age = cfg.takeover_timeout.as_millis() as u64;
780 let fresh = now.saturating_sub(t.requested_at_ms) <= max_age + 5_000;
781 let their = resolve_rank(cfg, &t.role, t.rank);
782 let obliges =
783 their != RANK_UNKNOWN && their < cfg.rank && t.pid != own_pid && alive(t.pid) && fresh;
784 obliges.then_some(their)
785}
786
787#[cfg(test)]
788struct ExitMark<'a>(&'a Bell);
789
790#[cfg(test)]
791impl Drop for ExitMark<'_> {
792 fn drop(&mut self) {
793 self.0.mark_exited();
794 }
795}
796
797fn takeover_monitor(s: Arc<Shared>) {
807 #[cfg(test)]
808 let _exit = ExitMark(&s.bell);
809 let path = takeover_path(&s.suite, &s.cfg.service);
810 let mut judged: Option<FileStamp> = None;
811 let mut look_now = false;
812 loop {
813 if s.stop.load(Ordering::SeqCst) {
814 return;
815 }
816 let force = std::mem::take(&mut look_now);
817 match file_stamp(&path) {
818 None => judged = None,
819 Some(stamp) if force || judged != Some(stamp) => {
820 judged = Some(stamp);
821 #[cfg(test)]
822 s.bell.note_read();
823 match try_read_takeover(&s.suite, &s.cfg.service) {
824 Ok(Some(t)) => {
825 let now = now_ms();
826 let obliging = obliging_rank(&s.cfg, s.record.pid, s.watch.alive, &t, now);
827 if let Some(their) = obliging {
828 hand_off(&s, t, their);
829 return;
830 }
831 }
832 Ok(None) => {}
833 Err(_) => judged = None,
837 }
838 }
839 Some(_) => {}
840 }
841 #[cfg(test)]
842 s.bell.note_pass();
843 match s.bell.wait(s.watch.interval) {
844 Wake::Stop => return,
845 Wake::Poke => look_now = true,
846 Wake::Timeout => {}
847 }
848 }
849}
850
851fn hand_off(s: &Shared, t: TakeoverRequest, their: u8) {
854 s.handoff.store(true, Ordering::SeqCst);
855 s.event(ServiceEvent::HandoffRequested {
856 by_role: t.role,
857 by_rank: their,
858 });
859 s.handler.on_handoff();
860 let until = Instant::now() + s.cfg.handoff_grace;
861 while s.inflight.load(Ordering::SeqCst) > 0
862 && Instant::now() < until
863 && !s.stop.load(Ordering::SeqCst)
864 {
865 std::thread::sleep(Duration::from_millis(10));
866 }
867 s.release();
868}
869
870fn frame_err(e: &FrameError) -> ServiceError {
871 ServiceError::new("bad_frame", e.to_string())
872}
873
874fn serve_conn(s: Arc<Shared>, conn: Conn) {
875 let Conn {
876 reader,
877 writer,
878 peer_closed,
879 shutdown,
880 peer,
881 } = conn;
882 let mut reader = BufReader::new(reader);
883 let writer = Arc::new(Mutex::new(writer));
884 let limit = s.cfg.frame_limit;
885 let bound = s
886 .write_timeout
887 .map(|d| (d, WriteBound::start(shutdown.clone())));
888 let _watcher_ends = bound.as_ref().map(|(_, b)| FinishOnDrop(b.clone()));
890 let write = {
894 let w = writer.clone();
895 let bound = bound.clone();
896 move |v: &Value| -> bool {
897 let mut w = w.lock().unwrap_or_else(|e| e.into_inner());
898 if let Some((d, b)) = &bound {
899 b.set(Some(Instant::now() + *d));
900 }
901 let r = match write_json_line(&mut *w, v, limit) {
902 Ok(()) => {
903 let _ = w.flush();
904 true
905 }
906 Err(FrameError::TooLarge { .. }) => false,
907 Err(_) => true,
908 };
909 if let Some((_, b)) = &bound {
910 b.set(None);
911 }
912 r
913 }
914 };
915 let send = {
916 let write = write.clone();
917 move |v: &Value| {
918 write(v);
919 }
920 };
921 let mut buf = Vec::new();
922 let conn_key = s.next_conn.fetch_add(1, Ordering::SeqCst);
924 let _forget_fails = ForgetFails {
925 s: s.clone(),
926 conn: conn_key,
927 };
928 let hello: Value = match read_json_line(&mut reader, &mut buf, 64 * 1024) {
930 Ok(Some(v)) => v,
931 _ => return, };
933 let app = hello["app"].as_str().unwrap_or("").to_string();
934 let reject = |reason: &str, code: &str, retryable: bool| {
935 let e = ServiceError {
936 code: code.into(),
937 message: reason.into(),
938 retryable,
939 };
940 send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
941 s.event(ServiceEvent::ConnectionRejected {
942 app: app.clone(),
943 reason: reason.into(),
944 });
945 shutdown();
946 };
947 if hello["type"] != "hello" || app.is_empty() {
948 return reject(
949 "first frame must be hello with an app label",
950 "bad_hello",
951 false,
952 );
953 }
954 if let Some(secret) = &s.cfg.private_secret {
955 if !secret.verify(hello["secret"].as_str().unwrap_or("")) {
958 let now = Instant::now();
959 let limited = s
960 .fails
961 .lock()
962 .unwrap_or_else(|e| e.into_inner())
963 .refuses_unverified(conn_key, now);
964 if limited {
965 return reject(
966 "too many failed authentications; wait before reconnecting",
967 crate::error::ERROR_RATE_LIMITED,
968 false,
969 );
970 }
971 s.fails
972 .lock()
973 .unwrap_or_else(|e| e.into_inner())
974 .record_failure(conn_key, now);
975 return reject(
976 "missing or wrong secret",
977 crate::error::ERROR_UNAUTHENTICATED,
978 false,
979 );
980 }
981 }
982 if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
983 return reject("protocol major mismatch", "protocol_mismatch", false);
984 }
985 if s.handoff.load(Ordering::SeqCst) {
986 return reject(
987 "host is handing off",
988 crate::error::ERROR_HOST_HANDOFF,
989 true,
990 );
991 }
992 if !s.cfg.allowlist.app_known(&app) {
993 return reject(
994 "app is not on the service allowlist",
995 crate::error::ERROR_NOT_ALLOWED,
996 false,
997 );
998 }
999 let id = conn_key;
1000 s.conns
1001 .lock()
1002 .unwrap_or_else(|e| e.into_inner())
1003 .insert(id, shutdown.clone());
1004 s.event(ServiceEvent::ConnectionOpened {
1005 conn: id,
1006 app: app.clone(),
1007 });
1008 send(&json!({
1009 "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
1010 "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
1011 "methods": s.cfg.allowlist.methods_for(&app),
1012 "frame_limit": limit, "max_message": s.max_message,
1013 "chunk_bytes": crate::chunk::chunk_bytes_for(limit),
1014 }));
1015
1016 let caller = Caller::local_app(app.clone(), peer.clone());
1017 let closed_notified = Arc::new(AtomicBool::new(false));
1018 let notify_closed = {
1019 let (s, flag) = (s.clone(), closed_notified.clone());
1020 move || {
1021 if !flag.swap(true, Ordering::SeqCst) {
1022 s.handler.connection_closed(id);
1023 }
1024 }
1025 };
1026 let chunk_bytes = crate::chunk::chunk_bytes_for(limit);
1029 let mut uploads = crate::chunk::Uploads::default();
1030 let mut download: Option<crate::chunk::Download> = None;
1031 let mut downloads_made: u64 = 0;
1032 loop {
1034 let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
1035 Ok(Some(v)) => v,
1036 Ok(None) | Err(FrameError::Io(_)) => break,
1037 Err(e) => {
1038 send(
1039 &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
1040 );
1041 continue;
1042 }
1043 };
1044 if let Some(secret) = &s.cfg.private_secret {
1045 if !secret.verify(req["secret"].as_str().unwrap_or("")) {
1048 let now = Instant::now();
1049 let limited = s
1050 .fails
1051 .lock()
1052 .unwrap_or_else(|e| e.into_inner())
1053 .refuses_unverified(conn_key, now);
1054 if limited {
1055 s.event(ServiceEvent::ConnectionRejected {
1056 app: app.clone(),
1057 reason: "too many failed authentications".into(),
1058 });
1059 send(
1060 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1061 "error": ServiceError::new(crate::error::ERROR_RATE_LIMITED, "too many failed authentications; wait before reconnecting").to_value()}),
1062 );
1063 break;
1064 }
1065 s.fails
1066 .lock()
1067 .unwrap_or_else(|e| e.into_inner())
1068 .record_failure(conn_key, now);
1069 s.event(ServiceEvent::ConnectionRejected {
1070 app: app.clone(),
1071 reason: "missing or wrong secret".into(),
1072 });
1073 send(
1074 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1075 "error": ServiceError::new(crate::error::ERROR_UNAUTHENTICATED, "missing or wrong secret").to_value()}),
1076 );
1077 break;
1078 }
1079 }
1080 let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
1081 send(
1082 &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
1083 );
1084 continue;
1085 };
1086 let respond_err = |e: ServiceError| {
1087 send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
1088 };
1089 uploads.expire();
1091 let mut method = req["method"].as_str().unwrap_or("").to_string();
1092 let chunked_ok = req["chunked_ok"] == true && s.max_message > 0;
1093 if s.handoff.load(Ordering::SeqCst) {
1094 respond_err(ServiceError::handoff());
1095 continue;
1096 }
1097 let mut assembled: Option<Value> = None;
1100 if method == crate::chunk::METHOD_FETCH {
1101 let answer = match &download {
1102 Some(d) => d.fetch(req.get("params").unwrap_or(&Value::Null)),
1103 None => Err(ServiceError::new(
1104 crate::error::ERROR_BAD_CHUNK,
1105 "no chunked result is held on this connection",
1106 )),
1107 };
1108 match answer {
1109 Ok(v) => send(&json!({"type": "response", "id": rid, "ok": true, "result": v})),
1110 Err(e) => respond_err(e),
1111 }
1112 continue;
1113 }
1114 if method == crate::chunk::METHOD_UPLOAD {
1115 if s.max_message == 0 {
1116 respond_err(ServiceError::new(
1117 crate::error::ERROR_REQUEST_TOO_LARGE,
1118 "this host does not accept chunked requests",
1119 ));
1120 continue;
1121 }
1122 let authorize = |m: &str| {
1125 crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, m).inspect_err(
1126 |_| {
1127 s.event(ServiceEvent::Denied {
1128 conn: id,
1129 app: app.clone(),
1130 method: m.to_string(),
1131 });
1132 },
1133 )
1134 };
1135 match uploads.accept_with(
1136 req.get("params").unwrap_or(&Value::Null),
1137 s.max_message,
1138 &authorize,
1139 ) {
1140 Ok(crate::chunk::UploadStep::More { received }) => {
1141 send(&json!({"type": "response", "id": rid, "ok": true,
1142 "result": {"received": received}}));
1143 continue;
1144 }
1145 Ok(crate::chunk::UploadStep::Done { method: m, params }) => {
1146 method = m;
1147 assembled = Some(params);
1148 }
1149 Err(e) => {
1150 respond_err(e);
1151 continue;
1152 }
1153 }
1154 }
1155 if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
1156 s.event(ServiceEvent::Denied {
1157 conn: id,
1158 app: app.clone(),
1159 method: method.clone(),
1160 });
1161 respond_err(e);
1162 continue;
1163 }
1164 if method == "ping" {
1165 send(
1166 &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
1167 );
1168 continue;
1169 }
1170 let params =
1171 assembled.unwrap_or_else(|| req.get("params").cloned().unwrap_or_else(|| json!({})));
1172 let cx = new_context(
1173 id,
1174 caller.clone(),
1175 Arc::new(AtomicBool::new(false)),
1176 s.handoff.clone(),
1177 );
1178 let emit: Emit = {
1179 let (write, s, method) = (write.clone(), s.clone(), method.clone());
1180 Arc::new(move |ev| {
1181 if !write(&json!({"type": "event", "id": rid, "event": ev})) {
1182 s.event(ServiceEvent::FrameTooLarge {
1183 conn: id,
1184 method: method.clone(),
1185 kind: "event",
1186 limit,
1187 });
1188 }
1189 })
1190 };
1191 s.inflight.fetch_add(1, Ordering::SeqCst);
1192 let done = AtomicBool::new(false);
1193 let result = std::thread::scope(|sc| {
1194 let watcher = sc.spawn(|| {
1197 while !done.load(Ordering::SeqCst) {
1198 if peer_closed() {
1199 cx.closed.store(true, Ordering::SeqCst);
1200 notify_closed();
1201 return;
1202 }
1203 std::thread::park_timeout(Duration::from_millis(40));
1204 }
1205 });
1206 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1207 s.handler.call(&cx, &method, ¶ms, &emit)
1208 }))
1209 .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
1210 done.store(true, Ordering::SeqCst);
1211 watcher.thread().unpark();
1212 r
1213 });
1214 s.inflight.fetch_sub(1, Ordering::SeqCst);
1215 if cx.peer_closed() {
1216 break;
1217 }
1218 match result {
1219 Ok(v) => {
1220 if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
1223 s.event(ServiceEvent::FrameTooLarge {
1224 conn: id,
1225 method: method.clone(),
1226 kind: "response",
1227 limit,
1228 });
1229 let held = chunked_ok
1232 .then(|| serde_json::to_string(&v).ok())
1233 .flatten()
1234 .filter(|text| text.len() <= s.max_message);
1235 drop(v);
1236 match held {
1237 Some(text) => {
1238 downloads_made += 1;
1239 let held = crate::chunk::Download::new(
1240 format!("d{downloads_made}"),
1241 text,
1242 chunk_bytes,
1243 );
1244 let descriptor = held.descriptor();
1245 download = Some(held);
1246 send(&json!({"type": "response", "id": rid, "ok": true,
1247 "result": Value::Null, "chunked": descriptor}));
1248 }
1249 None => respond_err(ServiceError::new(
1250 ERROR_RESPONSE_TOO_LARGE,
1251 format!(
1252 "the result of {method} is larger than the {limit}-byte frame limit"
1253 ),
1254 )),
1255 }
1256 }
1257 }
1258 Err(e) => respond_err(e),
1259 }
1260 }
1261 notify_closed();
1262 s.conns
1263 .lock()
1264 .unwrap_or_else(|e| e.into_inner())
1265 .remove(&id);
1266 shutdown();
1267}
1268
1269#[allow(dead_code)]
1270fn _assert_transport_used() {
1271 let _ = transport::current_uid;
1272}
1273
1274#[cfg(test)]
1275mod options_tests {
1276 use super::*;
1277
1278 #[test]
1279 fn pipe_instance_cap_is_small_finite_and_clamped() {
1280 let d = HostOptions::default();
1281 assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1282 assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1283 assert_eq!(
1284 HostOptions::new()
1285 .max_pipe_instances(4)
1286 .get_max_pipe_instances(),
1287 4
1288 );
1289 assert_eq!(
1290 HostOptions::new()
1291 .max_pipe_instances(0)
1292 .get_max_pipe_instances(),
1293 1
1294 );
1295 assert_eq!(
1297 HostOptions::new()
1298 .max_pipe_instances(u32::MAX)
1299 .get_max_pipe_instances(),
1300 254
1301 );
1302 }
1303}
1304
1305#[cfg(test)]
1306mod monitor_tests {
1307 use super::*;
1308 use std::sync::atomic::AtomicUsize;
1309
1310 const LONG: Duration = Duration::from_secs(600);
1312
1313 struct Quiet {
1314 handoffs: AtomicUsize,
1315 }
1316
1317 impl Handler for Quiet {
1318 fn call(
1319 &self,
1320 _cx: &CallContext,
1321 _method: &str,
1322 _params: &Value,
1323 _emit: &Emit,
1324 ) -> Result<Value, ServiceError> {
1325 Ok(json!({}))
1326 }
1327 fn on_handoff(&self) {
1328 self.handoffs.fetch_add(1, Ordering::SeqCst);
1329 }
1330 }
1331
1332 fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1333 let deadline = Instant::now() + Duration::from_secs(20);
1334 while !ok() {
1335 assert!(Instant::now() < deadline, "timed out waiting for {what}");
1336 std::thread::sleep(Duration::from_millis(5));
1337 }
1338 }
1339
1340 fn host_in(
1342 dir: &tempfile::TempDir,
1343 interval: Duration,
1344 alive: fn(u32) -> bool,
1345 ) -> (Suite, Hosted, Arc<Quiet>) {
1346 let suite = Suite::at(dir.path().join("suite")).unwrap();
1347 let mut cfg = ServiceConfig::new(
1348 "watch",
1349 "incumbent",
1350 5,
1351 "test",
1352 Allowlist::new().allow("*", ["echo"]),
1353 );
1354 cfg.takeover_timeout = Duration::from_secs(20);
1355 cfg.handoff_grace = Duration::from_secs(1);
1356 let handler = Arc::new(Quiet {
1357 handoffs: AtomicUsize::new(0),
1358 });
1359 let hosted = start_host(
1360 &suite,
1361 cfg,
1362 handler.clone(),
1363 HostOptions::default(),
1364 WatchTuning { interval, alive },
1365 )
1366 .expect("host starts");
1367 (suite, hosted, handler)
1368 }
1369
1370 fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1371 TakeoverRequest {
1372 role: role.into(),
1373 rank,
1374 pid,
1375 requested_at_ms: now_ms(),
1376 }
1377 }
1378
1379 fn other_pid() -> u32 {
1381 std::process::id().wrapping_add(1)
1382 }
1383
1384 #[test]
1385 fn a_request_from_another_process_is_found_by_the_file_check() {
1386 let dir = tempfile::tempdir().unwrap();
1387 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1388 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1390 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1391 assert!(
1392 hosted.wait_released(Duration::from_secs(20)),
1393 "the incumbent must hand off"
1394 );
1395 assert!(hosted.handoff_requested());
1396 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1397 }
1398
1399 #[test]
1400 fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1401 let dir = tempfile::tempdir().unwrap();
1402 let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1403 let bell = hosted.shared.bell.clone();
1404 wait_for("the first pass", || bell.passes() >= 1);
1405 write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1406 assert!(
1407 hosted.wait_released(Duration::from_secs(20)),
1408 "a 10-minute interval must not delay a request raised in this process"
1409 );
1410 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1411 }
1412
1413 #[test]
1414 fn releasing_the_host_ends_the_monitor_at_once() {
1415 let dir = tempfile::tempdir().unwrap();
1416 let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1417 let bell = hosted.shared.bell.clone();
1418 wait_for("the first pass", || bell.passes() >= 1);
1419 assert!(!bell.exited());
1420 hosted.shutdown();
1421 wait_for("the monitor thread to return", || bell.exited());
1422 }
1423
1424 #[test]
1425 fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1426 let dir = tempfile::tempdir().unwrap();
1427 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1428 let bell = hosted.shared.bell.clone();
1429 let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1431 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1432 wait_for("the request to be judged", || bell.reads() >= 1);
1433 let before = bell.wakes();
1434 wait_for("five more checks", || bell.wakes() >= before + 5);
1435 assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1436 assert!(!hosted.handoff_requested());
1437
1438 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1440 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1441 assert!(hosted.wait_released(Duration::from_secs(20)));
1442 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1443 }
1444
1445 #[test]
1446 fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1447 let dir = tempfile::tempdir().unwrap();
1448 let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1449 let bell = hosted.shared.bell.clone();
1450 let (start, woke_before) = (Instant::now(), bell.wakes());
1451 std::thread::sleep(Duration::from_secs(2));
1452 let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1453 let allowed = 3u64.max(secs.ceil() as u64 + 1);
1456 assert!(
1457 woke <= allowed,
1458 "the monitor woke {woke} times in {secs:.1} s"
1459 );
1460 drop(hosted);
1461 }
1462
1463 #[test]
1466 fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1467 let dir = tempfile::tempdir().unwrap();
1468 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1469 let bell = hosted.shared.bell.clone();
1470 let path = takeover_path(&suite, "watch");
1471 std::fs::create_dir_all(&path).unwrap();
1472 wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1474 assert!(!hosted.handoff_requested());
1475
1476 std::fs::remove_dir(&path).unwrap();
1478 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1479 write_atomic(&path, &body).unwrap();
1480 assert!(hosted.wait_released(Duration::from_secs(20)));
1481 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1482 }
1483
1484 #[test]
1485 fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1486 let dir = tempfile::tempdir().unwrap();
1487 let suite = Suite::at(dir.path().join("suite")).unwrap();
1488 let record = record_path(&suite, "watch");
1489 std::fs::create_dir_all(&record).unwrap();
1490 let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1491 cfg.takeover_timeout = Duration::from_millis(300);
1492 let err = start_host(
1493 &suite,
1494 cfg,
1495 Arc::new(Quiet {
1496 handoffs: AtomicUsize::new(0),
1497 }),
1498 HostOptions::default(),
1499 WatchTuning {
1500 interval: LONG,
1501 alive: |_| true,
1502 },
1503 )
1504 .err()
1505 .expect("a record that cannot be read must not be treated as no record");
1506 match err {
1507 HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1508 _ => panic!("expected the read error"),
1509 }
1510 assert!(record.is_dir(), "left as it was");
1511 }
1512
1513 #[test]
1514 fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1515 let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1516 cfg.takeover_timeout = Duration::from_secs(10);
1517 cfg.known_roles.insert("legacy".into(), 1);
1518 let now = 1_000_000_000_000u64;
1519 let live: fn(u32) -> bool = |_| true;
1520 let dead: fn(u32) -> bool = |_| false;
1521 let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1522 role: role.into(),
1523 rank,
1524 pid,
1525 requested_at_ms,
1526 };
1527
1528 assert_eq!(
1529 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1530 Some(0)
1531 );
1532 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1534 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1535 assert_eq!(
1537 obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1538 None
1539 );
1540 assert_eq!(
1542 obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1543 None
1544 );
1545 assert_eq!(
1547 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1548 Some(0)
1549 );
1550 assert_eq!(
1551 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1552 None
1553 );
1554 assert_eq!(
1556 obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1557 Some(1)
1558 );
1559 assert_eq!(
1560 obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1561 None
1562 );
1563 }
1564}