bitwarden_access_connector/
instance_lock.rs1use uuid::Uuid;
10
11pub struct InstanceLock {
14 _guard: platform::Guard,
15}
16
17#[derive(Debug, thiserror::Error)]
19pub enum InstanceLockError {
20 #[error("another bwac process is already running with this access connector token")]
22 AlreadyRunning,
23 #[error("could not check for another bwac process: {0}")]
25 Unavailable(std::io::Error),
26}
27
28pub fn acquire(api_key_id: Uuid) -> Result<InstanceLock, InstanceLockError> {
30 platform::acquire(&lock_name(api_key_id)).map(|guard| InstanceLock { _guard: guard })
31}
32
33fn lock_name(api_key_id: Uuid) -> String {
34 format!("bwac-{api_key_id}")
35}
36
37#[cfg(any(windows, target_os = "macos"))]
40mod platform {
41 use std::{
42 fs::{File, OpenOptions, TryLockError},
43 io,
44 path::PathBuf,
45 };
46
47 use super::InstanceLockError;
48
49 pub(super) struct Guard {
50 _file: File,
51 }
52
53 pub(super) fn acquire(name: &str) -> Result<Guard, InstanceLockError> {
54 let path = path(name).map_err(InstanceLockError::Unavailable)?;
55 let mut options = OpenOptions::new();
56 #[cfg(target_os = "macos")]
57 {
58 use std::os::unix::fs::OpenOptionsExt;
59 const O_NOFOLLOW: i32 = 0x100;
62 options.custom_flags(O_NOFOLLOW);
63 }
64
65 let opened = match options.clone().write(true).create(true).open(&path) {
67 Err(e) if e.kind() == io::ErrorKind::PermissionDenied => options.read(true).open(&path),
68 result => result,
69 };
70 let file = opened.map_err(InstanceLockError::Unavailable)?;
71 match file.try_lock() {
72 Ok(()) => Ok(Guard { _file: file }),
73 Err(TryLockError::WouldBlock) => Err(InstanceLockError::AlreadyRunning),
74 Err(TryLockError::Error(e)) => Err(InstanceLockError::Unavailable(e)),
75 }
76 }
77
78 #[cfg(windows)]
80 pub(super) fn path(name: &str) -> io::Result<PathBuf> {
81 let dir = std::env::var_os("ProgramData")
82 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "ProgramData is not set"))?;
83 Ok(PathBuf::from(dir).join(format!("{name}.lock")))
84 }
85
86 #[cfg(target_os = "macos")]
88 pub(super) fn path(name: &str) -> io::Result<PathBuf> {
89 Ok(PathBuf::from("/var/tmp").join(format!("{name}.lock")))
90 }
91}
92
93#[cfg(target_os = "linux")]
97mod platform {
98 use std::{
99 io,
100 os::{
101 linux::net::SocketAddrExt,
102 unix::net::{SocketAddr, UnixListener},
103 },
104 };
105
106 use super::InstanceLockError;
107
108 pub(super) struct Guard {
109 _listener: UnixListener,
110 }
111
112 pub(super) fn acquire(name: &str) -> Result<Guard, InstanceLockError> {
113 let addr = SocketAddr::from_abstract_name(name).map_err(InstanceLockError::Unavailable)?;
114 match UnixListener::bind_addr(&addr) {
115 Ok(listener) => Ok(Guard {
116 _listener: listener,
117 }),
118 Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
119 Err(InstanceLockError::AlreadyRunning)
120 }
121 Err(e) => Err(InstanceLockError::Unavailable(e)),
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn second_lock_for_the_same_token_is_refused() {
132 let id = Uuid::new_v4();
133 let held = acquire(id).expect("first lock");
134
135 assert!(matches!(
136 acquire(id),
137 Err(InstanceLockError::AlreadyRunning)
138 ));
139 drop(held);
140 remove_lock_file(id);
141 }
142
143 #[test]
144 fn different_tokens_do_not_conflict() {
145 let (a, b) = (Uuid::new_v4(), Uuid::new_v4());
146 let first = acquire(a).expect("first lock");
147 let second = acquire(b).expect("second lock");
148
149 drop((first, second));
150 remove_lock_file(a);
151 remove_lock_file(b);
152 }
153
154 #[test]
155 fn dropping_the_lock_releases_it() {
156 let id = Uuid::new_v4();
157 drop(acquire(id).expect("first lock"));
158
159 drop(acquire(id).expect("lock after release"));
160 remove_lock_file(id);
161 }
162
163 #[cfg(target_os = "macos")]
164 #[test]
165 fn a_symlink_at_the_lock_path_is_not_followed() {
166 let id = Uuid::new_v4();
167 let target = std::env::temp_dir().join(format!("bwac-symlink-target-{id}"));
168 let link = platform::path(&lock_name(id)).expect("lock path");
169 std::os::unix::fs::symlink(&target, &link).expect("plant symlink");
170
171 let result = acquire(id);
172
173 let created = target.exists();
174 let _ = std::fs::remove_file(&link);
175 let _ = std::fs::remove_file(&target);
176 assert!(matches!(result, Err(InstanceLockError::Unavailable(_))));
177 assert!(!created);
178 }
179
180 #[cfg(any(windows, target_os = "macos"))]
181 fn remove_lock_file(id: Uuid) {
182 if let Ok(path) = platform::path(&lock_name(id)) {
183 let _ = std::fs::remove_file(path);
184 }
185 }
186
187 #[cfg(target_os = "linux")]
188 fn remove_lock_file(_: Uuid) {}
189}