1 | use crate::error::Result; |
2 | use once_cell::sync::OnceCell; |
3 | use std::fs::{self, File, OpenOptions}; |
4 | use std::io; |
5 | use std::path::{Path, PathBuf}; |
6 | use std::sync::atomic::{AtomicBool, Ordering}; |
7 | use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; |
8 | use std::thread; |
9 | use std::time::{Duration, SystemTime}; |
10 | |
11 | static LOCK: OnceCell<Mutex<()>> = OnceCell::new(); |
12 | |
13 | pub struct Lock { |
14 | intraprocess_guard: Guard, |
15 | lockfile: FileLock, |
16 | } |
17 | |
18 | // High-quality lock to coordinate different #[test] functions within the *same* |
19 | // integration test crate. |
20 | enum Guard { |
21 | NotLocked, |
22 | Locked(MutexGuard<'static, ()>), |
23 | } |
24 | |
25 | // Best-effort filesystem lock to coordinate different #[test] functions across |
26 | // *different* integration tests. |
27 | enum FileLock { |
28 | NotLocked, |
29 | Locked { |
30 | path: PathBuf, |
31 | done: Arc<AtomicBool>, |
32 | }, |
33 | } |
34 | |
35 | impl Lock { |
36 | pub fn acquire(path: impl AsRef<Path>) -> Result<Self> { |
37 | Ok(Lock { |
38 | intraprocess_guard: Guard::acquire(), |
39 | lockfile: FileLock::acquire(path)?, |
40 | }) |
41 | } |
42 | } |
43 | |
44 | impl Guard { |
45 | fn acquire() -> Self { |
46 | Guard::Locked( |
47 | LOCK.get_or_init(|| Mutex::new(())) |
48 | .lock() |
49 | .unwrap_or_else(PoisonError::into_inner), |
50 | ) |
51 | } |
52 | } |
53 | |
54 | impl FileLock { |
55 | fn acquire(path: impl AsRef<Path>) -> Result<Self> { |
56 | let path = path.as_ref().to_owned(); |
57 | let lockfile = match create(&path) { |
58 | None => return Ok(FileLock::NotLocked), |
59 | Some(lockfile) => lockfile, |
60 | }; |
61 | let done = Arc::new(AtomicBool::new(false)); |
62 | let thread = thread::Builder::new().name("trybuild-flock" .to_owned()); |
63 | thread.spawn({ |
64 | let done = Arc::clone(&done); |
65 | move || poll(lockfile, done) |
66 | })?; |
67 | Ok(FileLock::Locked { path, done }) |
68 | } |
69 | } |
70 | |
71 | impl Drop for Lock { |
72 | fn drop(&mut self) { |
73 | let Lock { |
74 | intraprocess_guard, |
75 | lockfile, |
76 | } = self; |
77 | // Unlock file lock first. |
78 | *lockfile = FileLock::NotLocked; |
79 | *intraprocess_guard = Guard::NotLocked; |
80 | } |
81 | } |
82 | |
83 | impl Drop for FileLock { |
84 | fn drop(&mut self) { |
85 | match self { |
86 | FileLock::NotLocked => {} |
87 | FileLock::Locked { path, done } => { |
88 | done.store(true, Ordering::Release); |
89 | let _ = fs::remove_file(path); |
90 | } |
91 | } |
92 | } |
93 | } |
94 | |
95 | fn create(path: &Path) -> Option<File> { |
96 | loop { |
97 | match OpenOptions::new().write(true).create_new(true).open(path) { |
98 | // Acquired lock by creating lockfile. |
99 | Ok(lockfile) => return Some(lockfile), |
100 | Err(io_error) => match io_error.kind() { |
101 | // Lock is already held by another test. |
102 | io::ErrorKind::AlreadyExists => {} |
103 | // File based locking isn't going to work for some reason. |
104 | _ => return None, |
105 | }, |
106 | } |
107 | |
108 | // Check whether it's okay to bust the lock. |
109 | let metadata = match fs::metadata(path) { |
110 | Ok(metadata) => metadata, |
111 | Err(io_error) => match io_error.kind() { |
112 | // Other holder of the lock finished. Retry. |
113 | io::ErrorKind::NotFound => continue, |
114 | _ => return None, |
115 | }, |
116 | }; |
117 | |
118 | let modified = match metadata.modified() { |
119 | Ok(modified) => modified, |
120 | Err(_) => return None, |
121 | }; |
122 | |
123 | let now = SystemTime::now(); |
124 | let considered_stale = now - Duration::from_millis(1500); |
125 | let considered_future = now + Duration::from_millis(1500); |
126 | if modified < considered_stale || considered_future < modified { |
127 | return File::create(path).ok(); |
128 | } |
129 | |
130 | // Try again shortly. |
131 | thread::sleep(Duration::from_millis(500)); |
132 | } |
133 | } |
134 | |
135 | // Bump mtime periodically while test directory is in use. |
136 | fn poll(lockfile: File, done: Arc<AtomicBool>) { |
137 | loop { |
138 | thread::sleep(Duration::from_millis(500)); |
139 | if done.load(Ordering::Acquire) || lockfile.set_len(0).is_err() { |
140 | return; |
141 | } |
142 | } |
143 | } |
144 | |