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 details: None,
940 };
941 send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
942 s.event(ServiceEvent::ConnectionRejected {
943 app: app.clone(),
944 reason: reason.into(),
945 });
946 shutdown();
947 };
948 if hello["type"] != "hello" || app.is_empty() {
949 return reject(
950 "first frame must be hello with an app label",
951 "bad_hello",
952 false,
953 );
954 }
955 if let Some(secret) = &s.cfg.private_secret {
956 if !secret.verify(hello["secret"].as_str().unwrap_or("")) {
959 let now = Instant::now();
960 let limited = s
961 .fails
962 .lock()
963 .unwrap_or_else(|e| e.into_inner())
964 .refuses_unverified(conn_key, now);
965 if limited {
966 return reject(
967 "too many failed authentications; wait before reconnecting",
968 crate::error::ERROR_RATE_LIMITED,
969 false,
970 );
971 }
972 s.fails
973 .lock()
974 .unwrap_or_else(|e| e.into_inner())
975 .record_failure(conn_key, now);
976 return reject(
977 "missing or wrong secret",
978 crate::error::ERROR_UNAUTHENTICATED,
979 false,
980 );
981 }
982 }
983 if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
984 return reject("protocol major mismatch", "protocol_mismatch", false);
985 }
986 if s.handoff.load(Ordering::SeqCst) {
987 return reject(
988 "host is handing off",
989 crate::error::ERROR_HOST_HANDOFF,
990 true,
991 );
992 }
993 if !s.cfg.allowlist.app_known(&app) {
994 return reject(
995 "app is not on the service allowlist",
996 crate::error::ERROR_NOT_ALLOWED,
997 false,
998 );
999 }
1000 let id = conn_key;
1001 s.conns
1002 .lock()
1003 .unwrap_or_else(|e| e.into_inner())
1004 .insert(id, shutdown.clone());
1005 s.event(ServiceEvent::ConnectionOpened {
1006 conn: id,
1007 app: app.clone(),
1008 });
1009 send(&json!({
1010 "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
1011 "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
1012 "methods": s.cfg.allowlist.methods_for(&app),
1013 "frame_limit": limit, "max_message": s.max_message,
1014 "chunk_bytes": crate::chunk::chunk_bytes_for(limit),
1015 }));
1016
1017 let caller = Caller::local_app(app.clone(), peer.clone());
1018 let closed_notified = Arc::new(AtomicBool::new(false));
1019 let notify_closed = {
1020 let (s, flag) = (s.clone(), closed_notified.clone());
1021 move || {
1022 if !flag.swap(true, Ordering::SeqCst) {
1023 s.handler.connection_closed(id);
1024 }
1025 }
1026 };
1027 let chunk_bytes = crate::chunk::chunk_bytes_for(limit);
1030 let mut uploads = crate::chunk::Uploads::default();
1031 let mut download: Option<crate::chunk::Download> = None;
1032 let mut downloads_made: u64 = 0;
1033 loop {
1035 let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
1036 Ok(Some(v)) => v,
1037 Ok(None) | Err(FrameError::Io(_)) => break,
1038 Err(e) => {
1039 send(
1040 &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
1041 );
1042 continue;
1043 }
1044 };
1045 if let Some(secret) = &s.cfg.private_secret {
1046 if !secret.verify(req["secret"].as_str().unwrap_or("")) {
1049 let now = Instant::now();
1050 let limited = s
1051 .fails
1052 .lock()
1053 .unwrap_or_else(|e| e.into_inner())
1054 .refuses_unverified(conn_key, now);
1055 if limited {
1056 s.event(ServiceEvent::ConnectionRejected {
1057 app: app.clone(),
1058 reason: "too many failed authentications".into(),
1059 });
1060 send(
1061 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1062 "error": ServiceError::new(crate::error::ERROR_RATE_LIMITED, "too many failed authentications; wait before reconnecting").to_value()}),
1063 );
1064 break;
1065 }
1066 s.fails
1067 .lock()
1068 .unwrap_or_else(|e| e.into_inner())
1069 .record_failure(conn_key, now);
1070 s.event(ServiceEvent::ConnectionRejected {
1071 app: app.clone(),
1072 reason: "missing or wrong secret".into(),
1073 });
1074 send(
1075 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1076 "error": ServiceError::new(crate::error::ERROR_UNAUTHENTICATED, "missing or wrong secret").to_value()}),
1077 );
1078 break;
1079 }
1080 }
1081 let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
1082 send(
1083 &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
1084 );
1085 continue;
1086 };
1087 let respond_err = |e: ServiceError| {
1088 send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
1089 };
1090 uploads.expire();
1092 let mut method = req["method"].as_str().unwrap_or("").to_string();
1093 let chunked_ok = req["chunked_ok"] == true && s.max_message > 0;
1094 if s.handoff.load(Ordering::SeqCst) {
1095 respond_err(ServiceError::handoff());
1096 continue;
1097 }
1098 let mut assembled: Option<Value> = None;
1101 if method == crate::chunk::METHOD_FETCH {
1102 let answer = match &download {
1103 Some(d) => d.fetch(req.get("params").unwrap_or(&Value::Null)),
1104 None => Err(ServiceError::new(
1105 crate::error::ERROR_BAD_CHUNK,
1106 "no chunked result is held on this connection",
1107 )),
1108 };
1109 match answer {
1110 Ok(v) => send(&json!({"type": "response", "id": rid, "ok": true, "result": v})),
1111 Err(e) => respond_err(e),
1112 }
1113 continue;
1114 }
1115 if method == crate::chunk::METHOD_UPLOAD {
1116 if s.max_message == 0 {
1117 respond_err(ServiceError::new(
1118 crate::error::ERROR_REQUEST_TOO_LARGE,
1119 "this host does not accept chunked requests",
1120 ));
1121 continue;
1122 }
1123 let authorize = |m: &str| {
1126 crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, m).inspect_err(
1127 |_| {
1128 s.event(ServiceEvent::Denied {
1129 conn: id,
1130 app: app.clone(),
1131 method: m.to_string(),
1132 });
1133 },
1134 )
1135 };
1136 match uploads.accept_with(
1137 req.get("params").unwrap_or(&Value::Null),
1138 s.max_message,
1139 &authorize,
1140 ) {
1141 Ok(crate::chunk::UploadStep::More { received }) => {
1142 send(&json!({"type": "response", "id": rid, "ok": true,
1143 "result": {"received": received}}));
1144 continue;
1145 }
1146 Ok(crate::chunk::UploadStep::Done { method: m, params }) => {
1147 method = m;
1148 assembled = Some(params);
1149 }
1150 Err(e) => {
1151 respond_err(e);
1152 continue;
1153 }
1154 }
1155 }
1156 if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
1157 s.event(ServiceEvent::Denied {
1158 conn: id,
1159 app: app.clone(),
1160 method: method.clone(),
1161 });
1162 respond_err(e);
1163 continue;
1164 }
1165 if method == "ping" {
1166 send(
1167 &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
1168 );
1169 continue;
1170 }
1171 let params =
1172 assembled.unwrap_or_else(|| req.get("params").cloned().unwrap_or_else(|| json!({})));
1173 let cx = new_context(
1174 id,
1175 caller.clone(),
1176 Arc::new(AtomicBool::new(false)),
1177 s.handoff.clone(),
1178 );
1179 let emit: Emit = {
1180 let (write, s, method) = (write.clone(), s.clone(), method.clone());
1181 Arc::new(move |ev| {
1182 if !write(&json!({"type": "event", "id": rid, "event": ev})) {
1183 s.event(ServiceEvent::FrameTooLarge {
1184 conn: id,
1185 method: method.clone(),
1186 kind: "event",
1187 limit,
1188 });
1189 }
1190 })
1191 };
1192 s.inflight.fetch_add(1, Ordering::SeqCst);
1193 let done = AtomicBool::new(false);
1194 let result = std::thread::scope(|sc| {
1195 let watcher = sc.spawn(|| {
1198 while !done.load(Ordering::SeqCst) {
1199 if peer_closed() {
1200 cx.closed.store(true, Ordering::SeqCst);
1201 notify_closed();
1202 return;
1203 }
1204 std::thread::park_timeout(Duration::from_millis(40));
1205 }
1206 });
1207 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1208 s.handler.call(&cx, &method, ¶ms, &emit)
1209 }))
1210 .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
1211 done.store(true, Ordering::SeqCst);
1212 watcher.thread().unpark();
1213 r
1214 });
1215 s.inflight.fetch_sub(1, Ordering::SeqCst);
1216 if cx.peer_closed() {
1217 break;
1218 }
1219 match result {
1220 Ok(v) => {
1221 if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
1224 s.event(ServiceEvent::FrameTooLarge {
1225 conn: id,
1226 method: method.clone(),
1227 kind: "response",
1228 limit,
1229 });
1230 let held = chunked_ok
1233 .then(|| serde_json::to_string(&v).ok())
1234 .flatten()
1235 .filter(|text| text.len() <= s.max_message);
1236 drop(v);
1237 match held {
1238 Some(text) => {
1239 downloads_made += 1;
1240 let held = crate::chunk::Download::new(
1241 format!("d{downloads_made}"),
1242 text,
1243 chunk_bytes,
1244 );
1245 let descriptor = held.descriptor();
1246 download = Some(held);
1247 send(&json!({"type": "response", "id": rid, "ok": true,
1248 "result": Value::Null, "chunked": descriptor}));
1249 }
1250 None => respond_err(ServiceError::new(
1251 ERROR_RESPONSE_TOO_LARGE,
1252 format!(
1253 "the result of {method} is larger than the {limit}-byte frame limit"
1254 ),
1255 )),
1256 }
1257 }
1258 }
1259 Err(e) => respond_err(e),
1260 }
1261 }
1262 notify_closed();
1263 s.conns
1264 .lock()
1265 .unwrap_or_else(|e| e.into_inner())
1266 .remove(&id);
1267 shutdown();
1268}
1269
1270#[allow(dead_code)]
1271fn _assert_transport_used() {
1272 let _ = transport::current_uid;
1273}
1274
1275#[cfg(test)]
1276mod options_tests {
1277 use super::*;
1278
1279 #[test]
1280 fn pipe_instance_cap_is_small_finite_and_clamped() {
1281 let d = HostOptions::default();
1282 assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1283 assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1284 assert_eq!(
1285 HostOptions::new()
1286 .max_pipe_instances(4)
1287 .get_max_pipe_instances(),
1288 4
1289 );
1290 assert_eq!(
1291 HostOptions::new()
1292 .max_pipe_instances(0)
1293 .get_max_pipe_instances(),
1294 1
1295 );
1296 assert_eq!(
1298 HostOptions::new()
1299 .max_pipe_instances(u32::MAX)
1300 .get_max_pipe_instances(),
1301 254
1302 );
1303 }
1304}
1305
1306#[cfg(test)]
1307mod monitor_tests {
1308 use super::*;
1309 use std::sync::atomic::AtomicUsize;
1310
1311 const LONG: Duration = Duration::from_secs(600);
1313
1314 struct Quiet {
1315 handoffs: AtomicUsize,
1316 }
1317
1318 impl Handler for Quiet {
1319 fn call(
1320 &self,
1321 _cx: &CallContext,
1322 _method: &str,
1323 _params: &Value,
1324 _emit: &Emit,
1325 ) -> Result<Value, ServiceError> {
1326 Ok(json!({}))
1327 }
1328 fn on_handoff(&self) {
1329 self.handoffs.fetch_add(1, Ordering::SeqCst);
1330 }
1331 }
1332
1333 fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1334 let deadline = Instant::now() + Duration::from_secs(20);
1335 while !ok() {
1336 assert!(Instant::now() < deadline, "timed out waiting for {what}");
1337 std::thread::sleep(Duration::from_millis(5));
1338 }
1339 }
1340
1341 fn host_in(
1343 dir: &tempfile::TempDir,
1344 interval: Duration,
1345 alive: fn(u32) -> bool,
1346 ) -> (Suite, Hosted, Arc<Quiet>) {
1347 let suite = Suite::at(dir.path().join("suite")).unwrap();
1348 let mut cfg = ServiceConfig::new(
1349 "watch",
1350 "incumbent",
1351 5,
1352 "test",
1353 Allowlist::new().allow("*", ["echo"]),
1354 );
1355 cfg.takeover_timeout = Duration::from_secs(20);
1356 cfg.handoff_grace = Duration::from_secs(1);
1357 let handler = Arc::new(Quiet {
1358 handoffs: AtomicUsize::new(0),
1359 });
1360 let hosted = start_host(
1361 &suite,
1362 cfg,
1363 handler.clone(),
1364 HostOptions::default(),
1365 WatchTuning { interval, alive },
1366 )
1367 .expect("host starts");
1368 (suite, hosted, handler)
1369 }
1370
1371 fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1372 TakeoverRequest {
1373 role: role.into(),
1374 rank,
1375 pid,
1376 requested_at_ms: now_ms(),
1377 }
1378 }
1379
1380 fn other_pid() -> u32 {
1382 std::process::id().wrapping_add(1)
1383 }
1384
1385 #[test]
1386 fn a_request_from_another_process_is_found_by_the_file_check() {
1387 let dir = tempfile::tempdir().unwrap();
1388 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1389 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1391 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1392 assert!(
1393 hosted.wait_released(Duration::from_secs(20)),
1394 "the incumbent must hand off"
1395 );
1396 assert!(hosted.handoff_requested());
1397 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1398 }
1399
1400 #[test]
1401 fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1402 let dir = tempfile::tempdir().unwrap();
1403 let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1404 let bell = hosted.shared.bell.clone();
1405 wait_for("the first pass", || bell.passes() >= 1);
1406 write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1407 assert!(
1408 hosted.wait_released(Duration::from_secs(20)),
1409 "a 10-minute interval must not delay a request raised in this process"
1410 );
1411 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1412 }
1413
1414 #[test]
1415 fn releasing_the_host_ends_the_monitor_at_once() {
1416 let dir = tempfile::tempdir().unwrap();
1417 let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1418 let bell = hosted.shared.bell.clone();
1419 wait_for("the first pass", || bell.passes() >= 1);
1420 assert!(!bell.exited());
1421 hosted.shutdown();
1422 wait_for("the monitor thread to return", || bell.exited());
1423 }
1424
1425 #[test]
1426 fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1427 let dir = tempfile::tempdir().unwrap();
1428 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1429 let bell = hosted.shared.bell.clone();
1430 let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1432 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1433 wait_for("the request to be judged", || bell.reads() >= 1);
1434 let before = bell.wakes();
1435 wait_for("five more checks", || bell.wakes() >= before + 5);
1436 assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1437 assert!(!hosted.handoff_requested());
1438
1439 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1441 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1442 assert!(hosted.wait_released(Duration::from_secs(20)));
1443 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1444 }
1445
1446 #[test]
1447 fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1448 let dir = tempfile::tempdir().unwrap();
1449 let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1450 let bell = hosted.shared.bell.clone();
1451 let (start, woke_before) = (Instant::now(), bell.wakes());
1452 std::thread::sleep(Duration::from_secs(2));
1453 let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1454 let allowed = 3u64.max(secs.ceil() as u64 + 1);
1457 assert!(
1458 woke <= allowed,
1459 "the monitor woke {woke} times in {secs:.1} s"
1460 );
1461 drop(hosted);
1462 }
1463
1464 #[test]
1467 fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1468 let dir = tempfile::tempdir().unwrap();
1469 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1470 let bell = hosted.shared.bell.clone();
1471 let path = takeover_path(&suite, "watch");
1472 std::fs::create_dir_all(&path).unwrap();
1473 wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1475 assert!(!hosted.handoff_requested());
1476
1477 std::fs::remove_dir(&path).unwrap();
1479 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1480 write_atomic(&path, &body).unwrap();
1481 assert!(hosted.wait_released(Duration::from_secs(20)));
1482 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1483 }
1484
1485 #[test]
1486 fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1487 let dir = tempfile::tempdir().unwrap();
1488 let suite = Suite::at(dir.path().join("suite")).unwrap();
1489 let record = record_path(&suite, "watch");
1490 std::fs::create_dir_all(&record).unwrap();
1491 let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1492 cfg.takeover_timeout = Duration::from_millis(300);
1493 let err = start_host(
1494 &suite,
1495 cfg,
1496 Arc::new(Quiet {
1497 handoffs: AtomicUsize::new(0),
1498 }),
1499 HostOptions::default(),
1500 WatchTuning {
1501 interval: LONG,
1502 alive: |_| true,
1503 },
1504 )
1505 .err()
1506 .expect("a record that cannot be read must not be treated as no record");
1507 match err {
1508 HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1509 _ => panic!("expected the read error"),
1510 }
1511 assert!(record.is_dir(), "left as it was");
1512 }
1513
1514 #[test]
1515 fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1516 let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1517 cfg.takeover_timeout = Duration::from_secs(10);
1518 cfg.known_roles.insert("legacy".into(), 1);
1519 let now = 1_000_000_000_000u64;
1520 let live: fn(u32) -> bool = |_| true;
1521 let dead: fn(u32) -> bool = |_| false;
1522 let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1523 role: role.into(),
1524 rank,
1525 pid,
1526 requested_at_ms,
1527 };
1528
1529 assert_eq!(
1530 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1531 Some(0)
1532 );
1533 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1535 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1536 assert_eq!(
1538 obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1539 None
1540 );
1541 assert_eq!(
1543 obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1544 None
1545 );
1546 assert_eq!(
1548 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1549 Some(0)
1550 );
1551 assert_eq!(
1552 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1553 None
1554 );
1555 assert_eq!(
1557 obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1558 Some(1)
1559 );
1560 assert_eq!(
1561 obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1562 None
1563 );
1564 }
1565}