1use std::fmt;
7use std::fs::{File, OpenOptions};
8use std::io;
9use std::os::fd::AsRawFd;
10use std::path::Path;
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 .open(path)?;
131 if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
134 return Err(io::Error::last_os_error());
135 }
136 Ok(file)
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn rekey_check_and_tenant_create_and_remove_run_alone() {
145 for (work, sharing) in [
146 (Work::Serve, Sharing::Serve),
147 (Work::TenantCreate, Sharing::Alone),
148 (Work::TenantList, Sharing::Beside),
149 (Work::TenantSetRetention, Sharing::Beside),
150 (Work::TenantRemove, Sharing::Alone),
151 (Work::KeyCreate, Sharing::Beside),
152 (Work::KeyList, Sharing::Beside),
153 (Work::KeyRevoke, Sharing::Beside),
154 (Work::RekeyTenant, Sharing::Alone),
155 (Work::RekeyMaster, Sharing::Alone),
156 (Work::AuditExport, Sharing::Beside),
157 (Work::Check, Sharing::Alone),
158 ] {
159 assert_eq!(work.sharing(), sharing, "{work:?}");
160 }
161 }
162
163 #[test]
166 fn who_may_run_beside_whom() {
167 use Sharing::{Alone, Beside, Serve};
168 for (first, second, allowed) in [
169 (Serve, Serve, false),
170 (Serve, Alone, false),
171 (Serve, Beside, true),
172 (Alone, Serve, false),
173 (Alone, Alone, false),
174 (Alone, Beside, false),
175 (Beside, Serve, true),
176 (Beside, Alone, false),
177 (Beside, Beside, true),
178 ] {
179 let dir = tempfile::tempdir().expect("temp dir");
180 let held = lock(dir.path(), first).expect("the first lock");
181 match lock(dir.path(), second) {
182 Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
183 Err(LockError::InUse(asked)) => {
184 assert!(!allowed, "{second:?} refused beside {first:?}");
185 assert_eq!(asked, second);
186 }
187 Err(e) => panic!("{first:?} then {second:?}: {e}"),
188 }
189 drop(held);
190 assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
191 }
192 }
193
194 #[test]
197 fn a_refused_lock_holds_nothing() {
198 let dir = tempfile::tempdir().expect("temp dir");
199 let beside = lock(dir.path(), Sharing::Beside).expect("beside");
200 assert!(lock(dir.path(), Sharing::Alone).is_err());
201 let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
202 drop((beside, server));
203 assert!(lock(dir.path(), Sharing::Alone).is_ok());
204 }
205
206 #[test]
207 fn each_refusal_says_what_holds_the_directory() {
208 for (sharing, words) in [
209 (Sharing::Serve, "another vcon-server or a command"),
210 (Sharing::Alone, "stop the server"),
211 (Sharing::Beside, "runs alone"),
212 ] {
213 let text = LockError::InUse(sharing).to_string();
214 assert!(text.contains(words), "{sharing:?}: {text}");
215 }
216 }
217
218 #[test]
219 fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
220 let missing = Path::new("/nonexistent/vcon-server-data");
221 for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
222 assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
223 }
224 }
225}