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}