Skip to main content

kimetsu_brain/
lock.rs

1use std::fs::{self, OpenOptions};
2use std::io::Write;
3use std::path::PathBuf;
4
5use kimetsu_core::KimetsuResult;
6use kimetsu_core::ids::RunId;
7use kimetsu_core::paths::ProjectPaths;
8use serde::Serialize;
9use time::OffsetDateTime;
10
11pub struct ProjectLock {
12    path: PathBuf,
13    active: bool,
14}
15
16#[derive(Debug, Serialize)]
17struct LockPayload {
18    pid: u32,
19    command: String,
20    run_id: Option<String>,
21    #[serde(with = "time::serde::rfc3339")]
22    started_at: OffsetDateTime,
23}
24
25impl ProjectLock {
26    pub fn acquire(
27        paths: &ProjectPaths,
28        command: impl Into<String>,
29        run_id: Option<RunId>,
30    ) -> KimetsuResult<Self> {
31        fs::create_dir_all(&paths.kimetsu_dir)?;
32        let payload = LockPayload {
33            pid: std::process::id(),
34            command: command.into(),
35            run_id: run_id.map(|id| id.to_string()),
36            started_at: OffsetDateTime::now_utc(),
37        };
38        let payload = serde_json::to_string_pretty(&payload)?;
39
40        let mut file = match OpenOptions::new()
41            .write(true)
42            .create_new(true)
43            .open(&paths.lock_file)
44        {
45            Ok(file) => file,
46            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
47                let existing =
48                    fs::read_to_string(&paths.lock_file).unwrap_or_else(|_| "<unreadable>".into());
49                return Err(format!(
50                    "project writer lock is already held at {}\n{}",
51                    paths.lock_file.display(),
52                    existing
53                )
54                .into());
55            }
56            Err(err) => return Err(err.into()),
57        };
58
59        file.write_all(payload.as_bytes())?;
60        file.sync_all()?;
61
62        Ok(Self {
63            path: paths.lock_file.clone(),
64            active: true,
65        })
66    }
67
68    pub fn release(mut self) -> KimetsuResult<()> {
69        self.active = false;
70        match fs::remove_file(&self.path) {
71            Ok(()) => Ok(()),
72            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
73            Err(err) => Err(err.into()),
74        }
75    }
76}
77
78impl Drop for ProjectLock {
79    fn drop(&mut self) {
80        if self.active {
81            let _ = fs::remove_file(&self.path);
82        }
83    }
84}
85
86pub fn clear_force(paths: &ProjectPaths) -> KimetsuResult<bool> {
87    match fs::remove_file(&paths.lock_file) {
88        Ok(()) => Ok(true),
89        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
90        Err(err) => Err(err.into()),
91    }
92}