1use std::sync::Arc;
12
13mod store_guard;
14mod store_identity;
15#[cfg(unix)]
16use store_guard::ensure_claimed_parent_identity;
17#[cfg(unix)]
18pub use store_guard::{acquire_daemon_store_guards, bind_daemon_store_files, claim_stores};
19pub use store_guard::{assert_daemon_store_identities, DaemonStoreGuard};
20#[cfg(unix)]
21pub use store_identity::claimed_daemon_store_identity;
22mod supervisor_marker;
23#[cfg(unix)]
24pub use supervisor_marker::supervisor_marker_path;
25
26#[cfg(unix)]
27use std::io::Write as _;
28#[cfg(unix)]
29use std::os::unix::fs::{MetadataExt, PermissionsExt};
30#[cfg(unix)]
31use std::os::unix::io::AsRawFd;
32use std::path::PathBuf;
33
34#[cfg(unix)]
35use async_trait::async_trait;
36#[cfg(unix)]
37use libc;
38use serde::{Deserialize, Serialize};
39#[cfg(unix)]
40use tokio::io::{AsyncReadExt, AsyncWriteExt};
41#[cfg(unix)]
42use tokio::net::{UnixListener, UnixStream};
43
44#[cfg(unix)]
45use crate::pack::RequestIdentity;
46#[cfg(unix)]
47use khive_db::{run_checkpoint_task, CheckpointConfig, CheckpointLifecycleOwner, ConnectionPool};
48
49mod load_limits;
50#[cfg(unix)]
51use load_limits::{admit_or_refuse_busy, ConnectionAdmission};
52pub use load_limits::{
53 recall_ledger_snapshot, track_recall_ledger_task, ConnectionCapSnapshot, RecallLedgerSnapshot,
54};
55
56pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
58
59pub const PROTOCOL_VERSION: u32 = 8;
68
69pub const DEFAULT_DEMAND_IDLE_SECS: u64 = 1_800;
71
72#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum DaemonLifetime {
76 Demand,
77 #[default]
78 Persistent,
79}
80
81#[derive(Debug, Clone, Copy)]
83pub struct DaemonOptions {
84 pub lifetime: DaemonLifetime,
85 pub idle_interval: std::time::Duration,
86}
87
88impl Default for DaemonOptions {
89 fn default() -> Self {
90 Self {
91 lifetime: DaemonLifetime::Persistent,
92 idle_interval: std::time::Duration::from_secs(DEFAULT_DEMAND_IDLE_SECS),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct DaemonStartupReport {
100 pub skipped_components: Vec<String>,
101 pub idle_ineligible_reasons: Vec<String>,
103}
104
105#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
106#[serde(rename_all = "snake_case")]
107pub enum DaemonLifecyclePhase {
108 Serving,
109 Draining,
110 Stopped,
111}
112
113#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
114#[serde(rename_all = "snake_case")]
115pub enum DaemonShutdownReason {
116 Idle,
117 Signal,
118}
119
120#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
122pub struct DaemonLifecycleSnapshot {
123 pub lifetime: DaemonLifetime,
124 pub instance_generation: String,
125 pub effective_idle_interval_ms: u64,
126 pub phase: DaemonLifecyclePhase,
127 pub shutdown_reason: Option<DaemonShutdownReason>,
128 pub skipped_components: Vec<String>,
129 pub idle_ineligible_reasons: Vec<String>,
130 pub ordinary_requests: usize,
131 pub idle_blockers: Vec<String>,
132}
133
134#[cfg(unix)]
135struct DaemonLifecycle {
136 options: DaemonOptions,
137 state: std::sync::Mutex<DaemonLifecycleState>,
138 connections: ConnectionAdmission,
140}
141
142#[cfg(unix)]
143struct DaemonLifecycleState {
144 snapshot: DaemonLifecycleSnapshot,
145 last_request_completion: Option<tokio::time::Instant>,
146}
147
148#[cfg(unix)]
149impl DaemonLifecycle {
150 fn new(options: DaemonOptions, report: DaemonStartupReport) -> Self {
151 Self {
152 options,
153 state: std::sync::Mutex::new(DaemonLifecycleState {
154 snapshot: DaemonLifecycleSnapshot {
155 lifetime: options.lifetime,
156 instance_generation: uuid::Uuid::new_v4().to_string(),
157 effective_idle_interval_ms: options
158 .idle_interval
159 .as_millis()
160 .min(u128::from(u64::MAX))
161 as u64,
162 phase: DaemonLifecyclePhase::Serving,
163 shutdown_reason: None,
164 skipped_components: report.skipped_components,
165 idle_ineligible_reasons: report.idle_ineligible_reasons,
166 ordinary_requests: 0,
167 idle_blockers: Vec::new(),
168 },
169 last_request_completion: None,
170 }),
171 connections: ConnectionAdmission::from_env(),
172 }
173 }
174
175 fn snapshot(&self) -> DaemonLifecycleSnapshot {
176 self.state
177 .lock()
178 .unwrap_or_else(std::sync::PoisonError::into_inner)
179 .snapshot
180 .clone()
181 }
182
183 fn ready(&self) {
184 self.state
185 .lock()
186 .unwrap_or_else(std::sync::PoisonError::into_inner)
187 .last_request_completion = Some(tokio::time::Instant::now());
188 }
189
190 fn admit(self: &Arc<Self>) -> Option<OrdinaryRequestGuard> {
191 let mut state = self
192 .state
193 .lock()
194 .unwrap_or_else(std::sync::PoisonError::into_inner);
195 if state.snapshot.phase != DaemonLifecyclePhase::Serving {
196 return None;
197 }
198 state.snapshot.ordinary_requests += 1;
199 Some(OrdinaryRequestGuard(Arc::clone(self)))
200 }
201
202 fn try_idle(&self, blockers: impl FnOnce() -> Vec<String>) -> bool {
204 let mut state = self
205 .state
206 .lock()
207 .unwrap_or_else(std::sync::PoisonError::into_inner);
208 if self.options.lifetime != DaemonLifetime::Demand
209 || state.snapshot.phase != DaemonLifecyclePhase::Serving
210 || state.snapshot.ordinary_requests != 0
211 || !state.snapshot.idle_ineligible_reasons.is_empty()
212 || state
213 .last_request_completion
214 .is_none_or(|last| last.elapsed() < self.options.idle_interval)
215 {
216 return false;
217 }
218 state.snapshot.idle_blockers = blockers();
219 if !state.snapshot.idle_blockers.is_empty() {
220 return false;
221 }
222 state.snapshot.phase = DaemonLifecyclePhase::Draining;
223 state.snapshot.shutdown_reason = Some(DaemonShutdownReason::Idle);
224 true
225 }
226
227 fn draining(&self, reason: DaemonShutdownReason) {
228 let mut state = self
229 .state
230 .lock()
231 .unwrap_or_else(std::sync::PoisonError::into_inner);
232 if state.snapshot.phase != DaemonLifecyclePhase::Stopped {
233 state.snapshot.phase = DaemonLifecyclePhase::Draining;
234 state.snapshot.shutdown_reason = Some(reason);
235 }
236 }
237
238 fn stopped(&self) {
239 self.state
240 .lock()
241 .unwrap_or_else(std::sync::PoisonError::into_inner)
242 .snapshot
243 .phase = DaemonLifecyclePhase::Stopped;
244 }
245}
246
247#[cfg(unix)]
248struct OrdinaryRequestGuard(Arc<DaemonLifecycle>);
249
250#[cfg(unix)]
251impl Drop for OrdinaryRequestGuard {
252 fn drop(&mut self) {
253 let mut state = self
254 .0
255 .state
256 .lock()
257 .unwrap_or_else(std::sync::PoisonError::into_inner);
258 state.snapshot.ordinary_requests -= 1;
259 state.last_request_completion = Some(tokio::time::Instant::now());
261 }
262}
263
264#[doc(hidden)]
268pub const DAEMON_LEXICAL_TIMEOUT_MARKER: &str = "__khive_daemon_lexical_timeout";
269
270const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 10;
271#[cfg(unix)]
275const INITIAL_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
276
277#[cfg(unix)]
278fn next_accept_error_backoff(previous: Option<std::time::Duration>) -> std::time::Duration {
279 previous
280 .map(|delay| delay.saturating_mul(2))
281 .unwrap_or_else(|| std::time::Duration::from_millis(10))
282 .min(std::time::Duration::from_secs(1))
283}
284
285fn khive_dir() -> PathBuf {
301 khive_root_from(
302 std::env::var("HOME").ok(),
303 std::env::var("USERPROFILE").ok(),
304 )
305}
306
307fn khive_root_from(home: Option<String>, userprofile: Option<String>) -> PathBuf {
310 home.filter(|v| !v.trim().is_empty())
311 .or_else(|| userprofile.filter(|v| !v.trim().is_empty()))
312 .map(PathBuf::from)
313 .unwrap_or_else(last_resort_root)
314 .join(".khive")
315}
316
317pub fn volume_lock_dir() -> Result<PathBuf, khive_db::SqliteError> {
324 khive_db::default_volume_lock_dir()
325}
326
327#[cfg(unix)]
329fn last_resort_root() -> PathBuf {
330 PathBuf::from(".")
331}
332
333#[cfg(not(unix))]
334fn last_resort_root() -> PathBuf {
335 std::env::temp_dir()
336}
337
338#[cfg(unix)]
340const SOCKET_PATH_ENV: &str = "KHIVE_SOCKET";
341
342#[cfg(unix)]
344const PID_PATH_ENV: &str = "KHIVE_PID";
345
346#[cfg(unix)]
354fn path_override(key: &str) -> Option<PathBuf> {
355 match std::env::var(key) {
356 Ok(p) if !p.is_empty() => Some(PathBuf::from(p)),
357 _ => None,
358 }
359}
360
361#[cfg(unix)]
362fn default_socket_path() -> PathBuf {
363 khive_dir().join("khived.sock")
364}
365
366#[cfg(unix)]
367fn default_pid_path() -> PathBuf {
368 khive_dir().join("khived.pid")
369}
370
371#[cfg(unix)]
377pub fn socket_path() -> PathBuf {
378 path_override(SOCKET_PATH_ENV).unwrap_or_else(default_socket_path)
379}
380
381#[cfg(unix)]
387pub fn pid_path() -> PathBuf {
388 path_override(PID_PATH_ENV).unwrap_or_else(default_pid_path)
389}
390
391#[cfg(unix)]
408fn ensure_rendezvous_overrides_paired() -> anyhow::Result<()> {
409 match (path_override(SOCKET_PATH_ENV), path_override(PID_PATH_ENV)) {
410 (Some(socket), None) => anyhow::bail!(
411 "refusing to start: {SOCKET_PATH_ENV} is set to {} but {PID_PATH_ENV} is not set. \
412 The socket and the PID file are two halves of one daemon rendezvous and must move \
413 together: with only {SOCKET_PATH_ENV} set, this daemon would bind a private socket \
414 while claiming the shared PID file at {}, which belongs to the default rendezvous \
415 served on {}. Set {PID_PATH_ENV} to a private path beside the socket, or unset \
416 {SOCKET_PATH_ENV} to share the default rendezvous.",
417 socket.display(),
418 default_pid_path().display(),
419 default_socket_path().display(),
420 ),
421 (None, Some(pid)) => anyhow::bail!(
422 "refusing to start: {PID_PATH_ENV} is set to {} but {SOCKET_PATH_ENV} is not set. \
423 The socket and the PID file are two halves of one daemon rendezvous and must move \
424 together: with only {PID_PATH_ENV} set, this daemon would write a private PID file \
425 while binding the shared socket at {}, the default rendezvous whose owner is \
426 recorded in {}. Set {SOCKET_PATH_ENV} to a private path beside the PID file, or \
427 unset {PID_PATH_ENV} to share the default rendezvous.",
428 pid.display(),
429 default_socket_path().display(),
430 default_pid_path().display(),
431 ),
432 _ => Ok(()),
433 }
434}
435
436pub fn lock_path() -> PathBuf {
445 if let Ok(p) = std::env::var("KHIVE_LOCK") {
446 if !p.is_empty() {
447 return PathBuf::from(p);
448 }
449 }
450 khive_dir().join("khived.recovery.lock")
451}
452
453#[cfg(unix)]
463pub fn recoverer_lock_path() -> PathBuf {
464 if let Ok(p) = std::env::var("KHIVE_RECOVERER_LOCK") {
465 if !p.is_empty() {
466 return PathBuf::from(p);
467 }
468 }
469 khive_dir().join("khived.recoverer.lock")
470}
471
472#[cfg(unix)]
473pub const SUPERVISOR_CLAIM_ENV: &str = "KHIVE_SUPERVISOR_CLAIM";
474
475#[cfg(unix)]
476fn read_supervisor_marker_claim() -> Option<(u32, String)> {
477 use std::io::Read;
478 use std::os::unix::fs::OpenOptionsExt;
479
480 let file = std::fs::OpenOptions::new()
481 .read(true)
482 .custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW)
483 .open(supervisor_marker_path())
484 .ok()?;
485 if !file.metadata().ok()?.is_file() {
486 return None;
487 }
488 let mut marker = String::new();
489 file.take(4097).read_to_string(&mut marker).ok()?;
490 if marker.len() > 4096 {
491 return None;
492 }
493 let mut lines = marker.lines();
494 if lines.next()?.is_empty() {
495 return None;
496 }
497 let pid = lines.next()?.parse::<u32>().ok().filter(|pid| *pid > 0)?;
498 lines
499 .next()?
500 .parse::<u64>()
501 .ok()
502 .filter(|seconds| *seconds > 0)?;
503 let claim = lines.next()?.to_string();
504 if lines.next().is_some() {
505 return None;
506 }
507 let parsed = uuid::Uuid::parse_str(&claim).ok()?;
508 if parsed.get_version() != Some(uuid::Version::Random) || parsed.to_string() != claim {
509 return None;
510 }
511 Some((pid, claim))
512}
513
514#[cfg(unix)]
515fn current_supervisor_claim() -> Option<String> {
516 let claim = std::env::var(SUPERVISOR_CLAIM_ENV).ok()?;
517 let (pid, published_claim) = read_supervisor_marker_claim()?;
518 (pid == std::process::id() && claim == published_claim).then_some(claim)
519}
520
521#[cfg(unix)]
522fn open_lock_file(path: &std::path::Path) -> std::io::Result<std::fs::File> {
523 if let Some(parent) = path.parent() {
524 let _ = std::fs::create_dir_all(parent);
525 }
526 std::fs::OpenOptions::new()
527 .create(true)
528 .truncate(false)
529 .write(true)
530 .open(path)
531}
532
533#[cfg(unix)]
534fn acquire_flock_blocking(path: &std::path::Path, label: &str) -> Option<std::fs::File> {
535 let file = match open_lock_file(path) {
536 Ok(f) => f,
537 Err(e) => {
538 tracing::warn!(error = %e, path = ?path, "cannot open {label} lock file");
539 return None;
540 }
541 };
542 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
544 if rc != 0 {
545 tracing::warn!("flock LOCK_EX failed on {label} lock");
546 return None;
547 }
548 Some(file)
549}
550
551#[cfg(unix)]
558pub fn acquire_recovery_lock() -> Option<std::fs::File> {
559 acquire_flock_blocking(&lock_path(), "recovery")
560}
561
562#[cfg(unix)]
577fn try_acquire_flock_until(
578 path: &std::path::Path,
579 deadline: std::time::Instant,
580) -> std::io::Result<Option<std::fs::File>> {
581 let file = open_lock_file(path)?;
582 let poll_interval = std::time::Duration::from_millis(10);
583 loop {
584 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
586 if rc == 0 {
587 return Ok(Some(file));
588 }
589 let err = std::io::Error::last_os_error();
590 if err.raw_os_error() != Some(libc::EWOULDBLOCK) {
591 return Err(err);
592 }
593 let now = std::time::Instant::now();
594 if now >= deadline {
595 return Ok(None);
596 }
597 std::thread::sleep(poll_interval.min(deadline - now));
598 }
599}
600
601#[cfg(unix)]
608pub fn try_acquire_daemon_boot_guard_until(
609 deadline: std::time::Instant,
610) -> std::io::Result<Option<DaemonBootGuard>> {
611 try_acquire_flock_until(&lock_path(), deadline)
612}
613
614#[cfg(unix)]
620pub fn try_acquire_recoverer_lock_until(
621 deadline: std::time::Instant,
622) -> std::io::Result<Option<std::fs::File>> {
623 try_acquire_flock_until(&recoverer_lock_path(), deadline)
624}
625
626#[cfg(unix)]
630pub type DaemonBootGuard = std::fs::File;
631
632#[cfg(unix)]
642pub fn acquire_daemon_boot_guard() -> anyhow::Result<DaemonBootGuard> {
643 acquire_recovery_lock()
644 .ok_or_else(|| anyhow::anyhow!("failed to acquire daemon boot/recovery lock"))
645}
646
647#[cfg(unix)]
654#[derive(Clone, Copy, PartialEq, Eq)]
655struct SocketIdentity {
656 dev: u64,
657 ino: u64,
658}
659
660#[cfg(unix)]
661fn socket_identity(path: &std::path::Path) -> Option<SocketIdentity> {
662 use std::os::unix::fs::MetadataExt;
663 let meta = std::fs::metadata(path).ok()?;
664 Some(SocketIdentity {
665 dev: meta.dev(),
666 ino: meta.ino(),
667 })
668}
669
670#[cfg(unix)]
684pub(crate) fn peer_uid(stream: &UnixStream) -> std::io::Result<u32> {
685 use std::os::fd::AsRawFd;
686 let fd = stream.as_raw_fd();
687
688 #[cfg(any(target_os = "macos", target_os = "ios", target_vendor = "apple"))]
689 {
690 let mut uid: libc::uid_t = 0;
691 let mut gid: libc::gid_t = 0;
692 let rc = unsafe { libc::getpeereid(fd, &mut uid, &mut gid) };
695 if rc != 0 {
696 return Err(std::io::Error::last_os_error());
697 }
698 Ok(uid as u32)
699 }
700
701 #[cfg(target_os = "linux")]
702 {
703 let mut cred = libc::ucred {
704 pid: 0,
705 uid: 0,
706 gid: 0,
707 };
708 let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
709 let rc = unsafe {
713 libc::getsockopt(
714 fd,
715 libc::SOL_SOCKET,
716 libc::SO_PEERCRED,
717 (&mut cred as *mut libc::ucred).cast::<libc::c_void>(),
718 &mut len,
719 )
720 };
721 if rc != 0 {
722 return Err(std::io::Error::last_os_error());
723 }
724 Ok(cred.uid)
725 }
726
727 #[cfg(not(any(
728 target_os = "linux",
729 target_os = "macos",
730 target_os = "ios",
731 target_vendor = "apple"
732 )))]
733 {
734 let _ = fd;
735 Err(std::io::Error::new(
736 std::io::ErrorKind::Unsupported,
737 "peer-credential capture is not implemented for this platform",
738 ))
739 }
740}
741
742#[cfg(unix)]
763pub(crate) fn uid_is_permitted(peer: u32, daemon_euid: u32) -> bool {
764 peer == daemon_euid
765}
766
767mod config_id;
770#[cfg(test)]
771use config_id::parse_config_id;
772pub use config_id::{
773 config_id_extra_embedder_exclusions, config_ids_compatible, first_config_mismatch_field,
774};
775
776#[derive(Serialize, Deserialize, Default)]
778pub struct DaemonRequestFrame {
779 pub ops: String,
780 #[serde(default)]
782 pub plan: bool,
783 #[serde(skip_serializing_if = "Option::is_none")]
784 pub presentation: Option<String>,
785 #[serde(skip_serializing_if = "Option::is_none")]
786 pub presentation_per_op: Option<Vec<Option<String>>>,
787 pub namespace: String,
793 #[serde(default)]
798 pub actor_id: Option<String>,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub process_ref: Option<String>,
806 #[serde(default)]
814 pub visible_namespaces: Vec<String>,
815 #[serde(default)]
822 pub config_id: String,
823 #[serde(default)]
827 pub protocol_version: u32,
828 #[serde(default)]
834 pub probe_only: bool,
835 #[serde(default)]
845 pub metrics_only: bool,
846 #[serde(default)]
849 #[serde(skip_serializing_if = "Option::is_none")]
850 pub format: Option<String>,
851 #[serde(default)]
853 #[serde(skip_serializing_if = "Option::is_none")]
854 pub format_per_op: Option<Vec<Option<String>>>,
855 #[serde(default)]
866 pub from_wire: bool,
867 #[serde(default)]
877 #[serde(skip_serializing_if = "Option::is_none")]
878 pub request_id: Option<u64>,
879}
880
881#[derive(Debug, Clone)]
883pub struct DaemonDispatchError {
884 pub message: String,
885 pub error_detail: serde_json::Value,
886}
887
888pub const ERROR_DETAIL_NESTING_DEPTH_LIMIT: usize = 64;
890
891fn error_detail_value_within_limit(value: &serde_json::Value) -> bool {
892 let mut pending = vec![(value, 0_usize)];
893 while let Some((value, depth)) = pending.pop() {
894 match value {
895 serde_json::Value::Array(items) if depth < ERROR_DETAIL_NESTING_DEPTH_LIMIT => {
896 pending.extend(items.iter().map(|child| (child, depth + 1)));
897 }
898 serde_json::Value::Object(fields) if depth < ERROR_DETAIL_NESTING_DEPTH_LIMIT => {
899 pending.extend(fields.values().map(|child| (child, depth + 1)));
900 }
901 serde_json::Value::Array(_) | serde_json::Value::Object(_) => return false,
902 _ => {}
903 }
904 }
905 true
906}
907
908fn drop_error_detail_iteratively(value: serde_json::Value) {
909 let mut pending = vec![value];
910 while let Some(value) = pending.pop() {
911 match value {
912 serde_json::Value::Array(items) => pending.extend(items),
913 serde_json::Value::Object(fields) => pending.extend(fields.into_values()),
914 _ => {}
915 }
916 }
917}
918
919impl DaemonDispatchError {
920 pub fn new(message: impl Into<String>, error_detail: Option<serde_json::Value>) -> Self {
922 let message = message.into();
923 let mut fields = match error_detail {
924 Some(serde_json::Value::Object(fields)) => fields,
925 Some(data) => serde_json::Map::from_iter([("data".to_string(), data)]),
926 None => serde_json::Map::new(),
927 };
928 let disposition = match fields
929 .get("domain_disposition")
930 .and_then(serde_json::Value::as_str)
931 {
932 Some("committed") => crate::DomainDisposition::Committed,
933 Some("not_committed") => crate::DomainDisposition::NotCommitted,
934 _ => crate::DomainDisposition::Unknown,
935 };
936 if disposition != crate::DomainDisposition::Committed {
937 if let Some(result) = fields.remove("domain_result") {
938 drop_error_detail_iteratively(result);
939 }
940 }
941 let rejected: Vec<String> = fields
942 .iter()
943 .filter(|(_, value)| !error_detail_value_within_limit(value))
944 .map(|(name, _)| name.clone())
945 .collect();
946 let omitted_result = rejected.iter().any(|name| name == "domain_result");
947 let omitted_detail = !rejected.is_empty();
948 for name in rejected {
949 if let Some(value) = fields.remove(&name) {
950 drop_error_detail_iteratively(value);
951 }
952 }
953 let mut error_detail = serde_json::Value::Object(fields);
954 if error_detail["kind"].as_str().is_none() {
955 error_detail["kind"] = serde_json::json!("internal");
956 }
957 if error_detail["message"].as_str().is_none() {
958 error_detail["message"] = serde_json::json!(message);
959 }
960 error_detail["domain_disposition"] = serde_json::json!(disposition.as_str());
961 if omitted_detail {
962 error_detail["code"] = serde_json::json!(if omitted_result {
963 "result_too_deep"
964 } else {
965 "error_detail_too_deep"
966 });
967 }
968 Self {
969 message,
970 error_detail,
971 }
972 }
973}
974
975#[derive(Serialize, Deserialize, Debug)]
977pub struct DaemonResponseFrame {
978 pub ok: bool,
979 pub result: Option<String>,
980 pub error: Option<String>,
981 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub error_detail: Option<serde_json::Value>,
986 pub namespace_mismatch: bool,
987 #[serde(default)]
991 pub config_mismatch: bool,
992 #[serde(default)]
999 pub served_config_id: Option<String>,
1000 #[serde(default)]
1005 pub version_mismatch: bool,
1006 #[serde(default)]
1010 pub daemon_protocol_version: u32,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub metrics: Option<MetricsSnapshot>,
1018 #[serde(default)]
1025 pub request_id: Option<u64>,
1026}
1027
1028#[cfg(unix)]
1035fn take_daemon_lexical_timeout_marker(raw: String) -> (String, Option<serde_json::Value>) {
1036 if !raw.contains(DAEMON_LEXICAL_TIMEOUT_MARKER) {
1037 return (raw, None);
1038 }
1039 let Ok(mut value) = serde_json::from_str::<serde_json::Value>(&raw) else {
1040 return (raw, None);
1041 };
1042 let Some(fields) = value.as_object_mut() else {
1043 return (raw, None);
1044 };
1045 if !fields
1046 .get("results")
1047 .is_some_and(serde_json::Value::is_array)
1048 {
1049 return (raw, None);
1050 }
1051 let Some(marker) = fields.remove(DAEMON_LEXICAL_TIMEOUT_MARKER) else {
1052 return (raw, None);
1053 };
1054 let detail =
1055 (marker.as_bool() == Some(true)).then(|| serde_json::json!({"lexical_timeout": true}));
1056 (
1057 serde_json::to_string(&value).expect("serde_json::Value is serializable"),
1058 detail,
1059 )
1060}
1061
1062#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
1067#[serde(default)]
1068pub struct CheckpointStoreMetrics {
1069 pub store_id: String,
1070 pub role: String,
1071 pub database: Option<String>,
1072 #[serde(flatten)]
1073 pub timing: khive_db::checkpoint::CheckpointTiming,
1074}
1075
1076#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
1087pub struct MetricsSnapshot {
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1090 pub lifecycle: Option<DaemonLifecycleSnapshot>,
1091 pub wal_pages: Option<u64>,
1096 #[serde(default)]
1099 pub wal_log_frames: Option<u64>,
1100 #[serde(default)]
1102 pub wal_checkpointed_frames: Option<u64>,
1103 #[serde(default)]
1105 pub wal_pending_frames: Option<u64>,
1106 #[serde(default)]
1108 pub wal_physical_bytes: Option<u64>,
1109 #[serde(default)]
1111 pub wal_observed_at_unix_ms: Option<u64>,
1112 #[serde(default)]
1116 pub wal_checkpoint_stores: Vec<CheckpointStoreMetrics>,
1117 pub wal_truncate_attempts: u64,
1121 pub wal_truncate_consecutive_failures: u64,
1125 #[serde(default)]
1130 pub wal_checkpoint_skipped_ticks: u64,
1131 #[serde(default)]
1133 pub wal_checkpoint_consecutive_skips: u64,
1134 #[serde(default)]
1137 pub wal_checkpoint_last_skip_wal_pages: Option<u64>,
1138 pub oldest_pinned_tx_micros: Option<u64>,
1142 pub oldest_pinned_tx_label: Option<String>,
1145 pub open_tx_count: usize,
1147 pub write_queue_depth: Option<usize>,
1152 pub write_queue_capacity: Option<usize>,
1156 #[serde(default)]
1158 pub write_last_queue_wait_micros: Option<u64>,
1159 #[serde(default)]
1161 pub write_last_transaction_acquire_micros: Option<u64>,
1162 #[serde(default)]
1164 pub write_last_body_micros: Option<u64>,
1165 #[serde(default)]
1167 pub write_last_commit_micros: Option<u64>,
1168 #[serde(default)]
1171 pub write_last_total_micros: Option<u64>,
1172 #[serde(default)]
1174 pub write_last_observed_at_unix_ms: Option<u64>,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub connections: Option<ConnectionCapSnapshot>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub recall_ledger: Option<RecallLedgerSnapshot>,
1184}
1185
1186#[cfg(unix)]
1190pub async fn read_frame<R>(stream: &mut R) -> std::io::Result<Vec<u8>>
1191where
1192 R: tokio::io::AsyncRead + Unpin,
1193{
1194 let mut len_buf = [0u8; 4];
1195 stream.read_exact(&mut len_buf).await?;
1196 let len = u32::from_be_bytes(len_buf) as usize;
1197 if len > MAX_FRAME_BYTES {
1198 return Err(std::io::Error::new(
1199 std::io::ErrorKind::InvalidData,
1200 format!("daemon frame of {len} bytes exceeds {MAX_FRAME_BYTES} cap"),
1201 ));
1202 }
1203 let mut buf = vec![0u8; len];
1204 stream.read_exact(&mut buf).await?;
1205 Ok(buf)
1206}
1207
1208#[cfg(unix)]
1209fn initial_frame_timeout_error() -> std::io::Error {
1210 std::io::Error::new(
1211 std::io::ErrorKind::TimedOut,
1212 "daemon initial request frame read timed out",
1213 )
1214}
1215
1216#[cfg(unix)]
1217async fn read_initial_frame<R>(
1218 stream: &mut R,
1219 deadline: tokio::time::Instant,
1220) -> std::io::Result<Vec<u8>>
1221where
1222 R: tokio::io::AsyncRead + Unpin,
1223{
1224 if tokio::time::Instant::now() >= deadline {
1228 return Err(initial_frame_timeout_error());
1229 }
1230 let raw = tokio::time::timeout_at(deadline, read_frame(stream))
1231 .await
1232 .map_err(|_| initial_frame_timeout_error())??;
1233 if tokio::time::Instant::now() >= deadline {
1234 return Err(initial_frame_timeout_error());
1235 }
1236 Ok(raw)
1237}
1238
1239#[cfg(unix)]
1241pub async fn write_frame<W>(stream: &mut W, payload: &[u8]) -> std::io::Result<()>
1242where
1243 W: tokio::io::AsyncWrite + Unpin,
1244{
1245 if payload.len() > MAX_FRAME_BYTES {
1246 return Err(std::io::Error::new(
1247 std::io::ErrorKind::InvalidData,
1248 format!(
1249 "daemon frame of {} bytes exceeds {MAX_FRAME_BYTES} cap",
1250 payload.len()
1251 ),
1252 ));
1253 }
1254 let len = (payload.len() as u32).to_be_bytes();
1255 stream.write_all(&len).await?;
1256 stream.write_all(payload).await?;
1257 stream.flush().await?;
1258 Ok(())
1259}
1260
1261#[cfg(unix)]
1270#[async_trait]
1271pub trait DaemonDispatch: Clone + Send + Sync + 'static {
1272 fn idle_retirement_blockers(&self) -> Vec<String> {
1275 vec!["dispatcher_resource_inventory_unknown".to_owned()]
1276 }
1277
1278 fn plan(&self, ops: &str) -> String;
1280
1281 #[allow(clippy::too_many_arguments)]
1296 async fn dispatch(
1297 &self,
1298 ops: String,
1299 presentation: Option<String>,
1300 presentation_per_op: Option<Vec<Option<String>>>,
1301 format: Option<String>,
1302 format_per_op: Option<Vec<Option<String>>>,
1303 from_wire: bool,
1304 identity: Option<RequestIdentity>,
1305 ) -> Result<String, String>;
1306
1307 fn request_read_timeout(&self, _ops: &str) -> std::time::Duration {
1311 khive_storage::request_read_timeout_from_env()
1312 }
1313
1314 #[allow(clippy::too_many_arguments)]
1316 async fn dispatch_with_error_detail(
1317 &self,
1318 ops: String,
1319 presentation: Option<String>,
1320 presentation_per_op: Option<Vec<Option<String>>>,
1321 format: Option<String>,
1322 format_per_op: Option<Vec<Option<String>>>,
1323 from_wire: bool,
1324 identity: Option<RequestIdentity>,
1325 ) -> Result<String, DaemonDispatchError> {
1326 self.dispatch(
1327 ops,
1328 presentation,
1329 presentation_per_op,
1330 format,
1331 format_per_op,
1332 from_wire,
1333 identity,
1334 )
1335 .await
1336 .map_err(|message| DaemonDispatchError::new(message, None))
1337 }
1338
1339 async fn warm_all(&self);
1341
1342 fn namespace(&self) -> &str;
1344
1345 fn config_id(&self) -> &str;
1350
1351 fn pool_for_checkpoint(&self) -> Option<Arc<ConnectionPool>> {
1359 None
1360 }
1361
1362 fn secondary_pools_for_checkpoint(&self) -> Vec<Arc<ConnectionPool>> {
1372 Vec::new()
1373 }
1374
1375 fn event_store_for_checkpoint(&self) -> Option<Arc<dyn khive_storage::EventStore>> {
1385 None
1386 }
1387}
1388
1389#[cfg(unix)]
1390struct CheckpointTaskSpec {
1391 pool: Arc<ConnectionPool>,
1392 lifecycle_owner: Option<CheckpointLifecycleOwner>,
1393 is_main: bool,
1394}
1395
1396#[cfg(unix)]
1403fn checkpoint_task_specs(
1404 main_pool: Option<Arc<ConnectionPool>>,
1405 secondary_pools: Vec<Arc<ConnectionPool>>,
1406 event_store: Option<Arc<dyn khive_storage::EventStore>>,
1407 namespace: String,
1408) -> Vec<CheckpointTaskSpec> {
1409 let mut tasks = Vec::with_capacity(usize::from(main_pool.is_some()) + secondary_pools.len());
1410 if let Some(pool) = main_pool {
1411 tasks.push(CheckpointTaskSpec {
1412 pool,
1413 lifecycle_owner: None,
1414 is_main: true,
1415 });
1416 }
1417 tasks.extend(secondary_pools.into_iter().map(|pool| CheckpointTaskSpec {
1418 pool,
1419 lifecycle_owner: None,
1420 is_main: false,
1421 }));
1422
1423 if let (Some(task), Some(event_store)) = (tasks.first_mut(), event_store) {
1424 task.lifecycle_owner = Some(CheckpointLifecycleOwner::new(event_store, namespace));
1425 }
1426 tasks
1427}
1428
1429static WARM_INDEX_HOST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1443
1444pub fn mark_warm_index_host() {
1448 WARM_INDEX_HOST.store(true, std::sync::atomic::Ordering::Release);
1449}
1450
1451pub fn is_warm_index_host() -> bool {
1460 WARM_INDEX_HOST.load(std::sync::atomic::Ordering::Acquire)
1461}
1462
1463static BACKGROUND_TASKS: std::sync::OnceLock<Arc<std::sync::atomic::AtomicUsize>> =
1464 std::sync::OnceLock::new();
1465
1466fn background_tasks() -> &'static Arc<std::sync::atomic::AtomicUsize> {
1467 BACKGROUND_TASKS.get_or_init(|| Arc::new(std::sync::atomic::AtomicUsize::new(0)))
1468}
1469
1470static BACKGROUND_TASK_NAMES: std::sync::OnceLock<
1486 std::sync::Mutex<std::collections::HashMap<&'static str, usize>>,
1487> = std::sync::OnceLock::new();
1488
1489fn background_task_names_registry(
1490) -> &'static std::sync::Mutex<std::collections::HashMap<&'static str, usize>> {
1491 BACKGROUND_TASK_NAMES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
1492}
1493
1494pub const UNNAMED_BACKGROUND_TASK: &str = "unnamed";
1497
1498fn register_background_task_name(name: &'static str) {
1499 let mut names = background_task_names_registry()
1500 .lock()
1501 .unwrap_or_else(std::sync::PoisonError::into_inner);
1502 *names.entry(name).or_insert(0) += 1;
1503}
1504
1505fn release_background_task_name(name: &'static str) {
1506 let mut names = background_task_names_registry()
1507 .lock()
1508 .unwrap_or_else(std::sync::PoisonError::into_inner);
1509 if let Some(count) = names.get_mut(name) {
1510 *count -= 1;
1511 if *count == 0 {
1512 names.remove(name);
1513 }
1514 }
1515}
1516
1517pub fn background_task_names() -> Vec<String> {
1521 let names = background_task_names_registry()
1522 .lock()
1523 .unwrap_or_else(std::sync::PoisonError::into_inner);
1524 let mut out: Vec<String> = names.keys().map(|name| (*name).to_string()).collect();
1525 out.sort();
1526 out
1527}
1528
1529#[cfg(unix)]
1530fn idle_retirement_blockers<D: DaemonDispatch>(dispatcher: &D) -> Vec<String> {
1531 let mut blockers = dispatcher.idle_retirement_blockers();
1532 if !khive_storage::tx_registry::snapshot().is_empty() {
1533 blockers.push("open_sql_transaction".to_owned());
1534 }
1535 blockers.extend(
1536 active_phase_names()
1537 .into_iter()
1538 .map(|name| format!("active_phase:{name}")),
1539 );
1540 let count = background_task_count();
1541 let names = background_task_names_registry()
1542 .lock()
1543 .unwrap_or_else(std::sync::PoisonError::into_inner);
1544 if names.values().sum::<usize>() != count {
1545 blockers.push("tracked_worker_inventory_unsettled".to_owned());
1546 }
1547 for name in names.keys() {
1550 if !matches!(
1551 *name,
1552 "wal_checkpoint" | "memory_ann_rotation_watch" | "knowledge_ann_rotation_watch"
1553 ) {
1554 blockers.push(format!("unsettled_worker:{name}"));
1555 }
1556 }
1557 blockers.sort();
1558 blockers.dedup();
1559 blockers
1560}
1561
1562#[cfg(unix)]
1563async fn wait_for_idle<D: DaemonDispatch>(dispatcher: &D, lifecycle: &DaemonLifecycle) {
1564 if lifecycle.options.lifetime == DaemonLifetime::Persistent {
1565 std::future::pending::<()>().await;
1566 }
1567 loop {
1568 if lifecycle.try_idle(|| idle_retirement_blockers(dispatcher)) {
1569 return;
1570 }
1571 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1572 }
1573}
1574
1575struct BackgroundTaskGuard {
1576 counter: Arc<std::sync::atomic::AtomicUsize>,
1577 name: &'static str,
1578}
1579
1580impl Drop for BackgroundTaskGuard {
1581 fn drop(&mut self) {
1582 release_background_task_name(self.name);
1583 self.counter
1584 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
1585 }
1586}
1587
1588pub fn spawn_tracked_task<F, T>(fut: F) -> tokio::task::JoinHandle<T>
1594where
1595 F: std::future::Future<Output = T> + Send + 'static,
1596 T: Send + 'static,
1597{
1598 spawn_named_tracked_task(UNNAMED_BACKGROUND_TASK, fut)
1599}
1600
1601pub fn spawn_named_tracked_task<F, T>(name: &'static str, fut: F) -> tokio::task::JoinHandle<T>
1608where
1609 F: std::future::Future<Output = T> + Send + 'static,
1610 T: Send + 'static,
1611{
1612 background_tasks().fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1613 register_background_task_name(name);
1614 let guard = BackgroundTaskGuard {
1615 counter: background_tasks().clone(),
1616 name,
1617 };
1618 tokio::spawn(async move {
1619 let _guard = guard;
1620 fut.await
1621 })
1622}
1623
1624pub fn track_background_task<F>(fut: F)
1629where
1630 F: std::future::Future<Output = ()> + Send + 'static,
1631{
1632 track_named_background_task(UNNAMED_BACKGROUND_TASK, fut);
1633}
1634
1635pub fn track_named_background_task<F>(name: &'static str, fut: F)
1638where
1639 F: std::future::Future<Output = ()> + Send + 'static,
1640{
1641 drop(spawn_named_tracked_task(name, fut));
1642}
1643
1644pub fn background_task_count() -> usize {
1647 background_tasks().load(std::sync::atomic::Ordering::SeqCst)
1648}
1649
1650pub fn daemon_shutdown_token() -> tokio_util::sync::CancellationToken {
1661 static TOKEN: std::sync::OnceLock<tokio_util::sync::CancellationToken> =
1662 std::sync::OnceLock::new();
1663 TOKEN
1664 .get_or_init(tokio_util::sync::CancellationToken::new)
1665 .clone()
1666}
1667
1668static ACTIVE_PHASES: std::sync::OnceLock<
1679 std::sync::Mutex<std::collections::HashMap<String, usize>>,
1680> = std::sync::OnceLock::new();
1681
1682fn active_phases() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>> {
1683 ACTIVE_PHASES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
1684}
1685
1686pub struct PhaseGuard {
1692 name: String,
1693}
1694
1695impl Drop for PhaseGuard {
1696 fn drop(&mut self) {
1697 let mut map = active_phases()
1698 .lock()
1699 .unwrap_or_else(std::sync::PoisonError::into_inner);
1700 if let Some(count) = map.get_mut(&self.name) {
1701 *count -= 1;
1702 if *count == 0 {
1703 map.remove(&self.name);
1704 }
1705 }
1706 }
1707}
1708
1709pub fn register_active_phase(name: &str) -> PhaseGuard {
1714 let mut map = active_phases()
1715 .lock()
1716 .unwrap_or_else(std::sync::PoisonError::into_inner);
1717 *map.entry(name.to_string()).or_insert(0) += 1;
1718 PhaseGuard {
1719 name: name.to_string(),
1720 }
1721}
1722
1723pub fn active_phase_names() -> Vec<String> {
1726 let map = active_phases()
1727 .lock()
1728 .unwrap_or_else(std::sync::PoisonError::into_inner);
1729 let mut names: Vec<String> = map.keys().cloned().collect();
1730 names.sort();
1731 names
1732}
1733
1734#[cfg(unix)]
1742fn build_metrics_snapshot<D: DaemonDispatch>(dispatcher: &D) -> MetricsSnapshot {
1743 let open_tx_count = khive_storage::tx_registry::snapshot().len();
1744 let (oldest_pinned_tx_micros, oldest_pinned_tx_label) =
1751 match khive_storage::tx_registry::oldest() {
1752 Some((_id, age, label)) => (Some(age.as_micros() as u64), label),
1753 None => (None, None),
1754 };
1755
1756 let checkpoint_pool = dispatcher.pool_for_checkpoint();
1757 let mut secondary_index = 0;
1758 let wal_checkpoint_stores = checkpoint_task_specs(
1759 checkpoint_pool.clone(),
1760 dispatcher.secondary_pools_for_checkpoint(),
1761 None,
1762 String::new(),
1763 )
1764 .into_iter()
1765 .map(|task| {
1766 let (store_id, role) = if task.is_main {
1767 ("main".to_string(), "main".to_string())
1768 } else {
1769 let store_id = format!("secondary:{secondary_index}");
1770 secondary_index += 1;
1771 (store_id, "secondary".to_string())
1772 };
1773 CheckpointStoreMetrics {
1774 store_id,
1775 role,
1776 database: task
1777 .pool
1778 .canonical_path()
1779 .and_then(std::path::Path::file_name)
1780 .map(|name| name.to_string_lossy().into_owned()),
1781 timing: khive_db::checkpoint::checkpoint_timing(&task.pool),
1782 }
1783 })
1784 .collect();
1785 let routine_wal = checkpoint_pool
1786 .as_deref()
1787 .and_then(khive_db::checkpoint::routine_wal_observation);
1788 let writer_stages = checkpoint_pool
1789 .as_deref()
1790 .and_then(khive_db::writer_task::last_writer_stage_observation);
1791 let (write_queue_depth, write_queue_capacity) = checkpoint_pool
1792 .as_ref()
1793 .and_then(|pool| pool.writer_task_handle().ok().flatten())
1794 .map(|handle| (Some(handle.queue_depth()), Some(handle.capacity())))
1795 .unwrap_or((None, None));
1796
1797 MetricsSnapshot {
1798 lifecycle: None,
1799 wal_pages: routine_wal.as_ref().map(|sample| sample.log_frames),
1800 wal_log_frames: routine_wal.as_ref().map(|sample| sample.log_frames),
1801 wal_checkpointed_frames: routine_wal
1802 .as_ref()
1803 .map(|sample| sample.checkpointed_frames),
1804 wal_pending_frames: routine_wal.as_ref().map(|sample| sample.pending_frames),
1805 wal_physical_bytes: routine_wal
1806 .as_ref()
1807 .and_then(|sample| sample.physical_wal_bytes),
1808 wal_observed_at_unix_ms: routine_wal
1809 .as_ref()
1810 .map(|sample| sample.observed_at_unix_ms),
1811 wal_checkpoint_stores,
1812 wal_truncate_attempts: khive_db::checkpoint::truncate_attempts(),
1813 wal_truncate_consecutive_failures: khive_db::checkpoint::truncate_consecutive_failures(),
1814 wal_checkpoint_skipped_ticks: khive_db::checkpoint::checkpoint_skipped_ticks(),
1815 wal_checkpoint_consecutive_skips: khive_db::checkpoint::checkpoint_consecutive_skips(),
1816 wal_checkpoint_last_skip_wal_pages: khive_db::checkpoint::checkpoint_last_skip_wal_pages(),
1817 oldest_pinned_tx_micros,
1818 oldest_pinned_tx_label,
1819 open_tx_count,
1820 write_queue_depth,
1821 write_queue_capacity,
1822 write_last_queue_wait_micros: writer_stages
1823 .as_ref()
1824 .map(|sample| sample.queue_wait_micros),
1825 write_last_transaction_acquire_micros: writer_stages
1826 .as_ref()
1827 .map(|sample| sample.transaction_acquire_micros),
1828 write_last_body_micros: writer_stages.as_ref().map(|sample| sample.body_micros),
1829 write_last_commit_micros: writer_stages.as_ref().map(|sample| sample.commit_micros),
1830 write_last_total_micros: writer_stages.as_ref().map(|sample| sample.total_micros),
1831 write_last_observed_at_unix_ms: writer_stages
1832 .as_ref()
1833 .map(|sample| sample.observed_at_unix_ms),
1834 connections: None,
1835 recall_ledger: Some(recall_ledger_snapshot()),
1836 }
1837}
1838
1839#[cfg(unix)]
1840async fn write_response_frame<W>(stream: &mut W, payload: &[u8]) -> std::io::Result<()>
1841where
1842 W: tokio::io::AsyncWrite + Unpin,
1843{
1844 tokio::time::timeout(INITIAL_FRAME_READ_TIMEOUT, write_frame(stream, payload))
1845 .await
1846 .map_err(|_| {
1847 std::io::Error::new(
1848 std::io::ErrorKind::TimedOut,
1849 "daemon response write timed out",
1850 )
1851 })?
1852}
1853
1854#[cfg(unix)]
1855async fn wait_for_peer_disconnect(read: &mut tokio::net::unix::OwnedReadHalf) {
1856 let mut byte = [0u8; 1];
1857 let _ = read.read(&mut byte).await;
1861}
1862
1863#[cfg(all(unix, test))]
1864async fn handle_conn<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
1865 handle_conn_with_shutdown(
1866 stream,
1867 dispatcher,
1868 None,
1869 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
1870 )
1871 .await;
1872}
1873
1874#[cfg(all(unix, feature = "fault-injection"))]
1875#[doc(hidden)]
1876pub async fn handle_conn_for_test<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
1877 handle_conn_with_shutdown(
1878 stream,
1879 dispatcher,
1880 None,
1881 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
1882 )
1883 .await;
1884}
1885
1886#[cfg(unix)]
1887fn plan_frame_companion(raw: &[u8]) -> Option<&'static str> {
1888 let value: serde_json::Value = serde_json::from_slice(raw).ok()?;
1889 if value.get("plan").and_then(serde_json::Value::as_bool) != Some(true)
1890 || value
1891 .get("protocol_version")
1892 .and_then(serde_json::Value::as_u64)
1893 != Some(u64::from(PROTOCOL_VERSION))
1894 {
1895 return None;
1896 }
1897 [
1898 "presentation",
1899 "presentation_per_op",
1900 "format",
1901 "format_per_op",
1902 "request_id",
1903 ]
1904 .into_iter()
1905 .find(|field| value.get(*field).is_some())
1906}
1907
1908#[cfg(all(
1909 unix,
1910 any(test, feature = "fault-injection", feature = "test-internals")
1911))]
1912async fn handle_conn_with_shutdown<D: DaemonDispatch>(
1913 stream: UnixStream,
1914 dispatcher: D,
1915 shutdown: Option<tokio::sync::watch::Receiver<bool>>,
1916 initial_frame_deadline: tokio::time::Instant,
1917) {
1918 handle_conn_with_lifecycle(stream, dispatcher, shutdown, initial_frame_deadline, None).await;
1919}
1920
1921#[cfg(unix)]
1922async fn handle_conn_with_lifecycle<D: DaemonDispatch>(
1923 mut stream: UnixStream,
1924 dispatcher: D,
1925 shutdown: Option<tokio::sync::watch::Receiver<bool>>,
1926 initial_frame_deadline: tokio::time::Instant,
1927 lifecycle: Option<Arc<DaemonLifecycle>>,
1928) {
1929 let mut ordinary_admission = None;
1930 let production_shutdown = shutdown.is_some();
1931 let handover_peer_allowed = peer_uid(&stream)
1935 .ok()
1936 .is_some_and(|uid| uid == unsafe { libc::geteuid() } as u32);
1937 let (local_shutdown_tx, local_shutdown_rx) = tokio::sync::watch::channel(false);
1938 let shutdown = shutdown.unwrap_or(local_shutdown_rx);
1939 let _local_shutdown_tx = local_shutdown_tx;
1942 let raw = match read_initial_frame(&mut stream, initial_frame_deadline).await {
1943 Ok(r) => r,
1944 Err(e) => {
1945 tracing::debug!(error = %e, "failed to read daemon request frame");
1946 return;
1947 }
1948 };
1949 #[derive(Deserialize)]
1950 struct SupervisorRequestEnvelope {
1951 #[serde(flatten)]
1952 frame: DaemonRequestFrame,
1953 #[serde(default)]
1954 supervisor_handover: bool,
1955 }
1956 let decoded: Result<SupervisorRequestEnvelope, _> = serde_json::from_slice(&raw);
1957 if decoded.as_ref().ok().is_none_or(|item| item.frame.plan) {
1958 if let Some(field) = plan_frame_companion(&raw) {
1959 let response = DaemonResponseFrame {
1960 ok: false,
1961 result: None,
1962 error: Some(format!(
1963 "invalid_params: plan=true cannot be combined with {field}"
1964 )),
1965 error_detail: Some(serde_json::json!({
1966 "kind": "protocol",
1967 "code": "invalid_params",
1968 "message": format!("plan=true cannot be combined with {field}"),
1969 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
1970 })),
1971 namespace_mismatch: false,
1972 config_mismatch: false,
1973 served_config_id: Some(dispatcher.config_id().to_string()),
1974 version_mismatch: false,
1975 daemon_protocol_version: PROTOCOL_VERSION,
1976 metrics: None,
1977 request_id: None,
1978 };
1979 if let Ok(payload) = serde_json::to_vec(&response) {
1980 if let Err(error) = write_response_frame(&mut stream, &payload).await {
1981 tracing::debug!(%error, "failed to write plan envelope refusal");
1982 }
1983 }
1984 return;
1985 }
1986 }
1987 let (frame, handover_requested) = match decoded {
1988 Ok(item) => (item.frame, item.supervisor_handover),
1989 Err(e) => {
1990 tracing::debug!(error = %e, "failed to decode daemon request frame");
1991 return;
1992 }
1993 };
1994 let supervisor_probe = frame.probe_only;
1995 let handover_accepted = handover_requested
1996 && supervisor_probe
1997 && production_shutdown
1998 && handover_peer_allowed
1999 && frame.protocol_version == PROTOCOL_VERSION
2000 && !frame.plan
2001 && !frame.metrics_only
2002 && frame.ops.is_empty()
2003 && read_supervisor_marker_claim().is_some_and(|(pid, _)| pid != std::process::id());
2004 let (mut peer_read, mut peer_write) = stream.into_split();
2005
2006 let served_config_id = Some(dispatcher.config_id().to_string());
2007 let resp = if frame.protocol_version != PROTOCOL_VERSION {
2008 let msg = format!(
2009 "daemon protocol mismatch: client={} daemon={} — \
2010 rebuild/update the client binary (make local)",
2011 frame.protocol_version, PROTOCOL_VERSION,
2012 );
2013 tracing::warn!(
2014 client_version = frame.protocol_version,
2015 daemon_version = PROTOCOL_VERSION,
2016 "daemon protocol version mismatch"
2017 );
2018 DaemonResponseFrame {
2019 ok: false,
2020 result: None,
2021 error: Some(msg.clone()),
2022 error_detail: Some(serde_json::json!({
2023 "kind": "protocol",
2024 "code": "version_mismatch",
2025 "message": msg,
2026 "domain_disposition": crate::DomainDisposition::Unknown.as_str(),
2027 })),
2028 namespace_mismatch: false,
2029 config_mismatch: false,
2030 served_config_id,
2031 version_mismatch: frame.protocol_version > PROTOCOL_VERSION,
2046 daemon_protocol_version: PROTOCOL_VERSION,
2047 metrics: None,
2048 request_id: frame.request_id,
2049 }
2050 } else if handover_requested {
2051 if handover_accepted {
2052 DaemonResponseFrame {
2053 ok: true,
2054 result: None,
2055 error: None,
2056 error_detail: None,
2057 namespace_mismatch: false,
2058 config_mismatch: false,
2059 served_config_id,
2060 version_mismatch: false,
2061 daemon_protocol_version: PROTOCOL_VERSION,
2062 metrics: None,
2063 request_id: frame.request_id,
2064 }
2065 } else {
2066 DaemonResponseFrame {
2067 ok: false,
2068 result: None,
2069 error: Some("supervisor handover refused".to_string()),
2070 error_detail: Some(serde_json::json!({
2071 "kind": "protocol",
2072 "code": "supervisor_handover_refused",
2073 "message": "supervisor handover refused",
2074 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
2075 })),
2076 namespace_mismatch: false,
2077 config_mismatch: false,
2078 served_config_id,
2079 version_mismatch: false,
2080 daemon_protocol_version: PROTOCOL_VERSION,
2081 metrics: None,
2082 request_id: frame.request_id,
2083 }
2084 }
2085 } else if frame.metrics_only && !frame.plan {
2086 DaemonResponseFrame {
2093 ok: true,
2094 result: None,
2095 error: None,
2096 error_detail: None,
2097 namespace_mismatch: false,
2098 config_mismatch: false,
2099 served_config_id,
2100 version_mismatch: false,
2101 daemon_protocol_version: PROTOCOL_VERSION,
2102 metrics: Some({
2103 let mut metrics = build_metrics_snapshot(&dispatcher);
2104 metrics.lifecycle = lifecycle.as_ref().map(|state| {
2105 let mut snapshot = state.snapshot();
2106 snapshot.idle_blockers = idle_retirement_blockers(&dispatcher);
2107 snapshot
2108 });
2109 metrics.connections = lifecycle.as_ref().map(|state| state.connections.snapshot());
2110 metrics
2111 }),
2112 request_id: frame.request_id,
2113 }
2114 } else if !config_ids_compatible(&frame.config_id, dispatcher.config_id()) {
2123 DaemonResponseFrame {
2124 ok: false,
2125 result: None,
2126 error: None,
2127 error_detail: Some(serde_json::json!({
2128 "kind": "protocol",
2129 "code": "config_mismatch",
2130 "message": "daemon configuration does not match the request",
2131 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
2132 })),
2133 namespace_mismatch: false,
2134 config_mismatch: true,
2135 served_config_id,
2136 version_mismatch: false,
2137 daemon_protocol_version: PROTOCOL_VERSION,
2138 metrics: None,
2139 request_id: frame.request_id,
2140 }
2141 } else if frame.plan {
2142 DaemonResponseFrame {
2143 ok: true,
2144 result: Some(dispatcher.plan(&frame.ops)),
2145 error: None,
2146 error_detail: None,
2147 namespace_mismatch: false,
2148 config_mismatch: false,
2149 served_config_id,
2150 version_mismatch: false,
2151 daemon_protocol_version: PROTOCOL_VERSION,
2152 metrics: None,
2153 request_id: None,
2154 }
2155 } else if frame.probe_only {
2156 DaemonResponseFrame {
2160 ok: true,
2161 result: None,
2162 error: None,
2163 error_detail: None,
2164 namespace_mismatch: false,
2165 config_mismatch: false,
2166 served_config_id,
2167 version_mismatch: false,
2168 daemon_protocol_version: PROTOCOL_VERSION,
2169 metrics: None,
2170 request_id: frame.request_id,
2171 }
2172 } else {
2173 if let Some(lifecycle) = &lifecycle {
2174 ordinary_admission = lifecycle.admit();
2175 if ordinary_admission.is_none() {
2176 let refusal = DaemonResponseFrame {
2177 ok: false,
2178 result: None,
2179 error: Some("daemon is draining; request was not admitted".to_owned()),
2180 error_detail: Some(serde_json::json!({
2181 "kind": "runtime", "code": "daemon_draining",
2182 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
2183 })),
2184 request_id: frame.request_id,
2185 daemon_protocol_version: PROTOCOL_VERSION,
2186 namespace_mismatch: false,
2187 config_mismatch: false,
2188 served_config_id: Some(dispatcher.config_id().to_owned()),
2189 version_mismatch: false,
2190 metrics: None,
2191 };
2192 if let Ok(payload) = serde_json::to_vec(&refusal) {
2193 let _ = write_response_frame(&mut peer_write, &payload).await;
2194 }
2195 return;
2196 }
2197 }
2198 let identity = RequestIdentity {
2210 namespace: frame.namespace.clone(),
2211 actor_id: frame.actor_id.clone(),
2212 visible_namespaces: frame.visible_namespaces.clone(),
2213 process_ref: frame.process_ref.clone(),
2214 request_id: frame.request_id,
2215 };
2216 tracing::debug!(
2217 request_id = frame.request_id,
2218 "daemon RequestIdentity constructed"
2219 );
2220 let (read_cancel_tx, read_cancel_rx) = tokio::sync::watch::channel(false);
2221 let read_timeout = dispatcher.request_read_timeout(&frame.ops);
2225 let excluded_embedder_names =
2226 config_id_extra_embedder_exclusions(&frame.config_id, dispatcher.config_id())
2227 .expect("compatible configuration ids must expose their extra embedder sets");
2228 let dispatch = crate::runtime::scope_request_embedder_exclusions(
2229 excluded_embedder_names,
2230 khive_storage::scope_request_read_cancellation(
2231 shutdown,
2232 khive_storage::scope_request_read_cancellation(
2233 read_cancel_rx,
2234 khive_storage::scope_request_read_deadline(
2235 read_timeout,
2236 dispatcher.dispatch_with_error_detail(
2237 frame.ops,
2238 frame.presentation,
2239 frame.presentation_per_op,
2240 frame.format,
2241 frame.format_per_op,
2242 frame.from_wire,
2243 Some(identity),
2244 ),
2245 ),
2246 ),
2247 ),
2248 );
2249 tokio::pin!(dispatch);
2250 let dispatch_result = tokio::select! {
2251 result = &mut dispatch => result,
2252 _ = wait_for_peer_disconnect(&mut peer_read) => {
2253 let _ = read_cancel_tx.send(true);
2254 dispatch.await
2255 }
2256 };
2257 match dispatch_result {
2258 Ok(result) => {
2259 let (result, detail) = take_daemon_lexical_timeout_marker(result);
2260 DaemonResponseFrame {
2261 ok: true,
2262 result: Some(result),
2263 error: None,
2264 error_detail: detail,
2265 namespace_mismatch: false,
2266 config_mismatch: false,
2267 served_config_id,
2268 version_mismatch: false,
2269 daemon_protocol_version: PROTOCOL_VERSION,
2270 metrics: None,
2271 request_id: frame.request_id,
2272 }
2273 }
2274 Err(error) => {
2275 let error = DaemonDispatchError::new(error.message, Some(error.error_detail));
2276 DaemonResponseFrame {
2277 ok: false,
2278 result: None,
2279 error: Some(error.message),
2280 error_detail: Some(error.error_detail),
2281 namespace_mismatch: false,
2282 config_mismatch: false,
2283 served_config_id,
2284 version_mismatch: false,
2285 daemon_protocol_version: PROTOCOL_VERSION,
2286 metrics: None,
2287 request_id: frame.request_id,
2288 }
2289 }
2290 }
2291 };
2292
2293 let payload = if supervisor_probe {
2294 serde_json::to_value(&resp).and_then(|mut value| {
2295 if let Some(claim) = current_supervisor_claim() {
2296 value["supervisor_claim"] = serde_json::Value::String(claim);
2297 }
2298 if handover_accepted {
2299 value["supervisor_handover_accepted"] = serde_json::Value::Bool(true);
2300 }
2301 serde_json::to_vec(&value)
2302 })
2303 } else {
2304 serde_json::to_vec(&resp)
2305 };
2306 let mut handover_ack_written = false;
2307 match payload {
2308 Ok(payload) => {
2309 if payload.len() > MAX_FRAME_BYTES {
2310 tracing::warn!(
2317 bytes = payload.len(),
2318 limit = MAX_FRAME_BYTES,
2319 "daemon response exceeds MAX_FRAME_BYTES; sending explicit error frame"
2320 );
2321 let message = format!(
2322 "response too large: {} bytes exceeds {} byte IPC cap",
2323 payload.len(),
2324 MAX_FRAME_BYTES,
2325 );
2326 let err_resp = DaemonResponseFrame {
2328 ok: false,
2329 result: None,
2330 error: Some(message.clone()),
2331 error_detail: Some(serde_json::json!({
2332 "kind": "transport",
2333 "code": "response_frame_size_limit",
2334 "message": message,
2335 "domain_disposition": crate::DomainDisposition::Unknown.as_str(),
2336 })),
2337 namespace_mismatch: false,
2338 config_mismatch: false,
2339 served_config_id: resp.served_config_id,
2340 version_mismatch: false,
2341 daemon_protocol_version: PROTOCOL_VERSION,
2342 metrics: None,
2343 request_id: resp.request_id,
2344 };
2345 if let Ok(err_payload) = serde_json::to_vec(&err_resp) {
2346 if let Err(e) = write_response_frame(&mut peer_write, &err_payload).await {
2347 tracing::debug!(error = %e, "failed to write oversized-response error frame");
2348 }
2349 }
2350 } else {
2351 match write_response_frame(&mut peer_write, &payload).await {
2352 Ok(()) => handover_ack_written = true,
2353 Err(e) => tracing::debug!(error = %e, "failed to write daemon response frame"),
2354 }
2355 }
2356 }
2357 Err(e) => tracing::warn!(error = %e, "failed to serialize daemon response frame"),
2358 }
2359 if handover_accepted && handover_ack_written {
2360 if unsafe { libc::raise(libc::SIGTERM) } != 0 {
2363 tracing::error!(error = %std::io::Error::last_os_error(), "self-directed handover signal failed");
2364 }
2365 }
2366 drop(ordinary_admission);
2367}
2368
2369#[cfg(unix)]
2373struct ActiveConnectionGuard {
2374 active: Arc<std::sync::atomic::AtomicUsize>,
2375}
2376
2377#[cfg(unix)]
2378impl ActiveConnectionGuard {
2379 fn claim(active: Arc<std::sync::atomic::AtomicUsize>) -> Self {
2380 active.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2381 Self { active }
2382 }
2383}
2384
2385#[cfg(unix)]
2386impl Drop for ActiveConnectionGuard {
2387 fn drop(&mut self) {
2388 self.active
2389 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
2390 }
2391}
2392
2393#[cfg(unix)]
2394fn spawn_connection_task<F>(
2395 active: Arc<std::sync::atomic::AtomicUsize>,
2396 future: F,
2397) -> tokio::task::JoinHandle<()>
2398where
2399 F: std::future::Future<Output = ()> + Send + 'static,
2400{
2401 let guard = ActiveConnectionGuard::claim(active);
2402 tokio::spawn(async move {
2403 let _guard = guard;
2404 future.await;
2405 })
2406}
2407
2408#[cfg(unix)]
2419pub async fn run_daemon<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
2420 let boot_guard = Some(acquire_daemon_boot_guard()?);
2421 run_daemon_with_boot_guard_inner(
2422 dispatcher,
2423 boot_guard,
2424 false,
2425 DaemonOptions::default(),
2426 |_| DaemonStartupReport::default(),
2427 )
2428 .await
2429}
2430
2431#[cfg(all(unix, any(test, feature = "fault-injection")))]
2446#[doc(hidden)]
2447pub async fn run_daemon_in_process_test<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
2448 let boot_guard = Some(acquire_daemon_boot_guard()?);
2449 run_daemon_with_boot_guard_inner(
2450 dispatcher,
2451 boot_guard,
2452 true,
2453 DaemonOptions::default(),
2454 |_| DaemonStartupReport::default(),
2455 )
2456 .await
2457}
2458
2459#[cfg(unix)]
2460#[derive(Clone, Copy, PartialEq, Eq)]
2461enum RendezvousPathRole {
2462 Socket,
2463 PidFile,
2464}
2465
2466#[cfg(unix)]
2467impl RendezvousPathRole {
2468 fn env_name(self) -> &'static str {
2469 match self {
2470 Self::Socket => SOCKET_PATH_ENV,
2471 Self::PidFile => PID_PATH_ENV,
2472 }
2473 }
2474
2475 fn directory_name(self) -> &'static str {
2476 match self {
2477 Self::Socket => "socket directory",
2478 Self::PidFile => "PID-file directory",
2479 }
2480 }
2481
2482 fn path_name(self) -> &'static str {
2483 match self {
2484 Self::Socket => "socket path",
2485 Self::PidFile => "PID-file path",
2486 }
2487 }
2488
2489 fn path_component_name(self) -> &'static str {
2490 match self {
2491 Self::Socket => "socket-path",
2492 Self::PidFile => "PID-file-path",
2493 }
2494 }
2495}
2496
2497#[cfg(unix)]
2520pub(crate) fn ensure_socket_dir_is_trusted(parent: &std::path::Path) -> anyhow::Result<()> {
2521 let daemon_euid = unsafe { libc::geteuid() } as u32;
2523 ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::Socket, daemon_euid, true)
2524}
2525
2526#[cfg(unix)]
2531pub fn ensure_pid_file_dir_is_trusted(pid_file: &std::path::Path) -> anyhow::Result<()> {
2532 let parent = pid_file
2533 .parent()
2534 .filter(|parent| !parent.as_os_str().is_empty())
2535 .unwrap_or_else(|| std::path::Path::new("."));
2536 let daemon_euid = unsafe { libc::geteuid() } as u32;
2538 ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::PidFile, daemon_euid, false)
2539}
2540
2541#[cfg(unix)]
2542fn ensure_rendezvous_dir_is_trusted(
2543 parent: &std::path::Path,
2544 role: RendezvousPathRole,
2545 daemon_euid: u32,
2546 repair_owned_default: bool,
2547) -> anyhow::Result<()> {
2548 let env_name = role.env_name();
2549 let directory_name = role.directory_name();
2550
2551 if repair_owned_default && parent == khive_dir() {
2552 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
2553 anyhow::anyhow!(
2554 "refusing to start: cannot chmod 0700 {}: {e}. The khive directory must be \
2555 owner-only as the {directory_name} for {env_name}; it is part of the \
2556 same-uid guarantee this daemon enforces.",
2557 parent.display()
2558 )
2559 })?;
2560 return ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role);
2561 }
2562
2563 let meta = std::fs::metadata(parent).map_err(|e| {
2566 anyhow::anyhow!(
2567 "refusing to start: cannot stat {directory_name} {} for {env_name}: {e}. \
2568 It gates rendezvous-path safety, and unreadable metadata is not a passing state.",
2569 parent.display()
2570 )
2571 })?;
2572
2573 use std::os::unix::fs::MetadataExt;
2574 let owner = meta.uid();
2575 if owner != daemon_euid && owner != 0 {
2576 anyhow::bail!(
2577 "refusing to start: {directory_name} {} for {env_name} is owned by uid {owner}, \
2578 not this daemon's uid ({daemon_euid}) or root. A directory owner can replace \
2579 the rendezvous path regardless of mode bits. Point {env_name} at a directory \
2580 you own, or unset it for the default.",
2581 parent.display()
2582 );
2583 }
2584
2585 let mode = meta.permissions().mode();
2586 if mode & 0o022 != 0 {
2587 anyhow::bail!(
2588 "refusing to start: {directory_name} {} for {env_name} is mode {:04o} — writable \
2589 by group or other, so another local user could replace the rendezvous path. \
2590 Use a directory only you can write, or unset {env_name} for the default. \
2591 This daemon is not changing the permissions of a directory it does not own.",
2592 parent.display(),
2593 mode & 0o7777
2594 );
2595 }
2596
2597 ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role)
2598}
2599
2600#[cfg(all(unix, test))]
2603fn ensure_socket_path_is_swap_resistant(
2604 parent: &std::path::Path,
2605 daemon_euid: u32,
2606) -> anyhow::Result<()> {
2607 ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, RendezvousPathRole::Socket)
2608}
2609
2610#[cfg(unix)]
2635fn ensure_rendezvous_path_is_swap_resistant(
2636 parent: &std::path::Path,
2637 daemon_euid: u32,
2638 role: RendezvousPathRole,
2639) -> anyhow::Result<()> {
2640 use std::os::unix::fs::MetadataExt;
2641
2642 let env_name = role.env_name();
2643 let directory_name = role.directory_name();
2644 let path_name = role.path_name();
2645 let component_name = role.path_component_name();
2646
2647 let absolute = if parent.is_absolute() {
2648 parent.to_path_buf()
2649 } else {
2650 std::env::current_dir()
2651 .map_err(|e| {
2652 anyhow::anyhow!(
2653 "refusing to start: cannot resolve the working directory to absolutize \
2654 {directory_name} {} for {env_name}: {e}.",
2655 parent.display()
2656 )
2657 })?
2658 .join(parent)
2659 };
2660
2661 fn push_components(stack: &mut Vec<std::ffi::OsString>, path: &std::path::Path) {
2662 let components: Vec<_> = path
2663 .components()
2664 .map(|c| c.as_os_str().to_os_string())
2665 .collect();
2666 stack.extend(components.into_iter().rev());
2667 }
2668
2669 let mut stack: Vec<std::ffi::OsString> = Vec::new();
2670 push_components(&mut stack, &absolute);
2671 let mut resolved = std::path::PathBuf::new();
2672 let mut symlinks_followed = 0u32;
2673
2674 while let Some(component) = stack.pop() {
2675 if component == "/" {
2676 resolved = std::path::PathBuf::from("/");
2677 continue;
2678 }
2679 if component == "." {
2680 continue;
2681 }
2682 if component == ".." {
2683 resolved.pop();
2684 continue;
2685 }
2686 let candidate = resolved.join(&component);
2687 let meta = std::fs::symlink_metadata(&candidate).map_err(|e| {
2688 anyhow::anyhow!(
2689 "refusing to start: cannot stat {component_name} component {} for {env_name}: \
2690 {e}. An unreadable component is not a passing one.",
2691 candidate.display()
2692 )
2693 })?;
2694 let owner = meta.uid();
2695
2696 if meta.file_type().is_symlink() {
2697 symlinks_followed += 1;
2698 if symlinks_followed > 40 {
2699 anyhow::bail!(
2700 "refusing to start: {path_name} for {env_name} resolves through more than \
2701 40 symlinks at {} — treating this as a loop.",
2702 candidate.display()
2703 );
2704 }
2705 if owner != daemon_euid && owner != 0 {
2706 anyhow::bail!(
2707 "refusing to start: {component_name} symlink component {} for {env_name} \
2708 is owned by uid {owner}, not this daemon's uid ({daemon_euid}) or root — \
2709 its owner could retarget it after this check and re-root the {path_name}. \
2710 Point {env_name} somewhere trusted end to end, or unset it for the default.",
2711 candidate.display()
2712 );
2713 }
2714 let target = std::fs::read_link(&candidate).map_err(|e| {
2715 anyhow::anyhow!(
2716 "refusing to start: cannot read {component_name} symlink component {} \
2717 for {env_name}: {e}.",
2718 candidate.display()
2719 )
2720 })?;
2721 push_components(&mut stack, &target);
2722 continue;
2723 }
2724
2725 if meta.is_dir() {
2726 let mode = meta.permissions().mode();
2727 let sticky = mode & 0o1000 != 0;
2728 if owner != daemon_euid && owner != 0 {
2729 anyhow::bail!(
2730 "refusing to start: {component_name} ancestor {} for {env_name} is owned by \
2731 uid {owner}, not this daemon's uid ({daemon_euid}) or root — its owner \
2732 could rename the next path component and re-root the {path_name}. Point \
2733 {env_name} somewhere trusted end to end, or unset it for the default.",
2734 candidate.display()
2735 );
2736 }
2737 if mode & 0o022 != 0 && !sticky {
2738 anyhow::bail!(
2739 "refusing to start: {component_name} ancestor {} for {env_name} is mode \
2740 {:04o} — writable by group or other without the sticky bit, so another \
2741 local user could rename the next path component and re-root the {path_name}. \
2742 Point {env_name} somewhere trusted end to end, or unset it for the default.",
2743 candidate.display(),
2744 mode & 0o7777
2745 );
2746 }
2747 resolved = candidate;
2748 continue;
2749 }
2750
2751 anyhow::bail!(
2752 "refusing to start: {component_name} component {} for {env_name} is neither a \
2753 directory nor a symlink — the {path_name} cannot traverse it.",
2754 candidate.display()
2755 );
2756 }
2757
2758 Ok(())
2759}
2760
2761#[cfg(unix)]
2774pub async fn run_daemon_with_boot_guard<D: DaemonDispatch>(
2775 dispatcher: D,
2776 boot_guard: Option<std::fs::File>,
2777) -> anyhow::Result<()> {
2778 run_daemon_with_boot_guard_inner(
2779 dispatcher,
2780 boot_guard,
2781 false,
2782 DaemonOptions::default(),
2783 |_| DaemonStartupReport::default(),
2784 )
2785 .await
2786}
2787
2788#[cfg(unix)]
2794pub async fn run_daemon_with_boot_guard_and_start<D, F>(
2795 dispatcher: D,
2796 boot_guard: Option<std::fs::File>,
2797 start: F,
2798) -> anyhow::Result<()>
2799where
2800 D: DaemonDispatch,
2801 F: FnOnce(&D) + Send,
2802{
2803 run_daemon_with_options_and_boot_guard_and_start(
2804 dispatcher,
2805 boot_guard,
2806 DaemonOptions::default(),
2807 |dispatcher| {
2808 start(dispatcher);
2809 DaemonStartupReport::default()
2810 },
2811 )
2812 .await
2813}
2814
2815#[cfg(unix)]
2817pub async fn run_daemon_with_options_and_boot_guard_and_start<D, F>(
2818 dispatcher: D,
2819 boot_guard: Option<std::fs::File>,
2820 options: DaemonOptions,
2821 start: F,
2822) -> anyhow::Result<()>
2823where
2824 D: DaemonDispatch,
2825 F: FnOnce(&D) -> DaemonStartupReport + Send,
2826{
2827 anyhow::ensure!(
2828 !options.idle_interval.is_zero(),
2829 "daemon idle interval must be positive"
2830 );
2831 run_daemon_with_boot_guard_inner(dispatcher, boot_guard, false, options, start).await
2832}
2833
2834#[cfg(unix)]
2835async fn run_daemon_with_boot_guard_inner<D, F>(
2836 dispatcher: D,
2837 boot_guard: Option<std::fs::File>,
2838 allow_same_process_incumbent: bool,
2839 options: DaemonOptions,
2840 start: F,
2841) -> anyhow::Result<()>
2842where
2843 D: DaemonDispatch,
2844 F: FnOnce(&D) -> DaemonStartupReport + Send,
2845{
2846 struct ComponentTeardown;
2851 impl Drop for ComponentTeardown {
2852 fn drop(&mut self) {
2853 daemon_shutdown_token().cancel();
2854 }
2855 }
2856 let _component_teardown = ComponentTeardown;
2857
2858 ensure_rendezvous_overrides_paired()?;
2862
2863 let sock = socket_path();
2864 let pid_file = pid_path();
2865 let socket_parent = sock.parent();
2866 let pid_parent = pid_file.parent();
2867
2868 if let Some(parent) = socket_parent {
2869 std::fs::create_dir_all(parent)?;
2870 ensure_socket_dir_is_trusted(parent)?;
2871 }
2872 if pid_parent != socket_parent {
2876 ensure_pid_file_dir_is_trusted(&pid_file)?;
2877 }
2878
2879 let _startup_lock = boot_guard;
2893
2894 match cleanup_stale_daemon(
2897 &sock,
2898 &pid_file,
2899 allow_same_process_incumbent,
2900 dispatcher.config_id(),
2901 )
2902 .await
2903 {
2904 Incumbent::Serving(incumbent_pid) => {
2905 tracing::error!(
2906 pid = incumbent_pid,
2907 socket = ?sock,
2908 "refusing to start: a khived instance is already serving this socket"
2909 );
2910 anyhow::bail!(
2911 "refusing to start: khived is already running as pid {incumbent_pid}, \
2912 serving socket {}. Stop that instance first if you intend to replace it.",
2913 sock.display()
2914 );
2915 }
2916 Incumbent::Live(incumbent_pid) => {
2917 tracing::error!(
2918 pid = incumbent_pid,
2919 socket = ?sock,
2920 "refusing to start: a live process owns the PID file but no khived answered"
2921 );
2922 anyhow::bail!(
2923 "refusing to start: pid {incumbent_pid} owns the daemon PID file and is alive, \
2924 but nothing answered the khived protocol on {}. It may be draining. Nothing \
2925 was removed; stop that process first if you intend to replace it.",
2926 sock.display()
2927 );
2928 }
2929 Incumbent::Stale => {}
2930 }
2931
2932 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
2937 let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?;
2938
2939 let pid_file_guard = match write_pid_file_exclusive(&pid_file) {
2940 Ok(guard) => guard,
2941 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
2942 if pid_file_names_a_reachable_daemon(
2945 &pid_file,
2946 &sock,
2947 allow_same_process_incumbent,
2948 dispatcher.config_id(),
2949 )
2950 .await
2951 {
2952 tracing::info!(
2953 "a replacement khived already claimed the pid/socket rendezvous; exiting"
2954 );
2955 return Ok(());
2956 }
2957 anyhow::bail!(
2958 "failed to claim daemon pid file at {pid_file:?}: it already exists \
2959 and does not name a reachable daemon"
2960 );
2961 }
2962 Err(e) => return Err(e.into()),
2963 };
2964
2965 let listener = match UnixListener::bind(&sock) {
2966 Ok(listener) => listener,
2967 Err(e) => {
2968 remove_pid_file_if_owned(&pid_file, &pid_file_guard);
2969 return Err(e.into());
2970 }
2971 };
2972 if let Err(e) = std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o600)) {
2976 drop(listener);
2977 let _ = std::fs::remove_file(&sock);
2978 remove_pid_file_if_owned(&pid_file, &pid_file_guard);
2979 return Err(anyhow::anyhow!(
2980 "refusing to start: cannot chmod 0600 {}: {e}. The daemon socket must be owner-only \
2981 — it is half of the single-principal guarantee this daemon enforces.",
2982 sock.display()
2983 ));
2984 }
2985 let bound_identity = socket_identity(&sock);
2989
2990 let lifecycle = Arc::new(DaemonLifecycle::new(options, start(&dispatcher)));
2991
2992 drop(_startup_lock);
2995 tracing::info!(
2996 socket = ?sock,
2997 pid = std::process::id(),
2998 source_revision = crate::BUILD_INFO.source_revision,
2999 build_time = crate::BUILD_INFO.build_time,
3000 "khived listening"
3001 );
3002
3003 {
3004 let warm = dispatcher.clone();
3005 track_named_background_task("daemon_warmup", async move {
3006 warm.warm_all().await;
3007 });
3008 }
3009
3010 let (checkpoint_shutdown_tx, checkpoint_shutdown_rx) = tokio::sync::watch::channel(());
3018 let checkpoint_tasks = checkpoint_task_specs(
3024 dispatcher.pool_for_checkpoint(),
3025 dispatcher.secondary_pools_for_checkpoint(),
3026 dispatcher.event_store_for_checkpoint(),
3027 dispatcher.namespace().to_string(),
3028 );
3029 if !checkpoint_tasks.is_empty() {
3030 let cfg = CheckpointConfig::from_env();
3031 let checkpoint_task_count = checkpoint_tasks.len();
3032 for task in checkpoint_tasks {
3033 track_named_background_task(
3034 "wal_checkpoint",
3035 run_checkpoint_task(
3036 task.pool,
3037 cfg.clone(),
3038 task.lifecycle_owner,
3039 checkpoint_shutdown_rx.clone(),
3040 task.is_main,
3041 ),
3042 );
3043 }
3044 tracing::info!(checkpoint_task_count, "WAL checkpoint task(s) started");
3045 }
3046
3047 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3048 let connection_tasks = Arc::new(std::sync::Mutex::new(
3049 Vec::<tokio::task::JoinHandle<()>>::new(),
3050 ));
3051 let (request_shutdown_tx, request_shutdown_rx) = tokio::sync::watch::channel(false);
3052
3053 let shutdown = async {
3054 tokio::select! {
3055 _ = sigterm.recv() => tracing::info!("received SIGTERM"),
3056 _ = sigint.recv() => tracing::info!("received SIGINT"),
3057 }
3058 for signal in [libc::SIGTERM, libc::SIGINT] {
3062 if unsafe { libc::signal(signal, libc::SIG_DFL) } == libc::SIG_ERR {
3065 return Err(std::io::Error::last_os_error());
3066 }
3067 }
3068 Ok::<(), std::io::Error>(())
3069 };
3070 tokio::pin!(shutdown);
3071 lifecycle.ready();
3072
3073 let daemon_euid = unsafe { libc::geteuid() } as u32;
3075
3076 let reason = tokio::select! {
3077 _ = async {
3078 let mut accept_error_backoff = None;
3079 let mut last_accept_error_log: Option<std::time::Instant> = None;
3080 loop {
3081 match listener.accept().await {
3082 Ok((mut stream, _)) => {
3083 let initial_frame_deadline =
3084 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT;
3085 accept_error_backoff = None;
3086 last_accept_error_log = None;
3087 match peer_uid(&stream) {
3101 Ok(peer) if uid_is_permitted(peer, daemon_euid) => {}
3102 Ok(peer) => {
3103 tracing::error!(
3104 peer_uid = peer,
3105 daemon_euid,
3106 "refusing connection from a foreign uid: this daemon accepts \
3107 only peers running as its own uid"
3108 );
3109 drop(stream);
3110 continue;
3111 }
3112 Err(e) => {
3113 tracing::error!(
3114 error = %e,
3115 "refusing connection: cannot read peer credentials, so \
3116 same-uid cannot be proven"
3117 );
3118 drop(stream);
3119 continue;
3120 }
3121 }
3122 let Some(permit) = admit_or_refuse_busy(
3127 &lifecycle.connections,
3128 &mut stream,
3129 dispatcher.config_id(),
3130 )
3131 .await
3132 else {
3133 continue;
3134 };
3135 let d = dispatcher.clone();
3138 let shutdown = request_shutdown_rx.clone();
3139 let lifecycle = Arc::clone(&lifecycle);
3140 let handle = spawn_connection_task(Arc::clone(&active), async move {
3141 let _permit = permit;
3144 handle_conn_with_lifecycle(
3145 stream,
3146 d,
3147 Some(shutdown),
3148 initial_frame_deadline,
3149 Some(lifecycle),
3150 )
3151 .await;
3152 });
3153 let mut tasks = connection_tasks
3154 .lock()
3155 .unwrap_or_else(std::sync::PoisonError::into_inner);
3156 tasks.retain(|task| !task.is_finished());
3157 tasks.push(handle);
3158 }
3159 Err(e) => {
3160 let delay = next_accept_error_backoff(accept_error_backoff);
3161 accept_error_backoff = Some(delay);
3162 let capacity_exhausted = matches!(
3163 e.raw_os_error(),
3164 Some(libc::EMFILE) | Some(libc::ENFILE)
3165 );
3166 if last_accept_error_log.is_none_or(|last| {
3167 last.elapsed() >= std::time::Duration::from_secs(30)
3168 }) {
3169 tracing::error!(
3170 error = %e,
3171 capacity_exhausted,
3172 retry_ms = delay.as_millis(),
3173 "daemon accept failed; retrying with bounded backoff"
3174 );
3175 last_accept_error_log = Some(std::time::Instant::now());
3176 }
3177 tokio::time::sleep(delay).await;
3178 }
3179 }
3180 }
3181 } => DaemonShutdownReason::Signal,
3182 result = &mut shutdown => { result?; DaemonShutdownReason::Signal },
3183 _ = wait_for_idle(&dispatcher, &lifecycle) => DaemonShutdownReason::Idle,
3184 };
3185
3186 lifecycle.draining(reason);
3187
3188 drop(listener);
3191
3192 let _ = checkpoint_shutdown_tx.send(());
3196
3197 if reason == DaemonShutdownReason::Signal {
3200 let _ = request_shutdown_tx.send(true);
3201 }
3202
3203 daemon_shutdown_token().cancel();
3207
3208 let drained = if reason == DaemonShutdownReason::Idle {
3209 tokio::select! {
3210 _ = drain_for_idle(&active, drain_timeout()) => true,
3211 result = &mut shutdown => {
3212 result?;
3213 lifecycle.draining(DaemonShutdownReason::Signal);
3214 let _ = request_shutdown_tx.send(true);
3215 drain(&active).await
3216 }
3217 }
3218 } else {
3219 drain(&active).await
3220 };
3221 let tasks = {
3222 let mut retained = connection_tasks
3223 .lock()
3224 .unwrap_or_else(std::sync::PoisonError::into_inner);
3225 std::mem::take(&mut *retained)
3226 };
3227 finish_connection_tasks(tasks, drained).await;
3228
3229 match acquire_recovery_lock() {
3238 Some(_shutdown_lock) => {
3239 shutdown_cleanup_if_owned(&sock, &pid_file, bound_identity);
3240 }
3241 None => {
3242 tracing::warn!(
3243 "could not acquire recovery lock for shutdown cleanup; \
3244 skipping unlink to avoid deleting a replacement daemon's paths"
3245 );
3246 }
3247 }
3248 lifecycle.stopped();
3249 tracing::info!("khived stopped");
3250 Ok(())
3251}
3252
3253#[cfg(unix)]
3263fn shutdown_cleanup_if_owned(
3264 sock: &std::path::Path,
3265 pid_file: &std::path::Path,
3266 bound_identity: Option<SocketIdentity>,
3267) -> bool {
3268 let pid_is_ours = std::fs::read_to_string(pid_file)
3269 .ok()
3270 .and_then(|s| s.trim().parse::<u32>().ok())
3271 == Some(std::process::id());
3272 let socket_is_ours = bound_identity.is_some() && socket_identity(sock) == bound_identity;
3273 if pid_is_ours && socket_is_ours {
3274 let _ = std::fs::remove_file(sock);
3275 let _ = std::fs::remove_file(pid_file);
3276 true
3277 } else {
3278 tracing::warn!(
3279 socket = ?sock,
3280 pid_file = ?pid_file,
3281 "skipping shutdown cleanup — a replacement daemon already owns this socket/PID"
3282 );
3283 false
3284 }
3285}
3286
3287#[cfg(unix)]
3291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3292enum PidLiveness {
3293 Alive,
3296 Dead,
3298 PermissionDenied,
3303}
3304
3305#[cfg(unix)]
3306impl PidLiveness {
3307 fn is_running(self) -> bool {
3308 !matches!(self, PidLiveness::Dead)
3309 }
3310}
3311
3312#[cfg(unix)]
3316fn classify_kill_result(rc: i32, errno: i32) -> PidLiveness {
3317 if rc == 0 {
3318 return PidLiveness::Alive;
3319 }
3320 match errno {
3321 libc::EPERM => PidLiveness::PermissionDenied,
3322 _ => PidLiveness::Dead,
3323 }
3324}
3325
3326#[cfg(unix)]
3327fn is_process_running(pid: u32) -> bool {
3328 let Ok(pid) = i32::try_from(pid) else {
3329 return false;
3330 };
3331 if pid <= 0 {
3332 return false;
3333 }
3334 let rc = unsafe { libc::kill(pid, 0) };
3336 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
3337 classify_kill_result(rc, errno).is_running()
3338}
3339
3340#[cfg(unix)]
3347fn pid_can_name_incumbent(pid: u32, current_pid: u32, allow_same_process_incumbent: bool) -> bool {
3348 allow_same_process_incumbent || pid != current_pid
3349}
3350
3351#[cfg(unix)]
3356const DUPLICATE_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
3357
3358#[cfg(unix)]
3389async fn socket_speaks_khived_protocol(sock: &std::path::Path, expected_config_id: &str) -> bool {
3390 let probe = DaemonRequestFrame {
3391 probe_only: true,
3392 protocol_version: PROTOCOL_VERSION,
3393 config_id: expected_config_id.to_string(),
3394 ..Default::default()
3395 };
3396 let Ok(payload) = serde_json::to_vec(&probe) else {
3397 return false;
3398 };
3399 let response = tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, async {
3400 let mut stream = UnixStream::connect(sock).await.ok()?;
3401 write_frame(&mut stream, &payload).await.ok()?;
3402 let raw = read_frame(&mut stream).await.ok()?;
3403 serde_json::from_slice::<DaemonResponseFrame>(&raw).ok()
3404 })
3405 .await
3406 .ok()
3407 .flatten();
3408
3409 let Some(resp) = response else {
3410 return false;
3411 };
3412 let is_probe_ack = resp.ok
3413 && resp.result.is_none()
3414 && resp.error.is_none()
3415 && resp.metrics.is_none()
3416 && resp.request_id.is_none();
3417 is_probe_ack
3418 && !resp.version_mismatch
3419 && !resp.namespace_mismatch
3420 && !resp.config_mismatch
3421 && resp.daemon_protocol_version == PROTOCOL_VERSION
3422 && resp
3423 .served_config_id
3424 .as_deref()
3425 .is_some_and(|served| config_ids_compatible(expected_config_id, served))
3426}
3427
3428#[cfg(unix)]
3431async fn socket_is_unreachable(sock: &std::path::Path) -> bool {
3432 match tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, UnixStream::connect(sock)).await {
3433 Ok(Err(error)) => matches!(
3434 error.kind(),
3435 std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
3436 ),
3437 _ => false,
3438 }
3439}
3440
3441#[cfg(unix)]
3446enum Incumbent {
3447 Serving(u32),
3449 Live(u32),
3451 Stale,
3453}
3454
3455#[cfg(unix)]
3462async fn cleanup_stale_daemon(
3463 sock: &std::path::Path,
3464 pid_file: &std::path::Path,
3465 allow_same_process_incumbent: bool,
3466 expected_config_id: &str,
3467) -> Incumbent {
3468 let mut stale_pid_file_guard = None;
3469 if let Ok(pid_str) = std::fs::read_to_string(pid_file) {
3470 if let Ok(pid) = pid_str.trim().parse::<u32>() {
3471 if pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
3472 && is_process_running(pid)
3473 {
3474 if sock.exists() && socket_speaks_khived_protocol(sock, expected_config_id).await {
3475 return Incumbent::Serving(pid);
3476 }
3477 if sock.exists() && !socket_is_unreachable(sock).await {
3478 return Incumbent::Live(pid);
3479 }
3480 match try_acquire_pid_file_lock(pid_file) {
3481 Ok(Some(guard)) => stale_pid_file_guard = Some(guard),
3482 Ok(None) => return Incumbent::Live(pid),
3483 Err(e) => {
3484 tracing::warn!(
3485 error = %e,
3486 path = ?pid_file,
3487 "cannot check daemon PID-file lock"
3488 );
3489 return Incumbent::Live(pid);
3490 }
3491 }
3492 }
3493 }
3494 }
3495 if sock.exists() {
3496 if let Err(e) = std::fs::remove_file(sock) {
3497 tracing::warn!(error = %e, path = ?sock, "failed to remove stale socket");
3498 }
3499 }
3500 if pid_file.exists() {
3501 if let Err(e) = std::fs::remove_file(pid_file) {
3502 tracing::warn!(error = %e, path = ?pid_file, "failed to remove stale PID file");
3503 }
3504 }
3505 drop(stale_pid_file_guard);
3506 Incumbent::Stale
3507}
3508
3509#[cfg(unix)]
3516fn write_pid_file_exclusive(pid_file: &std::path::Path) -> std::io::Result<std::fs::File> {
3517 use std::os::unix::fs::OpenOptionsExt;
3518
3519 let mut opts = std::fs::OpenOptions::new();
3520 opts.write(true).create_new(true).mode(0o600);
3521 let mut f = opts.open(pid_file)?;
3522 let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
3524 if rc != 0 {
3525 return Err(std::io::Error::last_os_error());
3526 }
3527 f.write_all(std::process::id().to_string().as_bytes())?;
3528 Ok(f)
3529}
3530
3531#[cfg(unix)]
3534fn try_acquire_pid_file_lock(pid_file: &std::path::Path) -> std::io::Result<Option<std::fs::File>> {
3535 let file = std::fs::OpenOptions::new()
3536 .read(true)
3537 .write(true)
3538 .open(pid_file)?;
3539 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
3541 if rc == 0 {
3542 return Ok(Some(file));
3543 }
3544 let error = std::io::Error::last_os_error();
3545 if error.kind() == std::io::ErrorKind::WouldBlock
3546 || error.raw_os_error() == Some(libc::EWOULDBLOCK)
3547 {
3548 Ok(None)
3549 } else {
3550 Err(error)
3551 }
3552}
3553
3554#[cfg(unix)]
3557fn remove_pid_file_if_owned(pid_file: &std::path::Path, guard: &std::fs::File) {
3558 let Ok(owned) = guard.metadata() else {
3559 return;
3560 };
3561 let Ok(current) = std::fs::metadata(pid_file) else {
3562 return;
3563 };
3564 if owned.dev() == current.dev() && owned.ino() == current.ino() {
3565 if let Err(e) = std::fs::remove_file(pid_file) {
3566 tracing::warn!(error = %e, path = ?pid_file, "failed to remove unbound PID file");
3567 }
3568 }
3569}
3570
3571#[cfg(unix)]
3577async fn pid_file_names_a_reachable_daemon(
3578 pid_file: &std::path::Path,
3579 sock: &std::path::Path,
3580 allow_same_process_incumbent: bool,
3581 expected_config_id: &str,
3582) -> bool {
3583 let Ok(pid_str) = std::fs::read_to_string(pid_file) else {
3584 return false;
3585 };
3586 let Ok(pid) = pid_str.trim().parse::<u32>() else {
3587 return false;
3588 };
3589 pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
3590 && is_process_running(pid)
3591 && sock.exists()
3592 && socket_speaks_khived_protocol(sock, expected_config_id).await
3593}
3594
3595#[cfg(unix)]
3596async fn drain(active: &std::sync::atomic::AtomicUsize) -> bool {
3597 drain_with_timeout(active, drain_timeout()).await
3598}
3599
3600#[cfg(unix)]
3602async fn drain_for_idle(active: &std::sync::atomic::AtomicUsize, timeout: std::time::Duration) {
3603 let deadline = tokio::time::Instant::now() + timeout;
3604 let mut warned = false;
3605 while active.load(std::sync::atomic::Ordering::SeqCst) + background_task_count() != 0 {
3606 if !warned && tokio::time::Instant::now() >= deadline {
3607 tracing::warn!(
3608 "idle drain interval elapsed; retaining workers and rendezvous until settled"
3609 );
3610 warned = true;
3611 }
3612 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
3613 }
3614}
3615
3616#[cfg(unix)]
3617async fn drain_with_timeout(
3618 active: &std::sync::atomic::AtomicUsize,
3619 timeout: std::time::Duration,
3620) -> bool {
3621 use std::sync::atomic::Ordering;
3622 let remaining = || active.load(Ordering::SeqCst) + background_task_count();
3626 if remaining() == 0 {
3627 return true;
3628 }
3629 let deadline = tokio::time::Instant::now() + timeout;
3630 while remaining() > 0 {
3631 if tokio::time::Instant::now() >= deadline {
3632 tracing::warn!(
3633 remaining_connections = active.load(Ordering::SeqCst),
3634 remaining_background_tasks = background_task_count(),
3635 outstanding_background_tasks = %background_task_names().join(", "),
3636 "drain timeout reached; forcing shutdown"
3637 );
3638 return false;
3639 }
3640 tokio::select! {
3641 _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
3642 _ = tokio::time::sleep_until(deadline) => {}
3643 }
3644 }
3645 true
3646}
3647
3648#[cfg(unix)]
3649async fn finish_connection_tasks(tasks: Vec<tokio::task::JoinHandle<()>>, drained: bool) {
3650 if !drained {
3651 for task in &tasks {
3652 if !task.is_finished() {
3653 task.abort();
3654 }
3655 }
3656 }
3657 for task in tasks {
3658 let _ = task.await;
3659 }
3660}
3661
3662pub fn drain_timeout() -> std::time::Duration {
3668 let secs = khive_db::env::env_parse_or("KHIVE_DRAIN_TIMEOUT_SECS", DEFAULT_DRAIN_TIMEOUT_SECS);
3669 std::time::Duration::from_secs(secs)
3670}
3671
3672#[cfg(unix)]
3674pub fn env_truthy(key: &str) -> bool {
3675 std::env::var(key)
3676 .map(|v| {
3677 let v = v.trim();
3678 !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false")
3679 })
3680 .unwrap_or(false)
3681}
3682
3683include!("daemon_khive_root_tests.rs");
3684
3685#[cfg(all(unix, any(test, feature = "test-internals")))]
3691#[doc(hidden)]
3692pub async fn serve_connection_for_test<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
3693 handle_conn_with_shutdown(
3694 stream,
3695 dispatcher,
3696 None,
3697 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
3698 )
3699 .await;
3700}
3701
3702#[cfg(all(test, unix))]
3703#[path = "daemon_tests.rs"]
3704mod tests;