1use std::io::Write as _;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15#[cfg(unix)]
16use std::os::unix::fs::PermissionsExt;
17#[cfg(unix)]
18use std::os::unix::io::AsRawFd;
19
20use async_trait::async_trait;
21#[cfg(unix)]
22use libc;
23use serde::{Deserialize, Serialize};
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use tokio::net::{UnixListener, UnixStream};
26
27use khive_db::{run_checkpoint_task, CheckpointConfig, ConnectionPool};
28
29pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
31
32pub const PROTOCOL_VERSION: u32 = 2;
44
45const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 10;
46
47fn khive_dir() -> PathBuf {
50 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
51 PathBuf::from(home).join(".khive")
52}
53
54pub fn socket_path() -> PathBuf {
58 if let Ok(p) = std::env::var("KHIVE_SOCKET") {
59 if !p.is_empty() {
60 return PathBuf::from(p);
61 }
62 }
63 khive_dir().join("khived.sock")
64}
65
66pub fn pid_path() -> PathBuf {
70 if let Ok(p) = std::env::var("KHIVE_PID") {
71 if !p.is_empty() {
72 return PathBuf::from(p);
73 }
74 }
75 khive_dir().join("khived.pid")
76}
77
78pub fn lock_path() -> PathBuf {
83 if let Ok(p) = std::env::var("KHIVE_LOCK") {
84 if !p.is_empty() {
85 return PathBuf::from(p);
86 }
87 }
88 khive_dir().join("khived.recovery.lock")
89}
90
91#[cfg(unix)]
98pub fn acquire_recovery_lock() -> Option<std::fs::File> {
99 let path = lock_path();
100 if let Some(parent) = path.parent() {
101 let _ = std::fs::create_dir_all(parent);
102 }
103 let file = match std::fs::OpenOptions::new()
104 .create(true)
105 .truncate(false)
106 .write(true)
107 .open(&path)
108 {
109 Ok(f) => f,
110 Err(e) => {
111 tracing::warn!(error = %e, path = ?path, "cannot open recovery lock file");
112 return None;
113 }
114 };
115 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
117 if rc != 0 {
118 tracing::warn!("flock LOCK_EX failed on recovery lock");
119 return None;
120 }
121 Some(file)
122}
123
124#[derive(Serialize, Deserialize, Default)]
128pub struct DaemonRequestFrame {
129 pub ops: String,
130 pub presentation: Option<String>,
131 pub presentation_per_op: Option<Vec<Option<String>>>,
132 pub namespace: String,
133 #[serde(default)]
138 pub config_id: String,
139 #[serde(default)]
143 pub protocol_version: u32,
144 #[serde(default)]
150 pub probe_only: bool,
151 #[serde(default)]
154 pub format: Option<String>,
155 #[serde(default)]
157 pub format_per_op: Option<Vec<Option<String>>>,
158 #[serde(default)]
171 pub from_wire: bool,
172}
173
174#[derive(Serialize, Deserialize)]
176pub struct DaemonResponseFrame {
177 pub ok: bool,
178 pub result: Option<String>,
179 pub error: Option<String>,
180 pub namespace_mismatch: bool,
181 #[serde(default)]
185 pub config_mismatch: bool,
186 #[serde(default)]
193 pub served_config_id: Option<String>,
194 #[serde(default)]
199 pub version_mismatch: bool,
200 #[serde(default)]
204 pub daemon_protocol_version: u32,
205}
206
207pub async fn read_frame(stream: &mut UnixStream) -> std::io::Result<Vec<u8>> {
211 let mut len_buf = [0u8; 4];
212 stream.read_exact(&mut len_buf).await?;
213 let len = u32::from_be_bytes(len_buf) as usize;
214 if len > MAX_FRAME_BYTES {
215 return Err(std::io::Error::new(
216 std::io::ErrorKind::InvalidData,
217 format!("daemon frame of {len} bytes exceeds {MAX_FRAME_BYTES} cap"),
218 ));
219 }
220 let mut buf = vec![0u8; len];
221 stream.read_exact(&mut buf).await?;
222 Ok(buf)
223}
224
225pub async fn write_frame(stream: &mut UnixStream, payload: &[u8]) -> std::io::Result<()> {
227 if payload.len() > MAX_FRAME_BYTES {
228 return Err(std::io::Error::new(
229 std::io::ErrorKind::InvalidData,
230 format!(
231 "daemon frame of {} bytes exceeds {MAX_FRAME_BYTES} cap",
232 payload.len()
233 ),
234 ));
235 }
236 let len = (payload.len() as u32).to_be_bytes();
237 stream.write_all(&len).await?;
238 stream.write_all(payload).await?;
239 stream.flush().await?;
240 Ok(())
241}
242
243#[async_trait]
252pub trait DaemonDispatch: Clone + Send + Sync + 'static {
253 async fn dispatch(
260 &self,
261 ops: String,
262 presentation: Option<String>,
263 presentation_per_op: Option<Vec<Option<String>>>,
264 format: Option<String>,
265 format_per_op: Option<Vec<Option<String>>>,
266 from_wire: bool,
267 ) -> Result<String, String>;
268
269 async fn warm_all(&self);
271
272 fn namespace(&self) -> &str;
274
275 fn config_id(&self) -> &str;
280
281 fn pool_for_checkpoint(&self) -> Option<Arc<ConnectionPool>> {
289 None
290 }
291}
292
293async fn handle_conn<D: DaemonDispatch>(mut stream: UnixStream, dispatcher: D) {
296 let raw = match read_frame(&mut stream).await {
297 Ok(r) => r,
298 Err(e) => {
299 tracing::debug!(error = %e, "failed to read daemon request frame");
300 return;
301 }
302 };
303 let frame: DaemonRequestFrame = match serde_json::from_slice(&raw) {
304 Ok(f) => f,
305 Err(e) => {
306 tracing::debug!(error = %e, "failed to decode daemon request frame");
307 return;
308 }
309 };
310
311 let served_config_id = Some(dispatcher.config_id().to_string());
312 let resp = if frame.protocol_version != PROTOCOL_VERSION {
313 let msg = format!(
314 "daemon protocol mismatch: client={} daemon={} — \
315 rebuild/update the client binary (make local)",
316 frame.protocol_version, PROTOCOL_VERSION,
317 );
318 tracing::warn!(
319 client_version = frame.protocol_version,
320 daemon_version = PROTOCOL_VERSION,
321 "daemon protocol version mismatch"
322 );
323 DaemonResponseFrame {
324 ok: false,
325 result: None,
326 error: Some(msg),
327 namespace_mismatch: false,
328 config_mismatch: false,
329 served_config_id,
330 version_mismatch: true,
331 daemon_protocol_version: PROTOCOL_VERSION,
332 }
333 } else if frame.namespace != dispatcher.namespace() {
334 DaemonResponseFrame {
335 ok: false,
336 result: None,
337 error: None,
338 namespace_mismatch: true,
339 config_mismatch: false,
340 served_config_id,
341 version_mismatch: false,
342 daemon_protocol_version: PROTOCOL_VERSION,
343 }
344 } else if frame.config_id != dispatcher.config_id() {
345 DaemonResponseFrame {
346 ok: false,
347 result: None,
348 error: None,
349 namespace_mismatch: false,
350 config_mismatch: true,
351 served_config_id,
352 version_mismatch: false,
353 daemon_protocol_version: PROTOCOL_VERSION,
354 }
355 } else if frame.probe_only {
356 DaemonResponseFrame {
360 ok: true,
361 result: None,
362 error: None,
363 namespace_mismatch: false,
364 config_mismatch: false,
365 served_config_id,
366 version_mismatch: false,
367 daemon_protocol_version: PROTOCOL_VERSION,
368 }
369 } else {
370 match dispatcher
371 .dispatch(
372 frame.ops,
373 frame.presentation,
374 frame.presentation_per_op,
375 frame.format,
376 frame.format_per_op,
377 frame.from_wire,
378 )
379 .await
380 {
381 Ok(result) => DaemonResponseFrame {
382 ok: true,
383 result: Some(result),
384 error: None,
385 namespace_mismatch: false,
386 config_mismatch: false,
387 served_config_id,
388 version_mismatch: false,
389 daemon_protocol_version: PROTOCOL_VERSION,
390 },
391 Err(e) => DaemonResponseFrame {
392 ok: false,
393 result: None,
394 error: Some(e),
395 namespace_mismatch: false,
396 config_mismatch: false,
397 served_config_id,
398 version_mismatch: false,
399 daemon_protocol_version: PROTOCOL_VERSION,
400 },
401 }
402 };
403
404 match serde_json::to_vec(&resp) {
405 Ok(payload) => {
406 if payload.len() > MAX_FRAME_BYTES {
407 tracing::warn!(
414 bytes = payload.len(),
415 limit = MAX_FRAME_BYTES,
416 "daemon response exceeds MAX_FRAME_BYTES; sending explicit error frame"
417 );
418 let err_resp = DaemonResponseFrame {
419 ok: false,
420 result: None,
421 error: Some(format!(
422 "response too large: {} bytes exceeds {} byte IPC cap",
423 payload.len(),
424 MAX_FRAME_BYTES,
425 )),
426 namespace_mismatch: false,
427 config_mismatch: false,
428 served_config_id: resp.served_config_id,
429 version_mismatch: false,
430 daemon_protocol_version: PROTOCOL_VERSION,
431 };
432 if let Ok(err_payload) = serde_json::to_vec(&err_resp) {
433 if let Err(e) = write_frame(&mut stream, &err_payload).await {
434 tracing::debug!(error = %e, "failed to write oversized-response error frame");
435 }
436 }
437 } else if let Err(e) = write_frame(&mut stream, &payload).await {
438 tracing::debug!(error = %e, "failed to write daemon response frame");
439 }
440 }
441 Err(e) => tracing::warn!(error = %e, "failed to serialize daemon response frame"),
442 }
443}
444
445pub async fn run_daemon<D: DaemonDispatch>(dispatcher: D) -> anyhow::Result<()> {
448 let sock = socket_path();
449 let pid_file = pid_path();
450
451 if let Some(parent) = sock.parent() {
452 std::fs::create_dir_all(parent)?;
453 #[cfg(unix)]
454 if let Err(e) = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)) {
455 tracing::warn!(error = %e, path = ?parent, "failed to chmod 0700 khive dir");
456 }
457 }
458
459 #[cfg(unix)]
470 let _startup_lock = acquire_recovery_lock();
471
472 if !cleanup_stale_daemon(&sock, &pid_file).await {
473 tracing::info!("a responsive khived is already running; exiting");
474 return Ok(());
475 }
476
477 let listener = UnixListener::bind(&sock)?;
478 #[cfg(unix)]
479 if let Err(e) = std::fs::set_permissions(&sock, std::fs::Permissions::from_mode(0o600)) {
480 tracing::warn!(error = %e, path = ?sock, "failed to chmod 0600 socket");
481 }
482
483 write_pid_file(&pid_file)?;
484 #[cfg(unix)]
488 drop(_startup_lock);
489 tracing::info!(socket = ?sock, pid = std::process::id(), "khived listening");
490
491 {
492 let warm = dispatcher.clone();
493 tokio::spawn(async move {
494 warm.warm_all().await;
495 });
496 }
497
498 if let Some(pool) = dispatcher.pool_for_checkpoint() {
499 let cfg = CheckpointConfig::from_env();
500 tokio::spawn(run_checkpoint_task(pool, cfg));
501 tracing::info!("WAL checkpoint task started");
502 }
503
504 let active = Arc::new(std::sync::atomic::AtomicUsize::new(0));
505
506 let shutdown = async {
507 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
511 .expect("install SIGTERM handler");
512 let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
513 .expect("install SIGINT handler");
514 tokio::select! {
515 _ = sigterm.recv() => tracing::info!("received SIGTERM"),
516 _ = sigint.recv() => tracing::info!("received SIGINT"),
517 }
518 };
519
520 tokio::select! {
521 _ = async {
522 loop {
523 match listener.accept().await {
524 Ok((stream, _)) => {
525 let d = dispatcher.clone();
526 let active = Arc::clone(&active);
527 tokio::spawn(async move {
528 active.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
529 handle_conn(stream, d).await;
530 active.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
531 });
532 }
533 Err(e) => tracing::error!(error = %e, "accept failed"),
534 }
535 }
536 } => {}
537 _ = shutdown => {}
538 }
539
540 drain(&active).await;
541
542 let _ = std::fs::remove_file(&sock);
543 let _ = std::fs::remove_file(&pid_file);
544 tracing::info!("khived stopped");
545 Ok(())
546}
547
548fn is_process_running(pid: u32) -> bool {
551 let Ok(pid) = i32::try_from(pid) else {
552 return false;
553 };
554 if pid <= 0 {
555 return false;
556 }
557 unsafe { libc::kill(pid, 0) == 0 }
559}
560
561async fn cleanup_stale_daemon(sock: &std::path::Path, pid_file: &std::path::Path) -> bool {
562 if let Ok(pid_str) = std::fs::read_to_string(pid_file) {
563 if let Ok(pid) = pid_str.trim().parse::<u32>() {
564 if pid != std::process::id()
565 && is_process_running(pid)
566 && sock.exists()
567 && UnixStream::connect(sock).await.is_ok()
568 {
569 return false;
570 }
571 }
572 }
573 if sock.exists() {
574 if let Err(e) = std::fs::remove_file(sock) {
575 tracing::warn!(error = %e, path = ?sock, "failed to remove stale socket");
576 }
577 }
578 if pid_file.exists() {
579 if let Err(e) = std::fs::remove_file(pid_file) {
580 tracing::warn!(error = %e, path = ?pid_file, "failed to remove stale PID file");
581 }
582 }
583 true
584}
585
586fn write_pid_file(pid_file: &std::path::Path) -> std::io::Result<()> {
587 let mut opts = std::fs::OpenOptions::new();
588 opts.write(true).create(true).truncate(true);
589 #[cfg(unix)]
590 {
591 use std::os::unix::fs::OpenOptionsExt;
592 opts.mode(0o600);
593 }
594 let mut f = opts.open(pid_file)?;
595 f.write_all(std::process::id().to_string().as_bytes())?;
596 Ok(())
597}
598
599async fn drain(active: &std::sync::atomic::AtomicUsize) {
600 use std::sync::atomic::Ordering;
601 if active.load(Ordering::Relaxed) == 0 {
602 return;
603 }
604 let deadline = tokio::time::Instant::now() + drain_timeout();
605 while active.load(Ordering::Relaxed) > 0 {
606 if tokio::time::Instant::now() >= deadline {
607 tracing::warn!(
608 remaining = active.load(Ordering::Relaxed),
609 "drain timeout reached; forcing shutdown"
610 );
611 break;
612 }
613 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
614 }
615}
616
617fn drain_timeout() -> std::time::Duration {
618 let secs = std::env::var("KHIVE_DRAIN_TIMEOUT_SECS")
619 .ok()
620 .and_then(|v| v.parse::<u64>().ok())
621 .unwrap_or(DEFAULT_DRAIN_TIMEOUT_SECS);
622 std::time::Duration::from_secs(secs)
623}
624
625pub fn env_truthy(key: &str) -> bool {
627 std::env::var(key)
628 .map(|v| {
629 let v = v.trim();
630 !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false")
631 })
632 .unwrap_or(false)
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
643 fn current_process_is_running() {
644 let pid = std::process::id();
646 assert!(
647 is_process_running(pid),
648 "current process {pid} should be detected as running"
649 );
650 }
651
652 #[test]
653 fn pid_zero_is_not_running() {
654 assert!(
657 !is_process_running(0),
658 "pid 0 must be rejected by the guard before the unsafe call"
659 );
660 }
661
662 #[test]
663 fn very_large_pid_is_not_running() {
664 assert!(
666 !is_process_running(u32::MAX),
667 "u32::MAX should fail i32 conversion and return false"
668 );
669 }
670
671 #[test]
672 fn env_truthy_recognises_set_values() {
673 assert!(!env_truthy("__KHIVE_TEST_ABSENT_VAR_XYZ__"));
674
675 let key = "__KHIVE_TEST_TRUTHY_ABC__";
679 std::env::set_var(key, "1");
680 assert!(env_truthy(key));
681 std::env::set_var(key, "false");
682 assert!(!env_truthy(key));
683 std::env::set_var(key, "0");
684 assert!(!env_truthy(key));
685 std::env::remove_var(key);
686 }
687}