1use crate::util::paths;
7use crate::util::{Error, Result};
8use std::fs::{File, OpenOptions};
9use std::io::Write;
10use std::os::unix::fs::OpenOptionsExt;
11use std::path::PathBuf;
12
13pub struct InstanceLock {
15 _file: File,
16 path: PathBuf,
17}
18
19impl InstanceLock {
20 pub fn acquire() -> Result<Self> {
24 let path = Self::lock_path();
25
26 if let Some(parent) = path.parent() {
28 std::fs::create_dir_all(parent).ok();
29 }
30
31 let file = OpenOptions::new()
34 .read(true)
35 .write(true)
36 .create(true)
37 .truncate(false)
38 .mode(0o600)
39 .open(&path)
40 .map_err(|e| Error::Other(format!("Failed to open lock file: {}", e)))?;
41
42 use std::os::unix::io::AsRawFd;
44 let fd = file.as_raw_fd();
45 let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
46
47 if result != 0 {
48 return Err(Error::Other(
50 "Another instance is already running".to_string(),
51 ));
52 }
53
54 let mut file = file;
56 file.set_len(0)
57 .map_err(|e| Error::Other(format!("Failed to truncate lock file: {}", e)))?;
58 use std::io::Seek;
59 file.seek(std::io::SeekFrom::Start(0))
60 .map_err(|e| Error::Other(format!("Failed to seek lock file: {}", e)))?;
61 writeln!(file, "{}", std::process::id())
62 .map_err(|e| Error::Other(format!("Failed to write PID: {}", e)))?;
63 file.flush()
64 .map_err(|e| Error::Other(format!("Failed to flush PID: {}", e)))?;
65
66 tracing::debug!(
67 "Lock acquired, PID {} written to {}",
68 std::process::id(),
69 path.display()
70 );
71
72 Ok(Self { _file: file, path })
73 }
74
75 fn lock_path() -> PathBuf {
80 match paths::lock_file() {
82 Ok(path) => path,
83 Err(e) => {
84 tracing::error!(
87 "Failed to get secure lock path: {}. Using UID-based fallback.",
88 e
89 );
90 let uid = unsafe { libc::getuid() };
91 PathBuf::from(format!("/run/user/{}/open-sesame.lock", uid))
92 }
93 }
94 }
95}
96
97impl Drop for InstanceLock {
98 fn drop(&mut self) {
99 std::fs::remove_file(&self.path).ok();
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_lock_path_returns_valid_path() {
111 let path = InstanceLock::lock_path();
112 assert!(
114 path.to_string_lossy().contains("open-sesame"),
115 "Lock path should contain 'open-sesame': {:?}",
116 path
117 );
118 let filename = path
120 .file_name()
121 .expect("lock path should have a filename component")
122 .to_string_lossy();
123 assert!(
124 filename.contains("lock"),
125 "Lock file should have 'lock' in name: {:?}",
126 path
127 );
128 }
129
130 #[test]
131 fn test_instance_lock_acquire_and_release() {
132 let lock = InstanceLock::acquire();
137
138 if let Ok(_lock) = lock {
140 let lock2 = InstanceLock::acquire();
143 assert!(lock2.is_err(), "Double lock acquisition prevented");
144
145 }
147 }
149}