1use std::sync::Arc;
12
13mod locks;
14#[cfg(all(test, unix))]
15use locks::open_lock_file;
16#[cfg(unix)]
17pub use locks::{
18 acquire_daemon_boot_guard, acquire_recovery_lock, try_acquire_daemon_boot_guard_until,
19 try_acquire_recoverer_lock_until, DaemonBootGuard,
20};
21#[cfg(unix)]
22pub(crate) use locks::{peer_uid, uid_is_permitted};
23#[cfg(unix)]
24use locks::{socket_identity, SocketIdentity};
25
26mod store_guard;
27mod store_identity;
28#[cfg(unix)]
29use store_guard::ensure_claimed_parent_identity;
30#[cfg(unix)]
31pub use store_guard::{acquire_daemon_store_guards, bind_daemon_store_files, claim_stores};
32pub use store_guard::{assert_daemon_store_identities, DaemonStoreGuard};
33#[cfg(unix)]
34pub use store_identity::claimed_daemon_store_identity;
35mod supervisor_marker;
36#[cfg(unix)]
37pub use supervisor_marker::supervisor_marker_path;
38
39#[cfg(unix)]
40use std::io::Write as _;
41#[cfg(unix)]
42use std::os::unix::fs::{MetadataExt, PermissionsExt};
43#[cfg(unix)]
44use std::os::unix::io::AsRawFd;
45#[cfg(all(test, unix))]
46use std::path::PathBuf;
47
48#[cfg(unix)]
49use async_trait::async_trait;
50#[cfg(unix)]
51use libc;
52#[cfg(unix)]
53use serde::Deserialize;
54#[cfg(unix)]
55use tokio::io::AsyncReadExt;
56#[cfg(all(test, unix))]
57use tokio::io::AsyncWriteExt;
58#[cfg(unix)]
59use tokio::net::{UnixListener, UnixStream};
60
61#[cfg(unix)]
62use crate::pack::RequestIdentity;
63#[cfg(unix)]
64use khive_db::{run_checkpoint_task, CheckpointConfig, CheckpointLifecycleOwner, ConnectionPool};
65
66mod exit_record;
69mod lifecycle;
70#[cfg(unix)]
71use lifecycle::khive_dir;
72#[cfg(test)]
73use lifecycle::khive_root_from;
74#[cfg(unix)]
75use lifecycle::{
76 current_supervisor_claim, default_socket_path, ensure_rendezvous_overrides_paired,
77 read_supervisor_marker_claim, DaemonLifecycle, PID_PATH_ENV, SOCKET_PATH_ENV,
78};
79pub use lifecycle::{
80 lock_path, volume_lock_dir, DaemonLifecyclePhase, DaemonLifecycleSnapshot, DaemonLifetime,
81 DaemonOptions, DaemonShutdownReason, DaemonStartupReport, DEFAULT_DEMAND_IDLE_SECS,
82};
83#[cfg(unix)]
84pub use lifecycle::{
85 pid_path, recoverer_lock_path, rendezvous_split, socket_path, RendezvousSplit,
86 SUPERVISOR_CLAIM_ENV,
87};
88
89mod load_limits;
90#[cfg(unix)]
91use load_limits::{admit_or_refuse_busy, ConnectionAdmission};
92pub use load_limits::{
93 recall_ledger_snapshot, track_recall_ledger_task, ConnectionCapSnapshot, RecallLedgerSnapshot,
94};
95
96pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
98
99pub const PROTOCOL_VERSION: u32 = 8;
108
109#[doc(hidden)]
113pub const DAEMON_LEXICAL_TIMEOUT_MARKER: &str = "__khive_daemon_lexical_timeout";
114
115const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 10;
116#[cfg(unix)]
120const INITIAL_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
121
122#[cfg(unix)]
123fn next_accept_error_backoff(previous: Option<std::time::Duration>) -> std::time::Duration {
124 previous
125 .map(|delay| delay.saturating_mul(2))
126 .unwrap_or_else(|| std::time::Duration::from_millis(10))
127 .min(std::time::Duration::from_secs(1))
128}
129
130mod config_id;
133#[cfg(test)]
134use config_id::parse_config_id;
135pub use config_id::{
136 config_id_extra_embedder_exclusions, config_ids_compatible, first_config_mismatch_field,
137};
138
139mod wire_frames;
140#[cfg(unix)]
141pub use wire_frames::{read_frame, write_frame};
142#[cfg(unix)]
143use wire_frames::{read_initial_frame, take_daemon_lexical_timeout_marker};
144pub use wire_frames::{
145 CheckpointStoreMetrics, DaemonDispatchError, DaemonRequestFrame, DaemonResponseFrame,
146 MetricsSnapshot, ERROR_DETAIL_NESTING_DEPTH_LIMIT,
147};
148
149#[cfg(unix)]
158#[async_trait]
159pub trait DaemonDispatch: Clone + Send + Sync + 'static {
160 fn idle_retirement_blockers(&self) -> Vec<String> {
163 vec!["dispatcher_resource_inventory_unknown".to_owned()]
164 }
165
166 fn plan(&self, ops: &str) -> String;
168
169 #[allow(clippy::too_many_arguments)]
184 async fn dispatch(
185 &self,
186 ops: String,
187 presentation: Option<String>,
188 presentation_per_op: Option<Vec<Option<String>>>,
189 format: Option<String>,
190 format_per_op: Option<Vec<Option<String>>>,
191 from_wire: bool,
192 identity: Option<RequestIdentity>,
193 ) -> Result<String, String>;
194
195 fn request_read_timeout(&self, _ops: &str) -> std::time::Duration {
199 khive_storage::request_read_timeout_from_env()
200 }
201
202 #[allow(clippy::too_many_arguments)]
204 async fn dispatch_with_error_detail(
205 &self,
206 ops: String,
207 presentation: Option<String>,
208 presentation_per_op: Option<Vec<Option<String>>>,
209 format: Option<String>,
210 format_per_op: Option<Vec<Option<String>>>,
211 from_wire: bool,
212 identity: Option<RequestIdentity>,
213 ) -> Result<String, DaemonDispatchError> {
214 self.dispatch(
215 ops,
216 presentation,
217 presentation_per_op,
218 format,
219 format_per_op,
220 from_wire,
221 identity,
222 )
223 .await
224 .map_err(|message| DaemonDispatchError::new(message, None))
225 }
226
227 async fn warm_all(&self);
229
230 fn namespace(&self) -> &str;
232
233 fn config_id(&self) -> &str;
238
239 fn pool_for_checkpoint(&self) -> Option<Arc<ConnectionPool>> {
247 None
248 }
249
250 fn secondary_pools_for_checkpoint(&self) -> Vec<Arc<ConnectionPool>> {
260 Vec::new()
261 }
262
263 fn event_store_for_checkpoint(&self) -> Option<Arc<dyn khive_storage::EventStore>> {
273 None
274 }
275}
276
277#[cfg(unix)]
278struct CheckpointTaskSpec {
279 pool: Arc<ConnectionPool>,
280 lifecycle_owner: Option<CheckpointLifecycleOwner>,
281 is_main: bool,
282}
283
284#[cfg(unix)]
291fn checkpoint_task_specs(
292 main_pool: Option<Arc<ConnectionPool>>,
293 secondary_pools: Vec<Arc<ConnectionPool>>,
294 event_store: Option<Arc<dyn khive_storage::EventStore>>,
295 namespace: String,
296) -> Vec<CheckpointTaskSpec> {
297 let mut tasks = Vec::with_capacity(usize::from(main_pool.is_some()) + secondary_pools.len());
298 if let Some(pool) = main_pool {
299 tasks.push(CheckpointTaskSpec {
300 pool,
301 lifecycle_owner: None,
302 is_main: true,
303 });
304 }
305 tasks.extend(secondary_pools.into_iter().map(|pool| CheckpointTaskSpec {
306 pool,
307 lifecycle_owner: None,
308 is_main: false,
309 }));
310
311 if let (Some(task), Some(event_store)) = (tasks.first_mut(), event_store) {
312 task.lifecycle_owner = Some(CheckpointLifecycleOwner::new(event_store, namespace));
313 }
314 tasks
315}
316
317static WARM_INDEX_HOST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
331
332pub fn mark_warm_index_host() {
336 WARM_INDEX_HOST.store(true, std::sync::atomic::Ordering::Release);
337}
338
339pub fn is_warm_index_host() -> bool {
348 WARM_INDEX_HOST.load(std::sync::atomic::Ordering::Acquire)
349}
350
351static BACKGROUND_TASKS: std::sync::OnceLock<Arc<std::sync::atomic::AtomicUsize>> =
352 std::sync::OnceLock::new();
353
354fn background_tasks() -> &'static Arc<std::sync::atomic::AtomicUsize> {
355 BACKGROUND_TASKS.get_or_init(|| Arc::new(std::sync::atomic::AtomicUsize::new(0)))
356}
357
358static BACKGROUND_TASK_NAMES: std::sync::OnceLock<
374 std::sync::Mutex<std::collections::HashMap<&'static str, usize>>,
375> = std::sync::OnceLock::new();
376
377fn background_task_names_registry(
378) -> &'static std::sync::Mutex<std::collections::HashMap<&'static str, usize>> {
379 BACKGROUND_TASK_NAMES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
380}
381
382pub const UNNAMED_BACKGROUND_TASK: &str = "unnamed";
385
386fn register_background_task_name(name: &'static str) {
387 let mut names = background_task_names_registry()
388 .lock()
389 .unwrap_or_else(std::sync::PoisonError::into_inner);
390 *names.entry(name).or_insert(0) += 1;
391}
392
393fn release_background_task_name(name: &'static str) {
394 let mut names = background_task_names_registry()
395 .lock()
396 .unwrap_or_else(std::sync::PoisonError::into_inner);
397 if let Some(count) = names.get_mut(name) {
398 *count -= 1;
399 if *count == 0 {
400 names.remove(name);
401 }
402 }
403}
404
405pub fn background_task_names() -> Vec<String> {
409 let names = background_task_names_registry()
410 .lock()
411 .unwrap_or_else(std::sync::PoisonError::into_inner);
412 let mut out: Vec<String> = names.keys().map(|name| (*name).to_string()).collect();
413 out.sort();
414 out
415}
416
417#[cfg(unix)]
418fn idle_retirement_blockers<D: DaemonDispatch>(dispatcher: &D) -> Vec<String> {
419 let mut blockers = dispatcher.idle_retirement_blockers();
420 if !khive_storage::tx_registry::snapshot().is_empty() {
421 blockers.push("open_sql_transaction".to_owned());
422 }
423 blockers.extend(
424 active_phase_names()
425 .into_iter()
426 .map(|name| format!("active_phase:{name}")),
427 );
428 let count = background_task_count();
429 let names = background_task_names_registry()
430 .lock()
431 .unwrap_or_else(std::sync::PoisonError::into_inner);
432 if names.values().sum::<usize>() != count {
433 blockers.push("tracked_worker_inventory_unsettled".to_owned());
434 }
435 for name in names.keys() {
438 if !matches!(
439 *name,
440 "wal_checkpoint" | "memory_ann_rotation_watch" | "knowledge_ann_rotation_watch"
441 ) {
442 blockers.push(format!("unsettled_worker:{name}"));
443 }
444 }
445 blockers.sort();
446 blockers.dedup();
447 blockers
448}
449
450#[cfg(unix)]
451async fn wait_for_idle<D: DaemonDispatch>(dispatcher: &D, lifecycle: &DaemonLifecycle) {
452 if lifecycle.options.lifetime == DaemonLifetime::Persistent {
453 std::future::pending::<()>().await;
454 }
455 loop {
456 if lifecycle.try_idle(|| idle_retirement_blockers(dispatcher)) {
457 return;
458 }
459 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
460 }
461}
462
463struct BackgroundTaskGuard {
464 counter: Arc<std::sync::atomic::AtomicUsize>,
465 name: &'static str,
466}
467
468impl Drop for BackgroundTaskGuard {
469 fn drop(&mut self) {
470 release_background_task_name(self.name);
471 self.counter
472 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
473 }
474}
475
476pub fn spawn_tracked_task<F, T>(fut: F) -> tokio::task::JoinHandle<T>
482where
483 F: std::future::Future<Output = T> + Send + 'static,
484 T: Send + 'static,
485{
486 spawn_named_tracked_task(UNNAMED_BACKGROUND_TASK, fut)
487}
488
489pub fn spawn_named_tracked_task<F, T>(name: &'static str, fut: F) -> tokio::task::JoinHandle<T>
496where
497 F: std::future::Future<Output = T> + Send + 'static,
498 T: Send + 'static,
499{
500 background_tasks().fetch_add(1, std::sync::atomic::Ordering::SeqCst);
501 register_background_task_name(name);
502 let guard = BackgroundTaskGuard {
503 counter: background_tasks().clone(),
504 name,
505 };
506 tokio::spawn(async move {
507 let _guard = guard;
508 fut.await
509 })
510}
511
512pub fn track_background_task<F>(fut: F)
517where
518 F: std::future::Future<Output = ()> + Send + 'static,
519{
520 track_named_background_task(UNNAMED_BACKGROUND_TASK, fut);
521}
522
523pub fn track_named_background_task<F>(name: &'static str, fut: F)
526where
527 F: std::future::Future<Output = ()> + Send + 'static,
528{
529 drop(spawn_named_tracked_task(name, fut));
530}
531
532pub fn background_task_count() -> usize {
535 background_tasks().load(std::sync::atomic::Ordering::SeqCst)
536}
537
538pub fn daemon_shutdown_token() -> tokio_util::sync::CancellationToken {
549 static TOKEN: std::sync::OnceLock<tokio_util::sync::CancellationToken> =
550 std::sync::OnceLock::new();
551 TOKEN
552 .get_or_init(tokio_util::sync::CancellationToken::new)
553 .clone()
554}
555
556static ACTIVE_PHASES: std::sync::OnceLock<
567 std::sync::Mutex<std::collections::HashMap<String, usize>>,
568> = std::sync::OnceLock::new();
569
570fn active_phases() -> &'static std::sync::Mutex<std::collections::HashMap<String, usize>> {
571 ACTIVE_PHASES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
572}
573
574pub struct PhaseGuard {
580 name: String,
581}
582
583impl Drop for PhaseGuard {
584 fn drop(&mut self) {
585 let mut map = active_phases()
586 .lock()
587 .unwrap_or_else(std::sync::PoisonError::into_inner);
588 if let Some(count) = map.get_mut(&self.name) {
589 *count -= 1;
590 if *count == 0 {
591 map.remove(&self.name);
592 }
593 }
594 }
595}
596
597pub fn register_active_phase(name: &str) -> PhaseGuard {
602 let mut map = active_phases()
603 .lock()
604 .unwrap_or_else(std::sync::PoisonError::into_inner);
605 *map.entry(name.to_string()).or_insert(0) += 1;
606 PhaseGuard {
607 name: name.to_string(),
608 }
609}
610
611pub fn active_phase_names() -> Vec<String> {
614 let map = active_phases()
615 .lock()
616 .unwrap_or_else(std::sync::PoisonError::into_inner);
617 let mut names: Vec<String> = map.keys().cloned().collect();
618 names.sort();
619 names
620}
621
622#[cfg(unix)]
630fn build_metrics_snapshot<D: DaemonDispatch>(dispatcher: &D) -> MetricsSnapshot {
631 let open_tx_count = khive_storage::tx_registry::snapshot().len();
632 let (oldest_pinned_tx_micros, oldest_pinned_tx_label) =
639 match khive_storage::tx_registry::oldest() {
640 Some((_id, age, label)) => (Some(age.as_micros() as u64), label),
641 None => (None, None),
642 };
643
644 let checkpoint_pool = dispatcher.pool_for_checkpoint();
645 let mut secondary_index = 0;
646 let wal_checkpoint_stores = checkpoint_task_specs(
647 checkpoint_pool.clone(),
648 dispatcher.secondary_pools_for_checkpoint(),
649 None,
650 String::new(),
651 )
652 .into_iter()
653 .map(|task| {
654 let (store_id, role) = if task.is_main {
655 ("main".to_string(), "main".to_string())
656 } else {
657 let store_id = format!("secondary:{secondary_index}");
658 secondary_index += 1;
659 (store_id, "secondary".to_string())
660 };
661 CheckpointStoreMetrics {
662 store_id,
663 role,
664 database: task
665 .pool
666 .canonical_path()
667 .and_then(std::path::Path::file_name)
668 .map(|name| name.to_string_lossy().into_owned()),
669 timing: khive_db::checkpoint::checkpoint_timing(&task.pool),
670 }
671 })
672 .collect();
673 let routine_wal = checkpoint_pool
674 .as_deref()
675 .and_then(khive_db::checkpoint::routine_wal_observation);
676 let writer_stages = checkpoint_pool
677 .as_deref()
678 .and_then(khive_db::writer_task::last_writer_stage_observation);
679 let (write_queue_depth, write_queue_capacity) = checkpoint_pool
680 .as_ref()
681 .and_then(|pool| pool.writer_task_handle().ok().flatten())
682 .map(|handle| (Some(handle.queue_depth()), Some(handle.capacity())))
683 .unwrap_or((None, None));
684
685 MetricsSnapshot {
686 lifecycle: None,
687 wal_pages: routine_wal.as_ref().map(|sample| sample.log_frames),
688 wal_log_frames: routine_wal.as_ref().map(|sample| sample.log_frames),
689 wal_checkpointed_frames: routine_wal
690 .as_ref()
691 .map(|sample| sample.checkpointed_frames),
692 wal_pending_frames: routine_wal.as_ref().map(|sample| sample.pending_frames),
693 wal_physical_bytes: routine_wal
694 .as_ref()
695 .and_then(|sample| sample.physical_wal_bytes),
696 wal_observed_at_unix_ms: routine_wal
697 .as_ref()
698 .map(|sample| sample.observed_at_unix_ms),
699 wal_checkpoint_stores,
700 wal_truncate_attempts: khive_db::checkpoint::truncate_attempts(),
701 wal_truncate_consecutive_failures: khive_db::checkpoint::truncate_consecutive_failures(),
702 wal_checkpoint_skipped_ticks: khive_db::checkpoint::checkpoint_skipped_ticks(),
703 wal_checkpoint_consecutive_skips: khive_db::checkpoint::checkpoint_consecutive_skips(),
704 wal_checkpoint_last_skip_wal_pages: khive_db::checkpoint::checkpoint_last_skip_wal_pages(),
705 oldest_pinned_tx_micros,
706 oldest_pinned_tx_label,
707 open_tx_count,
708 write_queue_depth,
709 write_queue_capacity,
710 write_last_queue_wait_micros: writer_stages
711 .as_ref()
712 .map(|sample| sample.queue_wait_micros),
713 write_last_transaction_acquire_micros: writer_stages
714 .as_ref()
715 .map(|sample| sample.transaction_acquire_micros),
716 write_last_body_micros: writer_stages.as_ref().map(|sample| sample.body_micros),
717 write_last_commit_micros: writer_stages.as_ref().map(|sample| sample.commit_micros),
718 write_last_total_micros: writer_stages.as_ref().map(|sample| sample.total_micros),
719 write_last_observed_at_unix_ms: writer_stages
720 .as_ref()
721 .map(|sample| sample.observed_at_unix_ms),
722 connections: None,
723 recall_ledger: Some(recall_ledger_snapshot()),
724 }
725}
726
727#[cfg(unix)]
728async fn write_response_frame<W>(stream: &mut W, payload: &[u8]) -> std::io::Result<()>
729where
730 W: tokio::io::AsyncWrite + Unpin,
731{
732 tokio::time::timeout(INITIAL_FRAME_READ_TIMEOUT, write_frame(stream, payload))
733 .await
734 .map_err(|_| {
735 std::io::Error::new(
736 std::io::ErrorKind::TimedOut,
737 "daemon response write timed out",
738 )
739 })?
740}
741
742#[cfg(unix)]
743async fn wait_for_peer_disconnect(read: &mut tokio::net::unix::OwnedReadHalf) {
744 let mut byte = [0u8; 1];
745 let _ = read.read(&mut byte).await;
749}
750
751#[cfg(all(unix, test))]
752async fn handle_conn<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
753 handle_conn_with_shutdown(
754 stream,
755 dispatcher,
756 None,
757 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
758 )
759 .await;
760}
761
762#[cfg(all(unix, feature = "fault-injection"))]
763#[doc(hidden)]
764pub async fn handle_conn_for_test<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
765 handle_conn_with_shutdown(
766 stream,
767 dispatcher,
768 None,
769 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
770 )
771 .await;
772}
773
774#[cfg(unix)]
775fn plan_frame_companion(raw: &[u8]) -> Option<&'static str> {
776 let value: serde_json::Value = serde_json::from_slice(raw).ok()?;
777 if value.get("plan").and_then(serde_json::Value::as_bool) != Some(true)
778 || value
779 .get("protocol_version")
780 .and_then(serde_json::Value::as_u64)
781 != Some(u64::from(PROTOCOL_VERSION))
782 {
783 return None;
784 }
785 [
786 "presentation",
787 "presentation_per_op",
788 "format",
789 "format_per_op",
790 "request_id",
791 ]
792 .into_iter()
793 .find(|field| value.get(*field).is_some())
794}
795
796#[cfg(all(
797 unix,
798 any(test, feature = "fault-injection", feature = "test-internals")
799))]
800async fn handle_conn_with_shutdown<D: DaemonDispatch>(
801 stream: UnixStream,
802 dispatcher: D,
803 shutdown: Option<tokio::sync::watch::Receiver<bool>>,
804 initial_frame_deadline: tokio::time::Instant,
805) {
806 handle_conn_with_lifecycle(stream, dispatcher, shutdown, initial_frame_deadline, None).await;
807}
808
809#[cfg(unix)]
810async fn handle_conn_with_lifecycle<D: DaemonDispatch>(
811 mut stream: UnixStream,
812 dispatcher: D,
813 shutdown: Option<tokio::sync::watch::Receiver<bool>>,
814 initial_frame_deadline: tokio::time::Instant,
815 lifecycle: Option<Arc<DaemonLifecycle>>,
816) {
817 let mut ordinary_admission = None;
818 let production_shutdown = shutdown.is_some();
819 let handover_peer_allowed = peer_uid(&stream)
823 .ok()
824 .is_some_and(|uid| uid == unsafe { libc::geteuid() } as u32);
825 let (local_shutdown_tx, local_shutdown_rx) = tokio::sync::watch::channel(false);
826 let shutdown = shutdown.unwrap_or(local_shutdown_rx);
827 let _local_shutdown_tx = local_shutdown_tx;
830 let raw = match read_initial_frame(&mut stream, initial_frame_deadline).await {
831 Ok(r) => r,
832 Err(e) => {
833 tracing::debug!(error = %e, "failed to read daemon request frame");
834 return;
835 }
836 };
837 #[derive(Deserialize)]
838 struct SupervisorRequestEnvelope {
839 #[serde(flatten)]
840 frame: DaemonRequestFrame,
841 #[serde(default)]
842 supervisor_handover: bool,
843 }
844 let decoded: Result<SupervisorRequestEnvelope, _> = serde_json::from_slice(&raw);
845 if decoded.as_ref().ok().is_none_or(|item| item.frame.plan) {
846 if let Some(field) = plan_frame_companion(&raw) {
847 let response = DaemonResponseFrame {
848 ok: false,
849 result: None,
850 error: Some(format!(
851 "invalid_params: plan=true cannot be combined with {field}"
852 )),
853 error_detail: Some(serde_json::json!({
854 "kind": "protocol",
855 "code": "invalid_params",
856 "message": format!("plan=true cannot be combined with {field}"),
857 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
858 })),
859 namespace_mismatch: false,
860 config_mismatch: false,
861 served_config_id: Some(dispatcher.config_id().to_string()),
862 version_mismatch: false,
863 daemon_protocol_version: PROTOCOL_VERSION,
864 metrics: None,
865 request_id: None,
866 };
867 if let Ok(payload) = serde_json::to_vec(&response) {
868 if let Err(error) = write_response_frame(&mut stream, &payload).await {
869 tracing::debug!(%error, "failed to write plan envelope refusal");
870 }
871 }
872 return;
873 }
874 }
875 let (frame, handover_requested) = match decoded {
876 Ok(item) => (item.frame, item.supervisor_handover),
877 Err(e) => {
878 tracing::debug!(error = %e, "failed to decode daemon request frame");
879 return;
880 }
881 };
882 let supervisor_probe = frame.probe_only;
883 let handover_accepted = handover_requested
884 && supervisor_probe
885 && production_shutdown
886 && handover_peer_allowed
887 && frame.protocol_version == PROTOCOL_VERSION
888 && !frame.plan
889 && !frame.metrics_only
890 && frame.ops.is_empty()
891 && read_supervisor_marker_claim().is_some_and(|(pid, _)| pid != std::process::id());
892 let (mut peer_read, mut peer_write) = stream.into_split();
893
894 let served_config_id = Some(dispatcher.config_id().to_string());
895 let resp = if frame.protocol_version != PROTOCOL_VERSION {
896 let msg = format!(
897 "daemon protocol mismatch: client={} daemon={} — \
898 rebuild/update the client binary (make local)",
899 frame.protocol_version, PROTOCOL_VERSION,
900 );
901 tracing::warn!(
902 client_version = frame.protocol_version,
903 daemon_version = PROTOCOL_VERSION,
904 "daemon protocol version mismatch"
905 );
906 DaemonResponseFrame {
907 ok: false,
908 result: None,
909 error: Some(msg.clone()),
910 error_detail: Some(serde_json::json!({
911 "kind": "protocol",
912 "code": "version_mismatch",
913 "message": msg,
914 "domain_disposition": crate::DomainDisposition::Unknown.as_str(),
915 })),
916 namespace_mismatch: false,
917 config_mismatch: false,
918 served_config_id,
919 version_mismatch: frame.protocol_version > PROTOCOL_VERSION,
934 daemon_protocol_version: PROTOCOL_VERSION,
935 metrics: None,
936 request_id: frame.request_id,
937 }
938 } else if handover_requested {
939 if handover_accepted {
940 DaemonResponseFrame {
941 ok: true,
942 result: None,
943 error: None,
944 error_detail: None,
945 namespace_mismatch: false,
946 config_mismatch: false,
947 served_config_id,
948 version_mismatch: false,
949 daemon_protocol_version: PROTOCOL_VERSION,
950 metrics: None,
951 request_id: frame.request_id,
952 }
953 } else {
954 DaemonResponseFrame {
955 ok: false,
956 result: None,
957 error: Some("supervisor handover refused".to_string()),
958 error_detail: Some(serde_json::json!({
959 "kind": "protocol",
960 "code": "supervisor_handover_refused",
961 "message": "supervisor handover refused",
962 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
963 })),
964 namespace_mismatch: false,
965 config_mismatch: false,
966 served_config_id,
967 version_mismatch: false,
968 daemon_protocol_version: PROTOCOL_VERSION,
969 metrics: None,
970 request_id: frame.request_id,
971 }
972 }
973 } else if frame.metrics_only && !frame.plan {
974 DaemonResponseFrame {
981 ok: true,
982 result: None,
983 error: None,
984 error_detail: None,
985 namespace_mismatch: false,
986 config_mismatch: false,
987 served_config_id,
988 version_mismatch: false,
989 daemon_protocol_version: PROTOCOL_VERSION,
990 metrics: Some({
991 let mut metrics = build_metrics_snapshot(&dispatcher);
992 metrics.lifecycle = lifecycle.as_ref().map(|state| {
993 let mut snapshot = state.snapshot();
994 snapshot.idle_blockers = idle_retirement_blockers(&dispatcher);
995 snapshot
996 });
997 metrics.connections = lifecycle.as_ref().map(|state| state.connections.snapshot());
998 metrics
999 }),
1000 request_id: frame.request_id,
1001 }
1002 } else if !config_ids_compatible(&frame.config_id, dispatcher.config_id()) {
1011 DaemonResponseFrame {
1012 ok: false,
1013 result: None,
1014 error: None,
1015 error_detail: Some(serde_json::json!({
1016 "kind": "protocol",
1017 "code": "config_mismatch",
1018 "message": "daemon configuration does not match the request",
1019 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
1020 })),
1021 namespace_mismatch: false,
1022 config_mismatch: true,
1023 served_config_id,
1024 version_mismatch: false,
1025 daemon_protocol_version: PROTOCOL_VERSION,
1026 metrics: None,
1027 request_id: frame.request_id,
1028 }
1029 } else if frame.plan {
1030 DaemonResponseFrame {
1031 ok: true,
1032 result: Some(dispatcher.plan(&frame.ops)),
1033 error: None,
1034 error_detail: None,
1035 namespace_mismatch: false,
1036 config_mismatch: false,
1037 served_config_id,
1038 version_mismatch: false,
1039 daemon_protocol_version: PROTOCOL_VERSION,
1040 metrics: None,
1041 request_id: None,
1042 }
1043 } else if frame.probe_only {
1044 DaemonResponseFrame {
1048 ok: true,
1049 result: None,
1050 error: None,
1051 error_detail: None,
1052 namespace_mismatch: false,
1053 config_mismatch: false,
1054 served_config_id,
1055 version_mismatch: false,
1056 daemon_protocol_version: PROTOCOL_VERSION,
1057 metrics: None,
1058 request_id: frame.request_id,
1059 }
1060 } else {
1061 if let Some(lifecycle) = &lifecycle {
1062 ordinary_admission = lifecycle.admit();
1063 if ordinary_admission.is_none() {
1064 let refusal = DaemonResponseFrame {
1065 ok: false,
1066 result: None,
1067 error: Some("daemon is draining; request was not admitted".to_owned()),
1068 error_detail: Some(serde_json::json!({
1069 "kind": "runtime", "code": "daemon_draining",
1070 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
1071 })),
1072 request_id: frame.request_id,
1073 daemon_protocol_version: PROTOCOL_VERSION,
1074 namespace_mismatch: false,
1075 config_mismatch: false,
1076 served_config_id: Some(dispatcher.config_id().to_owned()),
1077 version_mismatch: false,
1078 metrics: None,
1079 };
1080 if let Ok(payload) = serde_json::to_vec(&refusal) {
1081 let _ = write_response_frame(&mut peer_write, &payload).await;
1082 }
1083 return;
1084 }
1085 }
1086 let identity = RequestIdentity {
1098 namespace: frame.namespace.clone(),
1099 actor_id: frame.actor_id.clone(),
1100 visible_namespaces: frame.visible_namespaces.clone(),
1101 process_ref: frame.process_ref.clone(),
1102 request_id: frame.request_id,
1103 };
1104 tracing::debug!(
1105 request_id = frame.request_id,
1106 "daemon RequestIdentity constructed"
1107 );
1108 let (read_cancel_tx, read_cancel_rx) = tokio::sync::watch::channel(false);
1109 let read_timeout = dispatcher.request_read_timeout(&frame.ops);
1113 let excluded_embedder_names =
1114 config_id_extra_embedder_exclusions(&frame.config_id, dispatcher.config_id())
1115 .expect("compatible configuration ids must expose their extra embedder sets");
1116 let dispatch = crate::runtime::scope_request_embedder_exclusions(
1117 excluded_embedder_names,
1118 khive_storage::scope_request_read_cancellation(
1119 shutdown,
1120 khive_storage::scope_request_read_cancellation(
1121 read_cancel_rx,
1122 khive_storage::scope_request_read_deadline(
1123 read_timeout,
1124 dispatcher.dispatch_with_error_detail(
1125 frame.ops,
1126 frame.presentation,
1127 frame.presentation_per_op,
1128 frame.format,
1129 frame.format_per_op,
1130 frame.from_wire,
1131 Some(identity),
1132 ),
1133 ),
1134 ),
1135 ),
1136 );
1137 tokio::pin!(dispatch);
1138 let dispatch_result = tokio::select! {
1139 result = &mut dispatch => result,
1140 _ = wait_for_peer_disconnect(&mut peer_read) => {
1141 let _ = read_cancel_tx.send(true);
1142 dispatch.await
1143 }
1144 };
1145 match dispatch_result {
1146 Ok(result) => {
1147 let (result, detail) = take_daemon_lexical_timeout_marker(result);
1148 DaemonResponseFrame {
1149 ok: true,
1150 result: Some(result),
1151 error: None,
1152 error_detail: detail,
1153 namespace_mismatch: false,
1154 config_mismatch: false,
1155 served_config_id,
1156 version_mismatch: false,
1157 daemon_protocol_version: PROTOCOL_VERSION,
1158 metrics: None,
1159 request_id: frame.request_id,
1160 }
1161 }
1162 Err(error) => {
1163 let error = DaemonDispatchError::new(error.message, Some(error.error_detail));
1164 DaemonResponseFrame {
1165 ok: false,
1166 result: None,
1167 error: Some(error.message),
1168 error_detail: Some(error.error_detail),
1169 namespace_mismatch: false,
1170 config_mismatch: false,
1171 served_config_id,
1172 version_mismatch: false,
1173 daemon_protocol_version: PROTOCOL_VERSION,
1174 metrics: None,
1175 request_id: frame.request_id,
1176 }
1177 }
1178 }
1179 };
1180
1181 let payload = if supervisor_probe {
1182 serde_json::to_value(&resp).and_then(|mut value| {
1183 if let Some(claim) = current_supervisor_claim() {
1184 value["supervisor_claim"] = serde_json::Value::String(claim);
1185 }
1186 if handover_accepted {
1187 value["supervisor_handover_accepted"] = serde_json::Value::Bool(true);
1188 }
1189 serde_json::to_vec(&value)
1190 })
1191 } else {
1192 serde_json::to_vec(&resp)
1193 };
1194 let mut handover_ack_written = false;
1195 match payload {
1196 Ok(payload) => {
1197 if payload.len() > MAX_FRAME_BYTES {
1198 tracing::warn!(
1205 bytes = payload.len(),
1206 limit = MAX_FRAME_BYTES,
1207 "daemon response exceeds MAX_FRAME_BYTES; sending explicit error frame"
1208 );
1209 let message = format!(
1210 "response too large: {} bytes exceeds {} byte IPC cap",
1211 payload.len(),
1212 MAX_FRAME_BYTES,
1213 );
1214 let err_resp = DaemonResponseFrame {
1216 ok: false,
1217 result: None,
1218 error: Some(message.clone()),
1219 error_detail: Some(serde_json::json!({
1220 "kind": "transport",
1221 "code": "response_frame_size_limit",
1222 "message": message,
1223 "domain_disposition": crate::DomainDisposition::Unknown.as_str(),
1224 })),
1225 namespace_mismatch: false,
1226 config_mismatch: false,
1227 served_config_id: resp.served_config_id,
1228 version_mismatch: false,
1229 daemon_protocol_version: PROTOCOL_VERSION,
1230 metrics: None,
1231 request_id: resp.request_id,
1232 };
1233 if let Ok(err_payload) = serde_json::to_vec(&err_resp) {
1234 if let Err(e) = write_response_frame(&mut peer_write, &err_payload).await {
1235 tracing::debug!(error = %e, "failed to write oversized-response error frame");
1236 }
1237 }
1238 } else {
1239 match write_response_frame(&mut peer_write, &payload).await {
1240 Ok(()) => handover_ack_written = true,
1241 Err(e) => tracing::debug!(error = %e, "failed to write daemon response frame"),
1242 }
1243 }
1244 }
1245 Err(e) => tracing::warn!(error = %e, "failed to serialize daemon response frame"),
1246 }
1247 if handover_accepted && handover_ack_written {
1248 if unsafe { libc::raise(libc::SIGTERM) } != 0 {
1251 tracing::error!(error = %std::io::Error::last_os_error(), "self-directed handover signal failed");
1252 }
1253 }
1254 drop(ordinary_admission);
1255}
1256
1257#[cfg(unix)]
1261struct ActiveConnectionGuard {
1262 active: Arc<std::sync::atomic::AtomicUsize>,
1263}
1264
1265#[cfg(unix)]
1266impl ActiveConnectionGuard {
1267 fn claim(active: Arc<std::sync::atomic::AtomicUsize>) -> Self {
1268 active.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1269 Self { active }
1270 }
1271}
1272
1273#[cfg(unix)]
1274impl Drop for ActiveConnectionGuard {
1275 fn drop(&mut self) {
1276 self.active
1277 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
1278 }
1279}
1280
1281#[cfg(unix)]
1282fn spawn_connection_task<F>(
1283 active: Arc<std::sync::atomic::AtomicUsize>,
1284 future: F,
1285) -> tokio::task::JoinHandle<()>
1286where
1287 F: std::future::Future<Output = ()> + Send + 'static,
1288{
1289 let guard = ActiveConnectionGuard::claim(active);
1290 tokio::spawn(async move {
1291 let _guard = guard;
1292 future.await;
1293 })
1294}
1295
1296#[cfg(unix)]
1307pub async fn run_daemon<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
1308 let boot_guard = Some(acquire_daemon_boot_guard()?);
1309 run_daemon_with_boot_guard_inner(
1310 dispatcher,
1311 boot_guard,
1312 false,
1313 DaemonOptions::default(),
1314 |_| DaemonStartupReport::default(),
1315 )
1316 .await
1317}
1318
1319#[cfg(all(unix, any(test, feature = "fault-injection")))]
1334#[doc(hidden)]
1335pub async fn run_daemon_in_process_test<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
1336 let boot_guard = Some(acquire_daemon_boot_guard()?);
1337 run_daemon_with_boot_guard_inner(
1338 dispatcher,
1339 boot_guard,
1340 true,
1341 DaemonOptions::default(),
1342 |_| DaemonStartupReport::default(),
1343 )
1344 .await
1345}
1346
1347#[cfg(unix)]
1348#[derive(Clone, Copy, PartialEq, Eq)]
1349enum RendezvousPathRole {
1350 Socket,
1351 PidFile,
1352}
1353
1354#[cfg(unix)]
1355impl RendezvousPathRole {
1356 fn env_name(self) -> &'static str {
1357 match self {
1358 Self::Socket => SOCKET_PATH_ENV,
1359 Self::PidFile => PID_PATH_ENV,
1360 }
1361 }
1362
1363 fn directory_name(self) -> &'static str {
1364 match self {
1365 Self::Socket => "socket directory",
1366 Self::PidFile => "PID-file directory",
1367 }
1368 }
1369
1370 fn path_name(self) -> &'static str {
1371 match self {
1372 Self::Socket => "socket path",
1373 Self::PidFile => "PID-file path",
1374 }
1375 }
1376
1377 fn path_component_name(self) -> &'static str {
1378 match self {
1379 Self::Socket => "socket-path",
1380 Self::PidFile => "PID-file-path",
1381 }
1382 }
1383}
1384
1385#[cfg(unix)]
1408pub(crate) fn ensure_socket_dir_is_trusted(parent: &std::path::Path) -> anyhow::Result<()> {
1409 let daemon_euid = unsafe { libc::geteuid() } as u32;
1411 ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::Socket, daemon_euid, true)
1412}
1413
1414#[cfg(unix)]
1419pub fn ensure_pid_file_dir_is_trusted(pid_file: &std::path::Path) -> anyhow::Result<()> {
1420 let parent = pid_file
1421 .parent()
1422 .filter(|parent| !parent.as_os_str().is_empty())
1423 .unwrap_or_else(|| std::path::Path::new("."));
1424 let daemon_euid = unsafe { libc::geteuid() } as u32;
1426 ensure_rendezvous_dir_is_trusted(parent, RendezvousPathRole::PidFile, daemon_euid, false)
1427}
1428
1429#[cfg(unix)]
1430fn ensure_rendezvous_dir_is_trusted(
1431 parent: &std::path::Path,
1432 role: RendezvousPathRole,
1433 daemon_euid: u32,
1434 repair_owned_default: bool,
1435) -> anyhow::Result<()> {
1436 let env_name = role.env_name();
1437 let directory_name = role.directory_name();
1438
1439 if repair_owned_default && parent == khive_dir() {
1440 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
1441 anyhow::anyhow!(
1442 "refusing to start: cannot chmod 0700 {}: {e}. The khive directory must be \
1443 owner-only as the {directory_name} for {env_name}; it is part of the \
1444 same-uid guarantee this daemon enforces.",
1445 parent.display()
1446 )
1447 })?;
1448 return ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role);
1449 }
1450
1451 let meta = std::fs::metadata(parent).map_err(|e| {
1454 anyhow::anyhow!(
1455 "refusing to start: cannot stat {directory_name} {} for {env_name}: {e}. \
1456 It gates rendezvous-path safety, and unreadable metadata is not a passing state.",
1457 parent.display()
1458 )
1459 })?;
1460
1461 use std::os::unix::fs::MetadataExt;
1462 let owner = meta.uid();
1463 if owner != daemon_euid && owner != 0 {
1464 anyhow::bail!(
1465 "refusing to start: {directory_name} {} for {env_name} is owned by uid {owner}, \
1466 not this daemon's uid ({daemon_euid}) or root. A directory owner can replace \
1467 the rendezvous path regardless of mode bits. Point {env_name} at a directory \
1468 you own, or unset it for the default.",
1469 parent.display()
1470 );
1471 }
1472
1473 let mode = meta.permissions().mode();
1474 if mode & 0o022 != 0 {
1475 anyhow::bail!(
1476 "refusing to start: {directory_name} {} for {env_name} is mode {:04o} — writable \
1477 by group or other, so another local user could replace the rendezvous path. \
1478 Use a directory only you can write, or unset {env_name} for the default. \
1479 This daemon is not changing the permissions of a directory it does not own.",
1480 parent.display(),
1481 mode & 0o7777
1482 );
1483 }
1484
1485 ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, role)
1486}
1487
1488#[cfg(all(unix, test))]
1491fn ensure_socket_path_is_swap_resistant(
1492 parent: &std::path::Path,
1493 daemon_euid: u32,
1494) -> anyhow::Result<()> {
1495 ensure_rendezvous_path_is_swap_resistant(parent, daemon_euid, RendezvousPathRole::Socket)
1496}
1497
1498#[cfg(unix)]
1523fn ensure_rendezvous_path_is_swap_resistant(
1524 parent: &std::path::Path,
1525 daemon_euid: u32,
1526 role: RendezvousPathRole,
1527) -> anyhow::Result<()> {
1528 use std::os::unix::fs::MetadataExt;
1529
1530 let env_name = role.env_name();
1531 let directory_name = role.directory_name();
1532 let path_name = role.path_name();
1533 let component_name = role.path_component_name();
1534
1535 let absolute = if parent.is_absolute() {
1536 parent.to_path_buf()
1537 } else {
1538 std::env::current_dir()
1539 .map_err(|e| {
1540 anyhow::anyhow!(
1541 "refusing to start: cannot resolve the working directory to absolutize \
1542 {directory_name} {} for {env_name}: {e}.",
1543 parent.display()
1544 )
1545 })?
1546 .join(parent)
1547 };
1548
1549 fn push_components(stack: &mut Vec<std::ffi::OsString>, path: &std::path::Path) {
1550 let components: Vec<_> = path
1551 .components()
1552 .map(|c| c.as_os_str().to_os_string())
1553 .collect();
1554 stack.extend(components.into_iter().rev());
1555 }
1556
1557 let mut stack: Vec<std::ffi::OsString> = Vec::new();
1558 push_components(&mut stack, &absolute);
1559 let mut resolved = std::path::PathBuf::new();
1560 let mut symlinks_followed = 0u32;
1561
1562 while let Some(component) = stack.pop() {
1563 if component == "/" {
1564 resolved = std::path::PathBuf::from("/");
1565 continue;
1566 }
1567 if component == "." {
1568 continue;
1569 }
1570 if component == ".." {
1571 resolved.pop();
1572 continue;
1573 }
1574 let candidate = resolved.join(&component);
1575 let meta = std::fs::symlink_metadata(&candidate).map_err(|e| {
1576 anyhow::anyhow!(
1577 "refusing to start: cannot stat {component_name} component {} for {env_name}: \
1578 {e}. An unreadable component is not a passing one.",
1579 candidate.display()
1580 )
1581 })?;
1582 let owner = meta.uid();
1583
1584 if meta.file_type().is_symlink() {
1585 symlinks_followed += 1;
1586 if symlinks_followed > 40 {
1587 anyhow::bail!(
1588 "refusing to start: {path_name} for {env_name} resolves through more than \
1589 40 symlinks at {} — treating this as a loop.",
1590 candidate.display()
1591 );
1592 }
1593 if owner != daemon_euid && owner != 0 {
1594 anyhow::bail!(
1595 "refusing to start: {component_name} symlink component {} for {env_name} \
1596 is owned by uid {owner}, not this daemon's uid ({daemon_euid}) or root — \
1597 its owner could retarget it after this check and re-root the {path_name}. \
1598 Point {env_name} somewhere trusted end to end, or unset it for the default.",
1599 candidate.display()
1600 );
1601 }
1602 let target = std::fs::read_link(&candidate).map_err(|e| {
1603 anyhow::anyhow!(
1604 "refusing to start: cannot read {component_name} symlink component {} \
1605 for {env_name}: {e}.",
1606 candidate.display()
1607 )
1608 })?;
1609 push_components(&mut stack, &target);
1610 continue;
1611 }
1612
1613 if meta.is_dir() {
1614 let mode = meta.permissions().mode();
1615 let sticky = mode & 0o1000 != 0;
1616 if owner != daemon_euid && owner != 0 {
1617 anyhow::bail!(
1618 "refusing to start: {component_name} ancestor {} for {env_name} is owned by \
1619 uid {owner}, not this daemon's uid ({daemon_euid}) or root — its owner \
1620 could rename the next path component and re-root the {path_name}. Point \
1621 {env_name} somewhere trusted end to end, or unset it for the default.",
1622 candidate.display()
1623 );
1624 }
1625 if mode & 0o022 != 0 && !sticky {
1626 anyhow::bail!(
1627 "refusing to start: {component_name} ancestor {} for {env_name} is mode \
1628 {:04o} — writable by group or other without the sticky bit, so another \
1629 local user could rename the next path component and re-root the {path_name}. \
1630 Point {env_name} somewhere trusted end to end, or unset it for the default.",
1631 candidate.display(),
1632 mode & 0o7777
1633 );
1634 }
1635 resolved = candidate;
1636 continue;
1637 }
1638
1639 anyhow::bail!(
1640 "refusing to start: {component_name} component {} for {env_name} is neither a \
1641 directory nor a symlink — the {path_name} cannot traverse it.",
1642 candidate.display()
1643 );
1644 }
1645
1646 Ok(())
1647}
1648
1649#[cfg(unix)]
1662pub async fn run_daemon_with_boot_guard<D: DaemonDispatch>(
1663 dispatcher: D,
1664 boot_guard: Option<std::fs::File>,
1665) -> anyhow::Result<()> {
1666 run_daemon_with_boot_guard_inner(
1667 dispatcher,
1668 boot_guard,
1669 false,
1670 DaemonOptions::default(),
1671 |_| DaemonStartupReport::default(),
1672 )
1673 .await
1674}
1675
1676#[cfg(unix)]
1682pub async fn run_daemon_with_boot_guard_and_start<D, F>(
1683 dispatcher: D,
1684 boot_guard: Option<std::fs::File>,
1685 start: F,
1686) -> anyhow::Result<()>
1687where
1688 D: DaemonDispatch,
1689 F: FnOnce(&D) + Send,
1690{
1691 run_daemon_with_options_and_boot_guard_and_start(
1692 dispatcher,
1693 boot_guard,
1694 DaemonOptions::default(),
1695 |dispatcher| {
1696 start(dispatcher);
1697 DaemonStartupReport::default()
1698 },
1699 )
1700 .await
1701}
1702
1703#[cfg(unix)]
1705pub async fn run_daemon_with_options_and_boot_guard_and_start<D, F>(
1706 dispatcher: D,
1707 boot_guard: Option<std::fs::File>,
1708 options: DaemonOptions,
1709 start: F,
1710) -> anyhow::Result<()>
1711where
1712 D: DaemonDispatch,
1713 F: FnOnce(&D) -> DaemonStartupReport + Send,
1714{
1715 anyhow::ensure!(
1716 !options.idle_interval.is_zero(),
1717 "daemon idle interval must be positive"
1718 );
1719 run_daemon_with_boot_guard_inner(dispatcher, boot_guard, false, options, start).await
1720}
1721
1722#[cfg(unix)]
1723async fn run_daemon_with_boot_guard_inner<D, F>(
1724 dispatcher: D,
1725 boot_guard: Option<std::fs::File>,
1726 allow_same_process_incumbent: bool,
1727 options: DaemonOptions,
1728 start: F,
1729) -> anyhow::Result<()>
1730where
1731 D: DaemonDispatch,
1732 F: FnOnce(&D) -> DaemonStartupReport + Send,
1733{
1734 struct ComponentTeardown;
1739 impl Drop for ComponentTeardown {
1740 fn drop(&mut self) {
1741 daemon_shutdown_token().cancel();
1742 }
1743 }
1744 let _component_teardown = ComponentTeardown;
1745
1746 ensure_rendezvous_overrides_paired()?;
1750
1751 let sock = socket_path();
1752 let pid_file = pid_path();
1753 let socket_parent = sock.parent();
1754 let pid_parent = pid_file.parent();
1755
1756 if let Some(parent) = socket_parent {
1757 std::fs::create_dir_all(parent)?;
1758 ensure_socket_dir_is_trusted(parent)?;
1759 }
1760 if pid_parent != socket_parent {
1764 ensure_pid_file_dir_is_trusted(&pid_file)?;
1765 }
1766
1767 let _startup_lock = boot_guard;
1781
1782 match cleanup_stale_daemon(
1785 &sock,
1786 &pid_file,
1787 allow_same_process_incumbent,
1788 dispatcher.config_id(),
1789 )
1790 .await
1791 {
1792 Incumbent::Serving(incumbent_pid) => {
1793 tracing::error!(
1794 pid = incumbent_pid,
1795 socket = ?sock,
1796 "refusing to start: a khived instance is already serving this socket"
1797 );
1798 anyhow::bail!(
1799 "refusing to start: khived is already running as pid {incumbent_pid}, \
1800 serving socket {}. Stop that instance first if you intend to replace it.",
1801 sock.display()
1802 );
1803 }
1804 Incumbent::Live(incumbent_pid) => {
1805 tracing::error!(
1806 pid = incumbent_pid,
1807 socket = ?sock,
1808 "refusing to start: a live process owns the PID file but no khived answered"
1809 );
1810 anyhow::bail!(
1811 "refusing to start: pid {incumbent_pid} owns the daemon PID file and is alive, \
1812 but nothing answered the khived protocol on {}. It may be draining. Nothing \
1813 was removed; stop that process first if you intend to replace it.",
1814 sock.display()
1815 );
1816 }
1817 Incumbent::Stale => {}
1818 }
1819
1820 if let Err(error) = exit_record::capture_signal_senders() {
1825 tracing::warn!(target: "khive.boot", error = %error, "signal sender capture unavailable; the exit record will say sender_pid=unknown");
1826 }
1827 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
1832 let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?;
1833
1834 let pid_file_guard = match write_pid_file_exclusive(&pid_file) {
1835 Ok(guard) => guard,
1836 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
1837 if pid_file_names_a_reachable_daemon(
1840 &pid_file,
1841 &sock,
1842 allow_same_process_incumbent,
1843 dispatcher.config_id(),
1844 )
1845 .await
1846 {
1847 tracing::info!(
1848 "a replacement khived already claimed the pid/socket rendezvous; exiting"
1849 );
1850 return Ok(());
1851 }
1852 anyhow::bail!(
1853 "failed to claim daemon pid file at {pid_file:?}: it already exists \
1854 and does not name a reachable daemon"
1855 );
1856 }
1857 Err(e) => return Err(e.into()),
1858 };
1859 let exit_record = exit_record::ExitRecord::claim(&pid_file);
1861
1862 let listener = match UnixListener::bind(&sock) {
1863 Ok(listener) => listener,
1864 Err(e) => {
1865 exit_record.exiting(exit_record::ExitReason::Error);
1866 remove_pid_file_if_owned(&pid_file, &pid_file_guard);
1867 return Err(e.into());
1868 }
1869 };
1870 if let Err(e) = std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o600)) {
1874 drop(listener);
1875 let _ = std::fs::remove_file(&sock);
1876 exit_record.exiting(exit_record::ExitReason::Error);
1877 remove_pid_file_if_owned(&pid_file, &pid_file_guard);
1878 return Err(anyhow::anyhow!(
1879 "refusing to start: cannot chmod 0600 {}: {e}. The daemon socket must be owner-only \
1880 — it is half of the single-principal guarantee this daemon enforces.",
1881 sock.display()
1882 ));
1883 }
1884 let bound_identity = socket_identity(&sock);
1888
1889 let lifecycle = Arc::new(DaemonLifecycle::new(options, start(&dispatcher)));
1890
1891 drop(_startup_lock);
1894 tracing::info!(
1895 socket = ?sock,
1896 pid = std::process::id(),
1897 source_revision = crate::BUILD_INFO.source_revision,
1898 build_time = crate::BUILD_INFO.build_time,
1899 "khived listening"
1900 );
1901
1902 {
1903 let warm = dispatcher.clone();
1904 track_named_background_task("daemon_warmup", async move {
1905 warm.warm_all().await;
1906 });
1907 }
1908
1909 let (checkpoint_shutdown_tx, checkpoint_shutdown_rx) = tokio::sync::watch::channel(());
1917 let checkpoint_tasks = checkpoint_task_specs(
1923 dispatcher.pool_for_checkpoint(),
1924 dispatcher.secondary_pools_for_checkpoint(),
1925 dispatcher.event_store_for_checkpoint(),
1926 dispatcher.namespace().to_string(),
1927 );
1928 if !checkpoint_tasks.is_empty() {
1929 let cfg = CheckpointConfig::from_env();
1930 let checkpoint_task_count = checkpoint_tasks.len();
1931 for task in checkpoint_tasks {
1932 track_named_background_task(
1933 "wal_checkpoint",
1934 run_checkpoint_task(
1935 task.pool,
1936 cfg.clone(),
1937 task.lifecycle_owner,
1938 checkpoint_shutdown_rx.clone(),
1939 task.is_main,
1940 ),
1941 );
1942 }
1943 tracing::info!(checkpoint_task_count, "WAL checkpoint task(s) started");
1944 }
1945
1946 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1947 let connection_tasks = Arc::new(std::sync::Mutex::new(
1948 Vec::<tokio::task::JoinHandle<()>>::new(),
1949 ));
1950 let (request_shutdown_tx, request_shutdown_rx) = tokio::sync::watch::channel(false);
1951
1952 let shutdown = async {
1953 let signal = tokio::select! {
1954 _ = sigterm.recv() => "SIGTERM",
1955 _ = sigint.recv() => "SIGINT",
1956 };
1957 tracing::info!("received {signal}");
1958 for signal in [libc::SIGTERM, libc::SIGINT] {
1962 if unsafe { libc::signal(signal, libc::SIG_DFL) } == libc::SIG_ERR {
1965 return Err(std::io::Error::last_os_error());
1966 }
1967 }
1968 Ok::<&'static str, std::io::Error>(signal)
1969 };
1970 tokio::pin!(shutdown);
1971 lifecycle.ready();
1972
1973 let daemon_euid = unsafe { libc::geteuid() } as u32;
1975
1976 let reason = tokio::select! {
1977 _ = async {
1978 let mut accept_error_backoff = None;
1979 let mut last_accept_error_log: Option<std::time::Instant> = None;
1980 loop {
1981 match listener.accept().await {
1982 Ok((mut stream, _)) => {
1983 let initial_frame_deadline =
1984 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT;
1985 accept_error_backoff = None;
1986 last_accept_error_log = None;
1987 match peer_uid(&stream) {
2001 Ok(peer) if uid_is_permitted(peer, daemon_euid) => {}
2002 Ok(peer) => {
2003 tracing::error!(
2004 peer_uid = peer,
2005 daemon_euid,
2006 "refusing connection from a foreign uid: this daemon accepts \
2007 only peers running as its own uid"
2008 );
2009 drop(stream);
2010 continue;
2011 }
2012 Err(e) => {
2013 tracing::error!(
2014 error = %e,
2015 "refusing connection: cannot read peer credentials, so \
2016 same-uid cannot be proven"
2017 );
2018 drop(stream);
2019 continue;
2020 }
2021 }
2022 let Some(permit) = admit_or_refuse_busy(
2027 &lifecycle.connections,
2028 &mut stream,
2029 dispatcher.config_id(),
2030 )
2031 .await
2032 else {
2033 continue;
2034 };
2035 let d = dispatcher.clone();
2038 let shutdown = request_shutdown_rx.clone();
2039 let lifecycle = Arc::clone(&lifecycle);
2040 let handle = spawn_connection_task(Arc::clone(&active), async move {
2041 let _permit = permit;
2044 handle_conn_with_lifecycle(
2045 stream,
2046 d,
2047 Some(shutdown),
2048 initial_frame_deadline,
2049 Some(lifecycle),
2050 )
2051 .await;
2052 });
2053 let mut tasks = connection_tasks
2054 .lock()
2055 .unwrap_or_else(std::sync::PoisonError::into_inner);
2056 tasks.retain(|task| !task.is_finished());
2057 tasks.push(handle);
2058 }
2059 Err(e) => {
2060 let delay = next_accept_error_backoff(accept_error_backoff);
2061 accept_error_backoff = Some(delay);
2062 let capacity_exhausted = matches!(
2063 e.raw_os_error(),
2064 Some(libc::EMFILE) | Some(libc::ENFILE)
2065 );
2066 if last_accept_error_log.is_none_or(|last| {
2067 last.elapsed() >= std::time::Duration::from_secs(30)
2068 }) {
2069 tracing::error!(
2070 error = %e,
2071 capacity_exhausted,
2072 retry_ms = delay.as_millis(),
2073 "daemon accept failed; retrying with bounded backoff"
2074 );
2075 last_accept_error_log = Some(std::time::Instant::now());
2076 }
2077 tokio::time::sleep(delay).await;
2078 }
2079 }
2080 }
2081 } => DaemonShutdownReason::Signal,
2082 result = &mut shutdown => {
2083 exit_record.exiting(exit_record::ExitReason::Signal(result?));
2084 DaemonShutdownReason::Signal
2085 },
2086 _ = wait_for_idle(&dispatcher, &lifecycle) => {
2087 exit_record.exiting(exit_record::ExitReason::Idle);
2088 DaemonShutdownReason::Idle
2089 },
2090 };
2091
2092 lifecycle.draining(reason);
2093
2094 drop(listener);
2097
2098 let _ = checkpoint_shutdown_tx.send(());
2102
2103 if reason == DaemonShutdownReason::Signal {
2106 let _ = request_shutdown_tx.send(true);
2107 }
2108
2109 daemon_shutdown_token().cancel();
2113
2114 let drained = if reason == DaemonShutdownReason::Idle {
2115 tokio::select! {
2116 _ = drain_for_idle(&active, drain_timeout()) => true,
2117 result = &mut shutdown => {
2118 exit_record.exiting(exit_record::ExitReason::Signal(result?));
2119 lifecycle.draining(DaemonShutdownReason::Signal);
2120 let _ = request_shutdown_tx.send(true);
2121 drain(&active).await
2122 }
2123 }
2124 } else {
2125 drain(&active).await
2126 };
2127 let tasks = {
2128 let mut retained = connection_tasks
2129 .lock()
2130 .unwrap_or_else(std::sync::PoisonError::into_inner);
2131 std::mem::take(&mut *retained)
2132 };
2133 finish_connection_tasks(tasks, drained).await;
2134
2135 match acquire_recovery_lock() {
2144 Some(_shutdown_lock) => {
2145 shutdown_cleanup_if_owned(&sock, &pid_file, bound_identity);
2146 }
2147 None => {
2148 tracing::warn!(
2149 "could not acquire recovery lock for shutdown cleanup; \
2150 skipping unlink to avoid deleting a replacement daemon's paths"
2151 );
2152 }
2153 }
2154 lifecycle.stopped();
2155 tracing::info!("khived stopped");
2156 Ok(())
2157}
2158
2159#[cfg(unix)]
2169fn shutdown_cleanup_if_owned(
2170 sock: &std::path::Path,
2171 pid_file: &std::path::Path,
2172 bound_identity: Option<SocketIdentity>,
2173) -> bool {
2174 let pid_is_ours = std::fs::read_to_string(pid_file)
2175 .ok()
2176 .and_then(|s| s.trim().parse::<u32>().ok())
2177 == Some(std::process::id());
2178 let socket_is_ours = bound_identity.is_some() && socket_identity(sock) == bound_identity;
2179 if pid_is_ours && socket_is_ours {
2180 let _ = std::fs::remove_file(sock);
2181 let _ = std::fs::remove_file(pid_file);
2182 true
2183 } else {
2184 tracing::warn!(
2185 socket = ?sock,
2186 pid_file = ?pid_file,
2187 "skipping shutdown cleanup — a replacement daemon already owns this socket/PID"
2188 );
2189 false
2190 }
2191}
2192
2193#[cfg(unix)]
2197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2198enum PidLiveness {
2199 Alive,
2202 Dead,
2204 PermissionDenied,
2209}
2210
2211#[cfg(unix)]
2212impl PidLiveness {
2213 fn is_running(self) -> bool {
2214 !matches!(self, PidLiveness::Dead)
2215 }
2216}
2217
2218#[cfg(unix)]
2222fn classify_kill_result(rc: i32, errno: i32) -> PidLiveness {
2223 if rc == 0 {
2224 return PidLiveness::Alive;
2225 }
2226 match errno {
2227 libc::EPERM => PidLiveness::PermissionDenied,
2228 _ => PidLiveness::Dead,
2229 }
2230}
2231
2232#[cfg(unix)]
2233fn is_process_running(pid: u32) -> bool {
2234 let Ok(pid) = i32::try_from(pid) else {
2235 return false;
2236 };
2237 if pid <= 0 {
2238 return false;
2239 }
2240 let rc = unsafe { libc::kill(pid, 0) };
2242 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
2243 classify_kill_result(rc, errno).is_running()
2244}
2245
2246#[cfg(unix)]
2253fn pid_can_name_incumbent(pid: u32, current_pid: u32, allow_same_process_incumbent: bool) -> bool {
2254 allow_same_process_incumbent || pid != current_pid
2255}
2256
2257#[cfg(unix)]
2262const DUPLICATE_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
2263
2264#[cfg(unix)]
2295async fn socket_speaks_khived_protocol(sock: &std::path::Path, expected_config_id: &str) -> bool {
2296 let probe = DaemonRequestFrame {
2297 probe_only: true,
2298 protocol_version: PROTOCOL_VERSION,
2299 config_id: expected_config_id.to_string(),
2300 ..Default::default()
2301 };
2302 let Ok(payload) = serde_json::to_vec(&probe) else {
2303 return false;
2304 };
2305 let response = tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, async {
2306 let mut stream = UnixStream::connect(sock).await.ok()?;
2307 write_frame(&mut stream, &payload).await.ok()?;
2308 let raw = read_frame(&mut stream).await.ok()?;
2309 serde_json::from_slice::<DaemonResponseFrame>(&raw).ok()
2310 })
2311 .await
2312 .ok()
2313 .flatten();
2314
2315 let Some(resp) = response else {
2316 return false;
2317 };
2318 let is_probe_ack = resp.ok
2319 && resp.result.is_none()
2320 && resp.error.is_none()
2321 && resp.metrics.is_none()
2322 && resp.request_id.is_none();
2323 is_probe_ack
2324 && !resp.version_mismatch
2325 && !resp.namespace_mismatch
2326 && !resp.config_mismatch
2327 && resp.daemon_protocol_version == PROTOCOL_VERSION
2328 && resp
2329 .served_config_id
2330 .as_deref()
2331 .is_some_and(|served| config_ids_compatible(expected_config_id, served))
2332}
2333
2334#[cfg(unix)]
2337async fn socket_is_unreachable(sock: &std::path::Path) -> bool {
2338 match tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, UnixStream::connect(sock)).await {
2339 Ok(Err(error)) => matches!(
2340 error.kind(),
2341 std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
2342 ),
2343 _ => false,
2344 }
2345}
2346
2347#[cfg(unix)]
2352enum Incumbent {
2353 Serving(u32),
2355 Live(u32),
2357 Stale,
2359}
2360
2361#[cfg(unix)]
2368async fn cleanup_stale_daemon(
2369 sock: &std::path::Path,
2370 pid_file: &std::path::Path,
2371 allow_same_process_incumbent: bool,
2372 expected_config_id: &str,
2373) -> Incumbent {
2374 let mut stale_pid_file_guard = None;
2375 if let Ok(pid_str) = std::fs::read_to_string(pid_file) {
2376 if let Ok(pid) = pid_str.trim().parse::<u32>() {
2377 if pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
2378 && is_process_running(pid)
2379 {
2380 if sock.exists() && socket_speaks_khived_protocol(sock, expected_config_id).await {
2381 return Incumbent::Serving(pid);
2382 }
2383 if sock.exists() && !socket_is_unreachable(sock).await {
2384 return Incumbent::Live(pid);
2385 }
2386 match try_acquire_pid_file_lock(pid_file) {
2387 Ok(Some(guard)) => stale_pid_file_guard = Some(guard),
2388 Ok(None) => return Incumbent::Live(pid),
2389 Err(e) => {
2390 tracing::warn!(
2391 error = %e,
2392 path = ?pid_file,
2393 "cannot check daemon PID-file lock"
2394 );
2395 return Incumbent::Live(pid);
2396 }
2397 }
2398 }
2399 }
2400 }
2401 if sock.exists() {
2402 if let Err(e) = std::fs::remove_file(sock) {
2403 tracing::warn!(error = %e, path = ?sock, "failed to remove stale socket");
2404 }
2405 }
2406 if pid_file.exists() {
2407 if let Err(e) = std::fs::remove_file(pid_file) {
2408 tracing::warn!(error = %e, path = ?pid_file, "failed to remove stale PID file");
2409 }
2410 }
2411 drop(stale_pid_file_guard);
2412 Incumbent::Stale
2413}
2414
2415#[cfg(unix)]
2422fn write_pid_file_exclusive(pid_file: &std::path::Path) -> std::io::Result<std::fs::File> {
2423 use std::os::unix::fs::OpenOptionsExt;
2424
2425 let mut opts = std::fs::OpenOptions::new();
2426 opts.write(true).create_new(true).mode(0o600);
2427 let mut f = opts.open(pid_file)?;
2428 let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
2430 if rc != 0 {
2431 return Err(std::io::Error::last_os_error());
2432 }
2433 f.write_all(std::process::id().to_string().as_bytes())?;
2434 Ok(f)
2435}
2436
2437#[cfg(unix)]
2440fn try_acquire_pid_file_lock(pid_file: &std::path::Path) -> std::io::Result<Option<std::fs::File>> {
2441 let file = std::fs::OpenOptions::new()
2442 .read(true)
2443 .write(true)
2444 .open(pid_file)?;
2445 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
2447 if rc == 0 {
2448 return Ok(Some(file));
2449 }
2450 let error = std::io::Error::last_os_error();
2451 if error.kind() == std::io::ErrorKind::WouldBlock
2452 || error.raw_os_error() == Some(libc::EWOULDBLOCK)
2453 {
2454 Ok(None)
2455 } else {
2456 Err(error)
2457 }
2458}
2459
2460#[cfg(unix)]
2463fn remove_pid_file_if_owned(pid_file: &std::path::Path, guard: &std::fs::File) {
2464 let Ok(owned) = guard.metadata() else {
2465 return;
2466 };
2467 let Ok(current) = std::fs::metadata(pid_file) else {
2468 return;
2469 };
2470 if owned.dev() == current.dev() && owned.ino() == current.ino() {
2471 if let Err(e) = std::fs::remove_file(pid_file) {
2472 tracing::warn!(error = %e, path = ?pid_file, "failed to remove unbound PID file");
2473 }
2474 }
2475}
2476
2477#[cfg(unix)]
2483async fn pid_file_names_a_reachable_daemon(
2484 pid_file: &std::path::Path,
2485 sock: &std::path::Path,
2486 allow_same_process_incumbent: bool,
2487 expected_config_id: &str,
2488) -> bool {
2489 let Ok(pid_str) = std::fs::read_to_string(pid_file) else {
2490 return false;
2491 };
2492 let Ok(pid) = pid_str.trim().parse::<u32>() else {
2493 return false;
2494 };
2495 pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
2496 && is_process_running(pid)
2497 && sock.exists()
2498 && socket_speaks_khived_protocol(sock, expected_config_id).await
2499}
2500
2501#[cfg(unix)]
2502async fn drain(active: &std::sync::atomic::AtomicUsize) -> bool {
2503 drain_with_timeout(active, drain_timeout()).await
2504}
2505
2506#[cfg(unix)]
2508async fn drain_for_idle(active: &std::sync::atomic::AtomicUsize, timeout: std::time::Duration) {
2509 let deadline = tokio::time::Instant::now() + timeout;
2510 let mut warned = false;
2511 while active.load(std::sync::atomic::Ordering::SeqCst) + background_task_count() != 0 {
2512 if !warned && tokio::time::Instant::now() >= deadline {
2513 tracing::warn!(
2514 "idle drain interval elapsed; retaining workers and rendezvous until settled"
2515 );
2516 warned = true;
2517 }
2518 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2519 }
2520}
2521
2522#[cfg(unix)]
2523async fn drain_with_timeout(
2524 active: &std::sync::atomic::AtomicUsize,
2525 timeout: std::time::Duration,
2526) -> bool {
2527 use std::sync::atomic::Ordering;
2528 let remaining = || active.load(Ordering::SeqCst) + background_task_count();
2532 if remaining() == 0 {
2533 return true;
2534 }
2535 let deadline = tokio::time::Instant::now() + timeout;
2536 while remaining() > 0 {
2537 if tokio::time::Instant::now() >= deadline {
2538 tracing::warn!(
2539 remaining_connections = active.load(Ordering::SeqCst),
2540 remaining_background_tasks = background_task_count(),
2541 outstanding_background_tasks = %background_task_names().join(", "),
2542 "drain timeout reached; forcing shutdown"
2543 );
2544 return false;
2545 }
2546 tokio::select! {
2547 _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
2548 _ = tokio::time::sleep_until(deadline) => {}
2549 }
2550 }
2551 true
2552}
2553
2554#[cfg(unix)]
2555async fn finish_connection_tasks(tasks: Vec<tokio::task::JoinHandle<()>>, drained: bool) {
2556 if !drained {
2557 for task in &tasks {
2558 if !task.is_finished() {
2559 task.abort();
2560 }
2561 }
2562 }
2563 for task in tasks {
2564 let _ = task.await;
2565 }
2566}
2567
2568pub fn drain_timeout() -> std::time::Duration {
2574 let secs = khive_db::env::env_parse_or("KHIVE_DRAIN_TIMEOUT_SECS", DEFAULT_DRAIN_TIMEOUT_SECS);
2575 std::time::Duration::from_secs(secs)
2576}
2577
2578#[cfg(unix)]
2580pub fn env_truthy(key: &str) -> bool {
2581 std::env::var(key)
2582 .map(|v| {
2583 let v = v.trim();
2584 !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false")
2585 })
2586 .unwrap_or(false)
2587}
2588
2589include!("daemon_khive_root_tests.rs");
2590
2591#[cfg(all(unix, any(test, feature = "test-internals")))]
2597#[doc(hidden)]
2598pub async fn serve_connection_for_test<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
2599 handle_conn_with_shutdown(
2600 stream,
2601 dispatcher,
2602 None,
2603 tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
2604 )
2605 .await;
2606}
2607
2608#[cfg(all(test, unix))]
2609#[path = "daemon_tests.rs"]
2610mod tests;