Skip to main content

agent_tui_infra/infra/daemon/
file_lock.rs

1//! Daemon lock file helpers.
2
3use std::fs::File;
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::os::unix::io::AsRawFd;
7use std::path::Path;
8
9use crate::common::DaemonError;
10use crate::infra::ipc::current_process_identity;
11
12#[derive(serde::Serialize)]
13struct LockFilePayload {
14    pid: u32,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    process_started_at: Option<u64>,
17}
18
19pub struct LockFile {
20    _file: File,
21}
22
23impl LockFile {
24    pub fn acquire(lock_path: &Path) -> Result<Self, DaemonError> {
25        let lock_file = OpenOptions::new()
26            .write(true)
27            .create(true)
28            .truncate(false)
29            .open(lock_path)
30            .map_err(|e| DaemonError::LockFailed {
31                operation: "open lock file",
32                source: Box::new(e),
33            })?;
34
35        let fd = lock_file.as_raw_fd();
36
37        // SAFETY: `flock` is safe to call with a valid file descriptor obtained from
38        // `as_raw_fd()`. The file is kept open for the lifetime of `LockFile`, ensuring
39        // the fd remains valid. LOCK_EX | LOCK_NB requests an exclusive, non-blocking lock.
40        let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
41        if result != 0 {
42            let err = std::io::Error::last_os_error();
43            match err.raw_os_error() {
44                Some(code) if code == libc::EWOULDBLOCK || code == libc::EAGAIN => {
45                    return Err(DaemonError::AlreadyRunning);
46                }
47                _ => {
48                    return Err(DaemonError::LockFailed {
49                        operation: "flock lock file",
50                        source: Box::new(err),
51                    });
52                }
53            }
54        }
55
56        lock_file.set_len(0).map_err(|e| DaemonError::LockFailed {
57            operation: "truncate lock file",
58            source: Box::new(e),
59        })?;
60
61        let mut lock_file = lock_file;
62        let identity = current_process_identity();
63        serde_json::to_writer_pretty(
64            &mut lock_file,
65            &LockFilePayload {
66                pid: identity.pid,
67                process_started_at: identity.started_at,
68            },
69        )
70        .map_err(|e| DaemonError::LockFailed {
71            operation: "write daemon identity",
72            source: Box::new(e),
73        })?;
74        writeln!(lock_file).map_err(|e| DaemonError::LockFailed {
75            operation: "flush daemon identity",
76            source: Box::new(e),
77        })?;
78
79        Ok(Self { _file: lock_file })
80    }
81}
82
83pub fn remove_lock_file(lock_path: &Path) {
84    if lock_path.exists() {
85        let _ = std::fs::remove_file(lock_path);
86    }
87}
88
89#[cfg(test)]
90#[path = "file_lock_tests.rs"]
91mod tests;