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}
41
42impl Work {
43 pub fn sharing(self) -> Sharing {
45 match self {
46 Work::Serve => Sharing::Serve,
47 Work::TenantCreate
48 | Work::TenantRemove
49 | Work::RekeyTenant
50 | Work::RekeyMaster
51 | Work::Check => Sharing::Alone,
52 Work::TenantList
53 | Work::TenantSetRetention
54 | Work::KeyCreate
55 | Work::KeyList
56 | Work::KeyRevoke
57 | Work::AuditExport => Sharing::Beside,
58 }
59 }
60}
61
62#[derive(Debug)]
65pub struct DataDirLock {
66 _held: Vec<File>,
67}
68
69#[derive(Debug)]
71pub enum LockError {
72 InUse(Sharing),
74 Io(io::Error),
76}
77
78impl fmt::Display for LockError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 LockError::InUse(Sharing::Serve) => f.write_str(
82 "the data directory is in use by another vcon-server or a command that runs alone",
83 ),
84 LockError::InUse(Sharing::Alone) => f.write_str(
85 "the data directory is in use; stop the server and any other command first",
86 ),
87 LockError::InUse(Sharing::Beside) => {
88 f.write_str("the data directory is in use by a command that runs alone")
89 }
90 LockError::Io(e) => write!(f, "the data directory cannot be locked: {e}"),
91 }
92 }
93}
94
95pub fn lock(dir: &Path, sharing: Sharing) -> Result<DataDirLock, LockError> {
102 let take = |name: &str, how| {
103 flock(&dir.join(name), how).map_err(|e| match e.kind() {
104 io::ErrorKind::WouldBlock => LockError::InUse(sharing),
105 _ => LockError::Io(e),
106 })
107 };
108 let held = match sharing {
109 Sharing::Serve => vec![take(SERVERS, libc::LOCK_EX)?],
110 Sharing::Alone => vec![
111 take(SERVERS, libc::LOCK_EX)?,
112 take(COMMANDS, libc::LOCK_EX)?,
113 ],
114 Sharing::Beside => vec![take(COMMANDS, libc::LOCK_SH)?],
115 };
116 Ok(DataDirLock { _held: held })
117}
118
119const SERVERS: &str = "lock";
120const COMMANDS: &str = "lock.commands";
121
122fn flock(path: &Path, how: libc::c_int) -> io::Result<File> {
124 let file = OpenOptions::new()
125 .create(true)
126 .truncate(false)
127 .write(true)
128 .open(path)?;
129 if unsafe { libc::flock(file.as_raw_fd(), how | libc::LOCK_NB) } != 0 {
132 return Err(io::Error::last_os_error());
133 }
134 Ok(file)
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn rekey_check_and_tenant_create_and_remove_run_alone() {
143 for (work, sharing) in [
144 (Work::Serve, Sharing::Serve),
145 (Work::TenantCreate, Sharing::Alone),
146 (Work::TenantList, Sharing::Beside),
147 (Work::TenantSetRetention, Sharing::Beside),
148 (Work::TenantRemove, Sharing::Alone),
149 (Work::KeyCreate, Sharing::Beside),
150 (Work::KeyList, Sharing::Beside),
151 (Work::KeyRevoke, Sharing::Beside),
152 (Work::RekeyTenant, Sharing::Alone),
153 (Work::RekeyMaster, Sharing::Alone),
154 (Work::AuditExport, Sharing::Beside),
155 (Work::Check, Sharing::Alone),
156 ] {
157 assert_eq!(work.sharing(), sharing, "{work:?}");
158 }
159 }
160
161 #[test]
164 fn who_may_run_beside_whom() {
165 use Sharing::{Alone, Beside, Serve};
166 for (first, second, allowed) in [
167 (Serve, Serve, false),
168 (Serve, Alone, false),
169 (Serve, Beside, true),
170 (Alone, Serve, false),
171 (Alone, Alone, false),
172 (Alone, Beside, false),
173 (Beside, Serve, true),
174 (Beside, Alone, false),
175 (Beside, Beside, true),
176 ] {
177 let dir = tempfile::tempdir().expect("temp dir");
178 let held = lock(dir.path(), first).expect("the first lock");
179 match lock(dir.path(), second) {
180 Ok(_) => assert!(allowed, "{second:?} beside {first:?}"),
181 Err(LockError::InUse(asked)) => {
182 assert!(!allowed, "{second:?} refused beside {first:?}");
183 assert_eq!(asked, second);
184 }
185 Err(e) => panic!("{first:?} then {second:?}: {e}"),
186 }
187 drop(held);
188 assert!(lock(dir.path(), second).is_ok(), "{second:?} once free");
189 }
190 }
191
192 #[test]
195 fn a_refused_lock_holds_nothing() {
196 let dir = tempfile::tempdir().expect("temp dir");
197 let beside = lock(dir.path(), Sharing::Beside).expect("beside");
198 assert!(lock(dir.path(), Sharing::Alone).is_err());
199 let server = lock(dir.path(), Sharing::Serve).expect("serve beside");
200 drop((beside, server));
201 assert!(lock(dir.path(), Sharing::Alone).is_ok());
202 }
203
204 #[test]
205 fn each_refusal_says_what_holds_the_directory() {
206 for (sharing, words) in [
207 (Sharing::Serve, "another vcon-server or a command"),
208 (Sharing::Alone, "stop the server"),
209 (Sharing::Beside, "runs alone"),
210 ] {
211 let text = LockError::InUse(sharing).to_string();
212 assert!(text.contains(words), "{sharing:?}: {text}");
213 }
214 }
215
216 #[test]
217 fn a_data_directory_that_cannot_hold_a_lock_file_is_an_error() {
218 let missing = Path::new("/nonexistent/vcon-server-data");
219 for sharing in [Sharing::Serve, Sharing::Alone, Sharing::Beside] {
220 assert!(matches!(lock(missing, sharing), Err(LockError::Io(_))));
221 }
222 }
223}