#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CargoLockWhat {
BuildDirectory,
PackageCache,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitKind {
CargoLock(CargoLockWhat),
UvLock,
}
impl WaitKind {
pub fn describe(self) -> &'static str {
match self {
WaitKind::CargoLock(CargoLockWhat::BuildDirectory) => {
"the cargo lock on the build directory"
}
WaitKind::CargoLock(CargoLockWhat::PackageCache) => {
"the cargo lock on the package cache"
}
WaitKind::CargoLock(CargoLockWhat::Other) => "a cargo lock",
WaitKind::UvLock => "a uv lock",
}
}
pub fn short(self) -> &'static str {
match self {
WaitKind::CargoLock(_) => "cargo lock",
WaitKind::UvLock => "uv lock",
}
}
pub fn holders(self) -> &'static str {
match self {
WaitKind::CargoLock(_) => {
"another cargo holds it (a second worktree, rust-analyzer, a build in another session)"
}
WaitKind::UvLock => "another uv holds it (a second worktree, or an install in another session)",
}
}
pub fn code(self) -> u8 {
match self {
WaitKind::CargoLock(CargoLockWhat::BuildDirectory) => 1,
WaitKind::CargoLock(CargoLockWhat::PackageCache) => 2,
WaitKind::CargoLock(CargoLockWhat::Other) => 3,
WaitKind::UvLock => 4,
}
}
pub fn from_code(code: u8) -> Option<WaitKind> {
Some(match code {
1 => WaitKind::CargoLock(CargoLockWhat::BuildDirectory),
2 => WaitKind::CargoLock(CargoLockWhat::PackageCache),
3 => WaitKind::CargoLock(CargoLockWhat::Other),
4 => WaitKind::UvLock,
_ => return None,
})
}
}
pub fn strip_csi(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next();
for c in chars.by_ref() {
if ('@'..='~').contains(&c) {
break;
}
}
continue;
}
out.push(c);
}
out
}
pub fn marker(line: &str) -> Option<WaitKind> {
let line = line.trim();
if let Some(what) = line.strip_prefix("Blocking waiting for file lock on ") {
return Some(WaitKind::CargoLock(match what.trim() {
"build directory" => CargoLockWhat::BuildDirectory,
"package cache" => CargoLockWhat::PackageCache,
_ => CargoLockWhat::Other,
}));
}
if line.starts_with("Waiting to acquire ") && line.contains(" lock for `") {
return Some(WaitKind::UvLock);
}
None
}
pub fn looks_like_wait(line: &str) -> bool {
let lower = line.to_ascii_lowercase();
[
"waiting for file lock",
"waiting to acquire",
"blocking waiting",
"waiting for lock",
]
.iter()
.any(|phrase| lower.contains(phrase))
}
pub const MAX_LINE: usize = 4096;
#[derive(Default)]
pub struct LineFramer {
tail: Vec<u8>,
overflowed: bool,
}
impl LineFramer {
pub fn feed(&mut self, chunk: &[u8]) -> Vec<String> {
let mut lines = Vec::new();
for byte in chunk {
if *byte == b'\n' {
if !self.overflowed {
let line = String::from_utf8_lossy(&self.tail).into_owned();
lines.push(line.trim_end_matches('\r').to_string());
}
self.tail.clear();
self.overflowed = false;
continue;
}
if self.overflowed {
continue;
}
if self.tail.len() >= MAX_LINE {
self.tail.clear();
self.overflowed = true;
continue;
}
self.tail.push(*byte);
}
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
const BUILD_DIR: &str = " Blocking waiting for file lock on build directory";
const PACKAGE_CACHE: &str = " Blocking waiting for file lock on package cache";
const COLOURED: &str =
"\u{1b}[1m\u{1b}[36m Blocking\u{1b}[0m waiting for file lock on build directory";
const UV: &str = "Waiting to acquire write lock for `/home/u/.cache/uv/environments-v2`";
#[test]
fn the_real_cargo_and_uv_lines_are_markers() {
assert_eq!(
marker(BUILD_DIR),
Some(WaitKind::CargoLock(CargoLockWhat::BuildDirectory))
);
assert_eq!(
marker(PACKAGE_CACHE),
Some(WaitKind::CargoLock(CargoLockWhat::PackageCache))
);
assert_eq!(
marker("Blocking waiting for file lock on the git checkouts"),
Some(WaitKind::CargoLock(CargoLockWhat::Other))
);
assert_eq!(marker(UV), Some(WaitKind::UvLock));
}
#[test]
fn other_lines_are_not_markers() {
assert_eq!(marker(" Checking foo v0.1.0"), None);
assert_eq!(
marker("test prints Blocking waiting for file lock on build directory"),
None
);
assert_eq!(marker("waiting for lock on x"), None);
assert_eq!(marker(""), None);
}
#[test]
fn a_coloured_line_matches_once_its_escapes_are_stripped() {
assert_eq!(marker(COLOURED), None);
assert_eq!(strip_csi(COLOURED), BUILD_DIR);
assert_eq!(
marker(&strip_csi(COLOURED)),
Some(WaitKind::CargoLock(CargoLockWhat::BuildDirectory))
);
assert_eq!(strip_csi("plain"), "plain");
assert_eq!(strip_csi("\u{1b}[2K\u{1b}[1Gline"), "line");
assert_eq!(strip_csi("a\u{1b}b"), "a\u{1b}b");
}
#[test]
fn looks_like_wait_matches_phrases_not_words() {
assert!(looks_like_wait("waiting for lock on x"));
assert!(looks_like_wait(BUILD_DIR));
assert!(looks_like_wait("Waiting to acquire write lock"));
assert!(looks_like_wait("BLOCKING WAITING on the index"));
assert!(!looks_like_wait("test_lock.py::test_it PASSED"));
assert!(!looks_like_wait("src/lock.rs: 3 warnings"));
assert!(!looks_like_wait("lock"));
}
#[test]
fn the_look_alike_line_is_no_marker() {
let line = "waiting for lock on x";
assert_eq!(marker(line), None);
assert!(looks_like_wait(line));
}
#[test]
fn framing_joins_a_line_split_across_chunks() {
let mut f = LineFramer::default();
assert_eq!(f.feed(b" Blocking waiting for"), Vec::<String>::new());
assert_eq!(
f.feed(b" file lock on build directory\n Check"),
vec![BUILD_DIR.to_string()]
);
assert_eq!(f.feed(b"ing foo\r\n"), vec![" Checking foo".to_string()]);
assert_eq!(f.feed(b"a\nb\n"), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn a_line_past_the_cap_is_dropped_not_grown() {
let mut f = LineFramer::default();
let long = vec![b'x'; 5 * 1024];
assert_eq!(f.feed(&long), Vec::<String>::new());
assert!(f.tail.len() <= MAX_LINE);
assert_eq!(f.feed(b"tail\nnext\n"), vec!["next".to_string()]);
}
#[test]
fn kinds_round_trip_through_their_code() {
for kind in [
WaitKind::CargoLock(CargoLockWhat::BuildDirectory),
WaitKind::CargoLock(CargoLockWhat::PackageCache),
WaitKind::CargoLock(CargoLockWhat::Other),
WaitKind::UvLock,
] {
assert_eq!(WaitKind::from_code(kind.code()), Some(kind));
}
assert_eq!(WaitKind::from_code(0), None);
}
}