release_kit/self_depend/
guard.rs1use std::fs;
12use std::io::Write as _;
13use std::path::PathBuf;
14
15use camino::Utf8Path;
16
17use super::lock_path;
18use crate::maintenance::GIT_HOOK_VARS;
19use crate::probes::git_bin;
20
21const LOCK_GRACE: std::time::Duration = std::time::Duration::from_secs(15 * 60);
23
24pub const CI_VARS: [&str; 6] = [
26 "CI",
27 "GITHUB_ACTIONS",
28 "GITLAB_CI",
29 "BUILDKITE",
30 "CIRCLECI",
31 "TF_BUILD",
32];
33
34pub const SWITCH_VAR: &str = "RK_DEVSHELL_SYNC";
37
38pub const IN_ENVRC_VAR: &str = "DIRENV_IN_ENVRC";
41
42#[must_use]
45pub fn in_envrc() -> bool {
46 std::env::var_os(IN_ENVRC_VAR).is_some_and(|value| !value.is_empty())
47}
48
49#[must_use]
51pub fn switched_off() -> bool {
52 std::env::var(SWITCH_VAR).is_ok_and(|value| value.trim() == "0")
53}
54
55#[must_use]
57pub fn in_ci() -> bool {
58 CI_VARS.iter().any(|var| {
59 std::env::var(var).is_ok_and(|value| {
60 let value = value.trim().to_ascii_lowercase();
61 !(value.is_empty() || value == "0" || value == "false")
62 })
63 })
64}
65
66#[derive(Debug)]
68pub struct Lock(PathBuf);
69
70impl Drop for Lock {
71 fn drop(&mut self) {
72 let _ = fs::remove_file(&self.0);
73 }
74}
75
76#[derive(Debug)]
78pub enum Acquired {
79 Held(Lock),
81 Contended,
83 Unavailable(std::io::Error),
85}
86
87#[must_use]
91pub fn acquire(key: &str) -> Acquired {
92 let Some(path) = lock_path(key) else {
93 return Acquired::Unavailable(std::io::Error::other(
94 "neither XDG_STATE_HOME nor HOME is set, so the lock has no root",
95 ));
96 };
97 if let Some(parent) = path.parent()
98 && let Err(source) = fs::create_dir_all(parent)
99 {
100 return Acquired::Unavailable(source);
101 }
102 match take(&path) {
103 Ok(lock) => Acquired::Held(lock),
104 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
105 let pid = fs::read_to_string(&path).ok().and_then(|text| {
106 text.lines()
107 .find_map(|line| line.strip_prefix("pid=")?.trim().parse::<u64>().ok())
108 });
109 if !super::txn::owner_gone_after(pid, &path, LOCK_GRACE) {
110 return Acquired::Contended;
111 }
112 match fs::remove_file(&path) {
115 Ok(()) => {}
116 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
117 Err(source) => return Acquired::Unavailable(source),
118 }
119 match take(&path) {
120 Ok(lock) => Acquired::Held(lock),
121 Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
122 Acquired::Contended
123 }
124 Err(source) => Acquired::Unavailable(source),
125 }
126 }
127 Err(source) => Acquired::Unavailable(source),
128 }
129}
130
131fn take(path: &std::path::Path) -> std::io::Result<Lock> {
133 let mut file = fs::OpenOptions::new()
134 .write(true)
135 .create_new(true)
136 .open(path)?;
137 file.write_all(format!("pid={}\n", std::process::id()).as_bytes())?;
138 file.write_all(format!("started={}\n", crate::applog::now_utc()).as_bytes())?;
139 Ok(Lock(path.to_path_buf()))
140}
141
142#[must_use]
149pub fn files_dirty(target: &Utf8Path, files: &[&str]) -> bool {
150 let mut command = std::process::Command::new(git_bin());
151 for var in GIT_HOOK_VARS {
152 command.env_remove(var);
153 }
154 command
155 .arg("-C")
156 .arg(target.as_std_path())
157 .args(["status", "--porcelain", "--"])
158 .args(files)
159 .output()
160 .map_or(true, |probed| {
161 !probed.status.success() || !probed.stdout.is_empty()
162 })
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{Acquired, take};
168
169 #[test]
170 fn the_lock_is_exclusive_and_released_on_drop() {
171 let dir = tempfile::tempdir().expect("a scratch dir exists");
172 let path = dir.path().join("k.lock");
173 let held = take(&path).expect("the first take holds");
174 assert!(path.exists());
175 let second = take(&path).expect_err("the second take refuses");
176 assert_eq!(second.kind(), std::io::ErrorKind::AlreadyExists);
177 drop(held);
178 assert!(!path.exists(), "the lock is removed on drop");
179 let again = take(&path).expect("the lock is free again");
180 assert!(
181 std::fs::read_to_string(&path)
182 .expect("reads")
183 .starts_with("pid="),
184 "the lock names its owner"
185 );
186 drop(again);
187 }
188
189 #[test]
190 fn the_acquired_vocabulary_is_three_states() {
191 let unavailable = Acquired::Unavailable(std::io::Error::other("x"));
192 assert!(matches!(unavailable, Acquired::Unavailable(_)));
193 assert!(matches!(Acquired::Contended, Acquired::Contended));
194 }
195}