1use std::fs::{File, OpenOptions};
7use std::os::fd::AsRawFd;
8use std::os::unix::fs::OpenOptionsExt;
9use std::path::Path;
10use std::{fmt, io};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Sharing {
15 Serve,
19 Alone,
21 Beside,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Work {
28 Serve,
29 TenantCreate,
30 TenantList,
31 TenantSetRetention,
32 TenantRemove,
33 KeyCreate,
34 KeyList,
35 KeyRevoke,
36 RekeyTenant,
37 RekeyMaster,
38 AuditExport,
39 Check,
40 Backup,
41}
42
43impl Work {
44 pub fn sharing(self) -> Sharing {
46 match self {
47 Work::Serve => Sharing::Serve,
48 Work::TenantCreate
49 | Work::TenantRemove
50 | Work::RekeyTenant
51 | Work::RekeyMaster
52 | Work::Check => Sharing::Alone,
53 Work::TenantList
54 | Work::TenantSetRetention
55 | Work::KeyCreate
56 | Work::KeyList
57 | Work::KeyRevoke
58 | Work::AuditExport
59 | Work::Backup => Sharing::Beside,
60 }
61 }
62}
63
64#[derive(Debug)]
67pub struct DataDirLock {
68 _held: Vec<File>,
69}
70
71#[derive(Debug)]
73pub enum LockError {
74 InUse(Sharing),
76 Io(io::Error),
78}
79
80impl fmt::Display for LockError {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 LockError::InUse(Sharing::Serve) => f.write_str(
84 "the data directory is in use by another vcon-server or a command that runs alone",
85 ),
86 LockError::InUse(Sharing::Alone) => f.write_str(
87 "the data directory is in use; stop the server and any other command first",
88 ),
89 LockError::InUse(Sharing::Beside) => {
90 f.write_str("the data directory is in use by a command that runs alone")
91 }
92 LockError::Io(e) => write!(f, "the data directory cannot be locked: {e}"),
93 }
94 }
95}
96
97pub fn lock(dir: &Path, sharing: Sharing) -> Result<DataDirLock, LockError> {
104 let take = |name: &str, how| {
105 flock(&dir.join(name), how).map_err(|e| match e.kind() {
106 io::ErrorKind::WouldBlock => LockError::InUse(sharing),
107 _ => LockError::Io(e),
108 })
109 };
110 let held = match sharing {
111 Sharing::Serve => vec![take(SERVERS, libc::LOCK_EX)?],
112 Sharing::Alone => vec![
113 take(SERVERS, libc::LOCK_EX)?,
114 take(COMMANDS, libc::LOCK_EX)?,
115 ],
116 Sharing::Beside => vec![take(COMMANDS, libc::LOCK_SH)?],
117 };
118 Ok(DataDirLock { _held: held })
119}
120
121const SERVERS: &str = "lock";
122const COMMANDS: &str = "lock.commands";
123
124fn flock(path: &Path, how: libc::c_int) -> io::Result<File> {
126 let file = OpenOptions::new()
127 .create(true)
128 .truncate(false)
129 .write(true)
130 .mode(crate::fs::FILE_MODE)
131 .open(path)?;
132 if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
135 return Err(io::Error::last_os_error());
136 }
137 Ok(file)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
150 fn every_kind_of_work_shares_the_directory_as_documented() {
151 let documented = |work: Work| match work {
152 Work::Serve => Sharing::Serve,
153 Work::TenantCreate => Sharing::Alone,
154 Work::TenantList => Sharing::Beside,
155 Work::TenantSetRetention => Sharing::Beside,
156 Work::TenantRemove => Sharing::Alone,
157 Work::KeyCreate => Sharing::Beside,
158 Work::KeyList => Sharing::Beside,
159 Work::KeyRevoke => Sharing::Beside,
160 Work::RekeyTenant => Sharing::Alone,
161 Work::RekeyMaster => Sharing::Alone,
162 Work::AuditExport => Sharing::Beside,
163 Work::Check => Sharing::Alone,
164 Work::Backup => Sharing::Beside,
165 };
166 for work in [
167 Work::Serve,
168 Work::TenantCreate,
169 Work::TenantList,
170 Work::TenantSetRetention,
171 Work::TenantRemove,
172 Work::KeyCreate,
173 Work::KeyList,
174 Work::KeyRevoke,
175 Work::RekeyTenant,
176 Work::RekeyMaster,
177 Work::AuditExport,
178 Work::Check,
179 Work::Backup,
180 ] {
181 assert_eq!(work.sharing(), documented(work), "{work:?}");
182 }
183 }
184
185 #[test]
188 fn who_may_run_beside_whom() {
189 use Sharing::{Alone, Beside, Serve};
190 for (first, second, allowed) in [
191 (Serve, Serve, false),
192 (Serve, Alone, false),
193 (Serve, Beside, true),
194 (Alone, Serve, false),
195 (Alone, Alone, false),
196 (Alone, Beside, false),
197 (Beside, Serve, true),
198 (Beside, Alone, false),
199 (Beside, Beside, true),
200 ] {
201 let dir = tempfile::tempdir().expect("temp dir");
202 let held = lock(dir.path(), first).expect("the first lock");
203 match lock(dir.path(), second) {
204 Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
205 Err(LockError::InUse(asked)) => {
206 assert!(!allowed, "{second:?} refused beside {first:?}");
207 assert_eq!(asked, second);
208 }
209 Err(e) => panic!("{first:?} then {second:?}: {e}"),
210 }
211 drop(held);
212 assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
213 }
214 }
215
216 #[test]
219 fn a_refused_lock_holds_nothing() {
220 let dir = tempfile::tempdir().expect("temp dir");
221 let beside = lock(dir.path(), Sharing::Beside).expect("beside");
222 assert!(lock(dir.path(), Sharing::Alone).is_err());
223 let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
224 drop((beside, server));
225 assert!(lock(dir.path(), Sharing::Alone).is_ok());
226 }
227
228 #[test]
229 fn each_refusal_says_what_holds_the_directory() {
230 for (sharing, words) in [
231 (Sharing::Serve, "another vcon-server or a command"),
232 (Sharing::Alone, "stop the server"),
233 (Sharing::Beside, "runs alone"),
234 ] {
235 let text = LockError::InUse(sharing).to_string();
236 assert!(text.contains(words), "{sharing:?}: {text}");
237 }
238 }
239
240 #[test]
241 fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
242 let missing = Path::new("/nonexistent/vcon-server-data");
243 for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
244 assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
245 }
246 }
247}