khive_runtime/daemon/
process_files.rs1use super::*;
2
3#[cfg(unix)]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub(super) enum PidLiveness {
9 Alive,
12 Dead,
14 PermissionDenied,
19}
20
21#[cfg(unix)]
22impl PidLiveness {
23 pub(super) fn is_running(self) -> bool {
24 !matches!(self, PidLiveness::Dead)
25 }
26}
27
28#[cfg(unix)]
32pub(super) fn classify_kill_result(rc: i32, errno: i32) -> PidLiveness {
33 if rc == 0 {
34 return PidLiveness::Alive;
35 }
36 match errno {
37 libc::EPERM => PidLiveness::PermissionDenied,
38 _ => PidLiveness::Dead,
39 }
40}
41
42#[cfg(unix)]
43pub(super) fn is_process_running(pid: u32) -> bool {
44 let Ok(pid) = i32::try_from(pid) else {
45 return false;
46 };
47 if pid <= 0 {
48 return false;
49 }
50 let rc = unsafe { libc::kill(pid, 0) };
52 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
53 classify_kill_result(rc, errno).is_running()
54}
55
56#[cfg(unix)]
63pub(super) fn pid_can_name_incumbent(
64 pid: u32,
65 current_pid: u32,
66 allow_same_process_incumbent: bool,
67) -> bool {
68 allow_same_process_incumbent || pid != current_pid
69}
70
71#[cfg(unix)]
76pub(super) const DUPLICATE_PROBE_TIMEOUT: std::time::Duration =
77 std::time::Duration::from_millis(500);
78
79#[cfg(unix)]
110pub(super) async fn socket_speaks_khived_protocol(
111 sock: &std::path::Path,
112 expected_config_id: &str,
113) -> bool {
114 let probe = DaemonRequestFrame {
115 probe_only: true,
116 protocol_version: PROTOCOL_VERSION,
117 config_id: expected_config_id.to_string(),
118 ..Default::default()
119 };
120 let Ok(payload) = serde_json::to_vec(&probe) else {
121 return false;
122 };
123 let response = tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, async {
124 let mut stream = UnixStream::connect(sock).await.ok()?;
125 write_frame(&mut stream, &payload).await.ok()?;
126 let raw = read_frame(&mut stream).await.ok()?;
127 serde_json::from_slice::<DaemonResponseFrame>(&raw).ok()
128 })
129 .await
130 .ok()
131 .flatten();
132
133 let Some(resp) = response else {
134 return false;
135 };
136 let is_probe_ack = resp.ok
137 && resp.result.is_none()
138 && resp.error.is_none()
139 && resp.metrics.is_none()
140 && resp.request_id.is_none();
141 is_probe_ack
142 && !resp.version_mismatch
143 && !resp.namespace_mismatch
144 && !resp.config_mismatch
145 && resp.daemon_protocol_version == PROTOCOL_VERSION
146 && resp
147 .served_config_id
148 .as_deref()
149 .is_some_and(|served| config_ids_compatible(expected_config_id, served))
150}
151
152#[cfg(unix)]
155pub(super) async fn socket_is_unreachable(sock: &std::path::Path) -> bool {
156 match tokio::time::timeout(DUPLICATE_PROBE_TIMEOUT, UnixStream::connect(sock)).await {
157 Ok(Err(error)) => matches!(
158 error.kind(),
159 std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
160 ),
161 _ => false,
162 }
163}
164
165#[cfg(unix)]
170pub(super) enum Incumbent {
171 Serving(u32),
173 Live(u32),
175 Stale,
177}
178
179#[cfg(unix)]
186pub(super) async fn cleanup_stale_daemon(
187 sock: &std::path::Path,
188 pid_file: &std::path::Path,
189 allow_same_process_incumbent: bool,
190 expected_config_id: &str,
191) -> Incumbent {
192 let mut stale_pid_file_guard = None;
193 if let Ok(pid_str) = std::fs::read_to_string(pid_file) {
194 if let Ok(pid) = pid_str.trim().parse::<u32>() {
195 if pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
196 && is_process_running(pid)
197 {
198 if sock.exists() && socket_speaks_khived_protocol(sock, expected_config_id).await {
199 return Incumbent::Serving(pid);
200 }
201 if sock.exists() && !socket_is_unreachable(sock).await {
202 return Incumbent::Live(pid);
203 }
204 match try_acquire_pid_file_lock(pid_file) {
205 Ok(Some(guard)) => stale_pid_file_guard = Some(guard),
206 Ok(None) => return Incumbent::Live(pid),
207 Err(e) => {
208 tracing::warn!(
209 error = %e,
210 path = ?pid_file,
211 "cannot check daemon PID-file lock"
212 );
213 return Incumbent::Live(pid);
214 }
215 }
216 }
217 }
218 }
219 if sock.exists() {
220 if let Err(e) = std::fs::remove_file(sock) {
221 tracing::warn!(error = %e, path = ?sock, "failed to remove stale socket");
222 }
223 }
224 if pid_file.exists() {
225 if let Err(e) = std::fs::remove_file(pid_file) {
226 tracing::warn!(error = %e, path = ?pid_file, "failed to remove stale PID file");
227 }
228 }
229 drop(stale_pid_file_guard);
230 Incumbent::Stale
231}
232
233#[cfg(unix)]
240pub(super) fn write_pid_file_exclusive(
241 pid_file: &std::path::Path,
242) -> std::io::Result<std::fs::File> {
243 use std::os::unix::fs::OpenOptionsExt;
244
245 let mut opts = std::fs::OpenOptions::new();
246 opts.write(true).create_new(true).mode(0o600);
247 let mut f = opts.open(pid_file)?;
248 let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
250 if rc != 0 {
251 return Err(std::io::Error::last_os_error());
252 }
253 f.write_all(std::process::id().to_string().as_bytes())?;
254 Ok(f)
255}
256
257#[cfg(unix)]
260pub(super) fn try_acquire_pid_file_lock(
261 pid_file: &std::path::Path,
262) -> std::io::Result<Option<std::fs::File>> {
263 let file = std::fs::OpenOptions::new()
264 .read(true)
265 .write(true)
266 .open(pid_file)?;
267 let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
269 if rc == 0 {
270 return Ok(Some(file));
271 }
272 let error = std::io::Error::last_os_error();
273 if error.kind() == std::io::ErrorKind::WouldBlock
274 || error.raw_os_error() == Some(libc::EWOULDBLOCK)
275 {
276 Ok(None)
277 } else {
278 Err(error)
279 }
280}
281
282#[cfg(unix)]
285pub(super) fn remove_pid_file_if_owned(pid_file: &std::path::Path, guard: &std::fs::File) {
286 let Ok(owned) = guard.metadata() else {
287 return;
288 };
289 let Ok(current) = std::fs::metadata(pid_file) else {
290 return;
291 };
292 if owned.dev() == current.dev() && owned.ino() == current.ino() {
293 if let Err(e) = std::fs::remove_file(pid_file) {
294 tracing::warn!(error = %e, path = ?pid_file, "failed to remove unbound PID file");
295 }
296 }
297}
298
299#[cfg(unix)]
305pub(super) async fn pid_file_names_a_reachable_daemon(
306 pid_file: &std::path::Path,
307 sock: &std::path::Path,
308 allow_same_process_incumbent: bool,
309 expected_config_id: &str,
310) -> bool {
311 let Ok(pid_str) = std::fs::read_to_string(pid_file) else {
312 return false;
313 };
314 let Ok(pid) = pid_str.trim().parse::<u32>() else {
315 return false;
316 };
317 pid_can_name_incumbent(pid, std::process::id(), allow_same_process_incumbent)
318 && is_process_running(pid)
319 && sock.exists()
320 && socket_speaks_khived_protocol(sock, expected_config_id).await
321}
322
323#[cfg(unix)]
324pub(super) async fn drain(active: &std::sync::atomic::AtomicUsize) -> bool {
325 drain_with_timeout(active, drain_timeout()).await
326}
327
328#[cfg(unix)]
330pub(super) async fn drain_for_idle(
331 active: &std::sync::atomic::AtomicUsize,
332 timeout: std::time::Duration,
333) {
334 let deadline = tokio::time::Instant::now() + timeout;
335 let mut warned = false;
336 while active.load(std::sync::atomic::Ordering::SeqCst) + background_task_count() != 0 {
337 if !warned && tokio::time::Instant::now() >= deadline {
338 tracing::warn!(
339 "idle drain interval elapsed; retaining workers and rendezvous until settled"
340 );
341 warned = true;
342 }
343 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
344 }
345}
346
347#[cfg(unix)]
348pub(super) async fn drain_with_timeout(
349 active: &std::sync::atomic::AtomicUsize,
350 timeout: std::time::Duration,
351) -> bool {
352 use std::sync::atomic::Ordering;
353 let remaining = || active.load(Ordering::SeqCst) + background_task_count();
357 if remaining() == 0 {
358 return true;
359 }
360 let deadline = tokio::time::Instant::now() + timeout;
361 while remaining() > 0 {
362 if tokio::time::Instant::now() >= deadline {
363 tracing::warn!(
364 remaining_connections = active.load(Ordering::SeqCst),
365 remaining_background_tasks = background_task_count(),
366 outstanding_background_tasks = %background_task_names().join(", "),
367 "drain timeout reached; forcing shutdown"
368 );
369 return false;
370 }
371 tokio::select! {
372 _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
373 _ = tokio::time::sleep_until(deadline) => {}
374 }
375 }
376 true
377}
378
379#[cfg(unix)]
380pub(super) async fn finish_connection_tasks(
381 tasks: Vec<tokio::task::JoinHandle<()>>,
382 drained: bool,
383) {
384 if !drained {
385 for task in &tasks {
386 if !task.is_finished() {
387 task.abort();
388 }
389 }
390 }
391 for task in tasks {
392 let _ = task.await;
393 }
394}
395
396pub fn drain_timeout() -> std::time::Duration {
402 let secs = khive_db::env::env_parse_or("KHIVE_DRAIN_TIMEOUT_SECS", DEFAULT_DRAIN_TIMEOUT_SECS);
403 std::time::Duration::from_secs(secs)
404}