amont_runtime/hooks/
wait.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CargoLockWhat {
15 BuildDirectory,
16 PackageCache,
17 Other,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum WaitKind {
24 CargoLock(CargoLockWhat),
25 UvLock,
26}
27
28impl WaitKind {
29 pub fn describe(self) -> &'static str {
32 match self {
33 WaitKind::CargoLock(CargoLockWhat::BuildDirectory) => {
34 "the cargo lock on the build directory"
35 }
36 WaitKind::CargoLock(CargoLockWhat::PackageCache) => {
37 "the cargo lock on the package cache"
38 }
39 WaitKind::CargoLock(CargoLockWhat::Other) => "a cargo lock",
40 WaitKind::UvLock => "a uv lock",
41 }
42 }
43
44 pub fn short(self) -> &'static str {
46 match self {
47 WaitKind::CargoLock(_) => "cargo lock",
48 WaitKind::UvLock => "uv lock",
49 }
50 }
51
52 pub fn holders(self) -> &'static str {
54 match self {
55 WaitKind::CargoLock(_) => {
56 "another cargo holds it (a second worktree, rust-analyzer, a build in another session)"
57 }
58 WaitKind::UvLock => "another uv holds it (a second worktree, or an install in another session)",
59 }
60 }
61
62 pub fn code(self) -> u8 {
64 match self {
65 WaitKind::CargoLock(CargoLockWhat::BuildDirectory) => 1,
66 WaitKind::CargoLock(CargoLockWhat::PackageCache) => 2,
67 WaitKind::CargoLock(CargoLockWhat::Other) => 3,
68 WaitKind::UvLock => 4,
69 }
70 }
71
72 pub fn from_code(code: u8) -> Option<WaitKind> {
73 Some(match code {
74 1 => WaitKind::CargoLock(CargoLockWhat::BuildDirectory),
75 2 => WaitKind::CargoLock(CargoLockWhat::PackageCache),
76 3 => WaitKind::CargoLock(CargoLockWhat::Other),
77 4 => WaitKind::UvLock,
78 _ => return None,
79 })
80 }
81}
82
83pub fn strip_csi(line: &str) -> String {
88 let mut out = String::with_capacity(line.len());
89 let mut chars = line.chars().peekable();
90 while let Some(c) = chars.next() {
91 if c == '\u{1b}' && chars.peek() == Some(&'[') {
92 chars.next();
93 for c in chars.by_ref() {
95 if ('@'..='~').contains(&c) {
96 break;
97 }
98 }
99 continue;
100 }
101 out.push(c);
102 }
103 out
104}
105
106pub fn marker(line: &str) -> Option<WaitKind> {
114 let line = line.trim();
115 if let Some(what) = line.strip_prefix("Blocking waiting for file lock on ") {
116 return Some(WaitKind::CargoLock(match what.trim() {
117 "build directory" => CargoLockWhat::BuildDirectory,
118 "package cache" => CargoLockWhat::PackageCache,
119 _ => CargoLockWhat::Other,
120 }));
121 }
122 if line.starts_with("Waiting to acquire ") && line.contains(" lock for `") {
123 return Some(WaitKind::UvLock);
124 }
125 None
126}
127
128pub fn looks_like_wait(line: &str) -> bool {
132 let lower = line.to_ascii_lowercase();
133 [
134 "waiting for file lock",
135 "waiting to acquire",
136 "blocking waiting",
137 "waiting for lock",
138 ]
139 .iter()
140 .any(|phrase| lower.contains(phrase))
141}
142
143pub const MAX_LINE: usize = 4096;
147
148#[derive(Default)]
151pub struct LineFramer {
152 tail: Vec<u8>,
153 overflowed: bool,
156}
157
158impl LineFramer {
159 pub fn feed(&mut self, chunk: &[u8]) -> Vec<String> {
162 let mut lines = Vec::new();
163 for byte in chunk {
164 if *byte == b'\n' {
165 if !self.overflowed {
166 let line = String::from_utf8_lossy(&self.tail).into_owned();
167 lines.push(line.trim_end_matches('\r').to_string());
168 }
169 self.tail.clear();
170 self.overflowed = false;
171 continue;
172 }
173 if self.overflowed {
174 continue;
175 }
176 if self.tail.len() >= MAX_LINE {
177 self.tail.clear();
178 self.overflowed = true;
179 continue;
180 }
181 self.tail.push(*byte);
182 }
183 lines
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 const BUILD_DIR: &str = " Blocking waiting for file lock on build directory";
192 const PACKAGE_CACHE: &str = " Blocking waiting for file lock on package cache";
193 const COLOURED: &str =
196 "\u{1b}[1m\u{1b}[36m Blocking\u{1b}[0m waiting for file lock on build directory";
197 const UV: &str = "Waiting to acquire write lock for `/home/u/.cache/uv/environments-v2`";
198
199 #[test]
200 fn the_real_cargo_and_uv_lines_are_markers() {
201 assert_eq!(
202 marker(BUILD_DIR),
203 Some(WaitKind::CargoLock(CargoLockWhat::BuildDirectory))
204 );
205 assert_eq!(
206 marker(PACKAGE_CACHE),
207 Some(WaitKind::CargoLock(CargoLockWhat::PackageCache))
208 );
209 assert_eq!(
210 marker("Blocking waiting for file lock on the git checkouts"),
211 Some(WaitKind::CargoLock(CargoLockWhat::Other))
212 );
213 assert_eq!(marker(UV), Some(WaitKind::UvLock));
214 }
215
216 #[test]
218 fn other_lines_are_not_markers() {
219 assert_eq!(marker(" Checking foo v0.1.0"), None);
220 assert_eq!(
221 marker("test prints Blocking waiting for file lock on build directory"),
222 None
223 );
224 assert_eq!(marker("waiting for lock on x"), None);
225 assert_eq!(marker(""), None);
226 }
227
228 #[test]
231 fn a_coloured_line_matches_once_its_escapes_are_stripped() {
232 assert_eq!(marker(COLOURED), None);
233 assert_eq!(strip_csi(COLOURED), BUILD_DIR);
234 assert_eq!(
235 marker(&strip_csi(COLOURED)),
236 Some(WaitKind::CargoLock(CargoLockWhat::BuildDirectory))
237 );
238 assert_eq!(strip_csi("plain"), "plain");
239 assert_eq!(strip_csi("\u{1b}[2K\u{1b}[1Gline"), "line");
240 assert_eq!(strip_csi("a\u{1b}b"), "a\u{1b}b");
242 }
243
244 #[test]
246 fn looks_like_wait_matches_phrases_not_words() {
247 assert!(looks_like_wait("waiting for lock on x"));
248 assert!(looks_like_wait(BUILD_DIR));
249 assert!(looks_like_wait("Waiting to acquire write lock"));
250 assert!(looks_like_wait("BLOCKING WAITING on the index"));
251 assert!(!looks_like_wait("test_lock.py::test_it PASSED"));
252 assert!(!looks_like_wait("src/lock.rs: 3 warnings"));
253 assert!(!looks_like_wait("lock"));
254 }
255
256 #[test]
258 fn the_look_alike_line_is_no_marker() {
259 let line = "waiting for lock on x";
260 assert_eq!(marker(line), None);
261 assert!(looks_like_wait(line));
262 }
263
264 #[test]
267 fn framing_joins_a_line_split_across_chunks() {
268 let mut f = LineFramer::default();
269 assert_eq!(f.feed(b" Blocking waiting for"), Vec::<String>::new());
270 assert_eq!(
271 f.feed(b" file lock on build directory\n Check"),
272 vec![BUILD_DIR.to_string()]
273 );
274 assert_eq!(f.feed(b"ing foo\r\n"), vec![" Checking foo".to_string()]);
275 assert_eq!(f.feed(b"a\nb\n"), vec!["a".to_string(), "b".to_string()]);
276 }
277
278 #[test]
281 fn a_line_past_the_cap_is_dropped_not_grown() {
282 let mut f = LineFramer::default();
283 let long = vec![b'x'; 5 * 1024];
284 assert_eq!(f.feed(&long), Vec::<String>::new());
285 assert!(f.tail.len() <= MAX_LINE);
286 assert_eq!(f.feed(b"tail\nnext\n"), vec!["next".to_string()]);
287 }
288
289 #[test]
290 fn kinds_round_trip_through_their_code() {
291 for kind in [
292 WaitKind::CargoLock(CargoLockWhat::BuildDirectory),
293 WaitKind::CargoLock(CargoLockWhat::PackageCache),
294 WaitKind::CargoLock(CargoLockWhat::Other),
295 WaitKind::UvLock,
296 ] {
297 assert_eq!(WaitKind::from_code(kind.code()), Some(kind));
298 }
299 assert_eq!(WaitKind::from_code(0), None);
300 }
301}