Skip to main content

git_policy_cli/
lock_holder_facts.rs

1//! What:
2//!  What a waiter can tell its user about the process that holds the lock it waits for: how long the lock has been
3//!  held, what the holder runs, and which limits end the wait.
4//! Why:
5//!  A silent or anonymous wait looks like a hang. The age is the time since the lock directory was published, which
6//!  is when the holder took the lock; the command comes from the kernel's record of the holder (Linux `/proc`; other
7//!  systems name no command); the limits are the push deadline that stops the holder's push and the wait limit that
8//!  stops this wait.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! `The holder has held the lock for ${age} seconds and runs \`${command}\`. ...`
13//! ```
14
15/// Time spans.
16use std::time::Duration;
17/// File locations.
18use std::path::Path;
19
20/// Global options that take their value as the next argument.
21const VALUE_OPTIONS: [&str; 5] = ["-c", "-C", "--git-dir", "--work-tree", "--namespace"];
22
23/// What:
24///  The program and subcommand of a command line, with every other argument left out.
25/// Why:
26///   Arguments can hold secrets (`-c http.extraHeader=...`), and the notice repeats at an interval into logs, so only
27///  the program's name and the subcommand are shown. Global options are skipped to find the subcommand.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// `${basename(argv[0])} ${argv.slice(1).find((word, i) => !word.startsWith('-') && !valueOptions.includes(argv[i]))}`
32/// ```
33pub fn program_and_subcommand(words: &[&str]) -> Option<String> {
34    let program: &str = words.first()?.rsplit('/').next()?;
35    let mut rest = words.iter().skip(1);
36    while let Some(word) = rest.next() {
37        if VALUE_OPTIONS.contains(word) {
38            rest.next();
39            continue;
40        }
41        if !word.starts_with('-') {
42            return Some(format!("{program} {word}"));
43        }
44    }
45    return Some(program.to_owned());
46}
47
48/// What:
49///  The holder's program and subcommand from the kernel's record, and the repository it runs in.
50/// Why:
51///   The repository is the holder's working directory, which `/proc` records without any argument.
52///
53/// In TS you'd write (pseudocode):
54/// ```ts
55/// `${programAndSubcommand(cmdline)} in ${readlink(`/proc/${pid}/cwd`)}`
56/// ```
57#[cfg(target_os = "linux")]
58fn holder_command(pid: i64) -> Option<String> {
59    let bytes: Vec<u8> = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
60    let text: String = String::from_utf8_lossy(bytes.as_slice()).into_owned();
61    let words: Vec<&str> = text.split('\0').filter(|word| {
62        return !word.is_empty();
63    }).collect();
64    let shown: String = program_and_subcommand(words.as_slice())?;
65    return Some(match std::fs::read_link(format!("/proc/{pid}/cwd")) {
66        Ok(directory) => format!("{shown} in {}", directory.display()),
67        Err(_) => shown,
68    });
69}
70
71/// Only Linux records the command of another process in a file; elsewhere none is named.
72// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
73#[cfg(not(target_os = "linux"))]
74fn holder_command(_pid: i64) -> Option<String> {
75    return None;
76}
77
78/// What:
79///  How long ago the lock directory was published, in whole seconds, or nothing when that cannot be read.
80/// Why:
81///   The directory is published once, by the holder, at the moment it takes the lock.
82///
83/// In TS you'd write (pseudocode):
84/// ```ts
85/// Math.floor((Date.now() - statSync(lockDirectory).mtimeMs) / 1000)
86/// ```
87fn lock_age(lock_directory: &Path) -> Option<Duration> {
88    return std::fs::metadata(lock_directory).ok()?.modified().ok()?.elapsed().ok();
89}
90
91/// What:
92///  The sentences a waiting notice adds about its holder and the limits.
93/// Why:
94///   They say who holds the lock and for how long, and what ends the holder's push (`push_deadline`); the wait's own
95///  limit is already in the notice, so a long wait is told from a stuck one.
96///
97/// In TS you'd write (pseudocode):
98/// ```ts
99/// function holderFacts({ pid, lockDirectory, pushDeadline }): string;
100/// ```
101pub fn holder_facts(pid: i64, lock_directory: &Path, push_deadline: Duration) -> String {
102    let mut facts: String = String::new();
103    match (lock_age(lock_directory), holder_command(pid)) {
104        (Some(age), Some(command)) => {
105            facts.push_str(&format!(" The holder has held the lock for {} seconds and runs `{command}`.", age.as_secs()));
106        }
107        (Some(age), None) => {
108            facts.push_str(&format!(" The holder has held the lock for {} seconds.", age.as_secs()));
109        }
110        (None, Some(command)) => facts.push_str(&format!(" The holder runs `{command}`.")),
111        (None, None) => {}
112    }
113    facts.push_str(&format!(
114        " Its push is stopped after {} seconds; a holder still holding the lock when this wait ends is reported and left in place.",
115        push_deadline.as_secs()
116    ));
117    return facts;
118}
119
120/// The facts are controlled in the test module.
121#[cfg(test)]
122#[path = "lock_holder_facts_tests.rs"]
123mod tests;