use std::fs;
use std::io::Write as _;
use std::path::PathBuf;
use camino::Utf8Path;
use super::{lock_path, stamp_path};
use crate::maintenance::GIT_HOOK_VARS;
use crate::probes::git_bin;
const LOCK_GRACE: std::time::Duration = std::time::Duration::from_secs(15 * 60);
pub const CI_VARS: [&str; 6] = [
"CI",
"GITHUB_ACTIONS",
"GITLAB_CI",
"BUILDKITE",
"CIRCLECI",
"TF_BUILD",
];
pub const SWITCH_VAR: &str = "RK_DEVSHELL_SYNC";
#[must_use]
pub fn switched_off() -> bool {
std::env::var(SWITCH_VAR).is_ok_and(|value| value.trim() == "0")
}
#[must_use]
pub fn in_ci() -> bool {
CI_VARS.iter().any(|var| {
std::env::var(var).is_ok_and(|value| {
let value = value.trim().to_ascii_lowercase();
!(value.is_empty() || value == "0" || value == "false")
})
})
}
#[must_use]
pub fn today() -> String {
crate::applog::now_utc()[..10].to_owned()
}
pub fn write_stamp(key: &str) -> std::io::Result<()> {
let Some(path) = stamp_path(key) else {
return Err(std::io::Error::other("no state root for the stamp"));
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
crate::atomic::write(&path, format!("{}\n", today()).as_bytes())
}
#[derive(Debug)]
pub struct Lock(PathBuf);
impl Drop for Lock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
#[derive(Debug)]
pub enum Acquired {
Held(Lock),
Contended,
Unavailable(std::io::Error),
}
#[must_use]
pub fn acquire(key: &str) -> Acquired {
let Some(path) = lock_path(key) else {
return Acquired::Unavailable(std::io::Error::other(
"neither XDG_STATE_HOME nor HOME is set, so the lock has no root",
));
};
if let Some(parent) = path.parent() {
if let Err(source) = fs::create_dir_all(parent) {
return Acquired::Unavailable(source);
}
}
match take(&path) {
Ok(lock) => Acquired::Held(lock),
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
let pid = fs::read_to_string(&path).ok().and_then(|text| {
text.lines()
.find_map(|line| line.strip_prefix("pid=")?.trim().parse::<u64>().ok())
});
if !super::txn::owner_gone_after(pid, &path, LOCK_GRACE) {
return Acquired::Contended;
}
match fs::remove_file(&path) {
Ok(()) => {}
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => return Acquired::Unavailable(source),
}
match take(&path) {
Ok(lock) => Acquired::Held(lock),
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
Acquired::Contended
}
Err(source) => Acquired::Unavailable(source),
}
}
Err(source) => Acquired::Unavailable(source),
}
}
fn take(path: &std::path::Path) -> std::io::Result<Lock> {
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(format!("pid={}\n", std::process::id()).as_bytes())?;
file.write_all(format!("started={}\n", crate::applog::now_utc()).as_bytes())?;
Ok(Lock(path.to_path_buf()))
}
#[must_use]
pub fn two_files_dirty(target: &Utf8Path) -> bool {
let mut command = std::process::Command::new(git_bin());
for var in GIT_HOOK_VARS {
command.env_remove(var);
}
command
.arg("-C")
.arg(target.as_std_path())
.args(["status", "--porcelain", "--", "flake.nix", "flake.lock"])
.output()
.map_or(true, |probed| {
!probed.status.success() || !probed.stdout.is_empty()
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::{Acquired, take};
#[test]
fn the_lock_is_exclusive_and_released_on_drop() {
let dir = tempfile::tempdir().expect("a scratch dir exists");
let path = dir.path().join("k.lock");
let held = take(&path).expect("the first take holds");
assert!(path.exists());
let second = take(&path).expect_err("the second take refuses");
assert_eq!(second.kind(), std::io::ErrorKind::AlreadyExists);
drop(held);
assert!(!path.exists(), "the lock is removed on drop");
let again = take(&path).expect("the lock is free again");
assert!(
std::fs::read_to_string(&path)
.expect("reads")
.starts_with("pid="),
"the lock names its owner"
);
drop(again);
}
#[test]
fn the_acquired_vocabulary_is_three_states() {
let unavailable = Acquired::Unavailable(std::io::Error::other("x"));
assert!(matches!(unavailable, Acquired::Unavailable(_)));
assert!(matches!(Acquired::Contended, Acquired::Contended));
}
}