use std::fs::{self, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant, SystemTime};
const ENV_LOCK_FILE: &str = "jira-cli-test-env.lock";
const ENV_LOCK_TIMEOUT: Duration = Duration::from_secs(15);
const STALE_LOCK_AGE: Duration = Duration::from_secs(60);
pub struct ProcessEnvLock {
path: PathBuf,
_file: std::fs::File,
}
impl ProcessEnvLock {
pub fn acquire() -> io::Result<Self> {
let path = std::env::temp_dir().join(ENV_LOCK_FILE);
let deadline = Instant::now() + ENV_LOCK_TIMEOUT;
loop {
match OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => return Ok(Self { path, _file: file }),
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
clear_stale_lock(&path);
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("timed out waiting for env lock at {}", path.display()),
));
}
thread::sleep(Duration::from_millis(10));
}
Err(err) => return Err(err),
}
}
}
}
impl Drop for ProcessEnvLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
pub struct EnvVarGuard {
name: &'static str,
previous: Option<String>,
}
impl EnvVarGuard {
pub fn set(name: &'static str, value: &str) -> Self {
let previous = std::env::var(name).ok();
unsafe { std::env::set_var(name, value) };
Self { name, previous }
}
pub fn unset(name: &'static str) -> Self {
let previous = std::env::var(name).ok();
unsafe { std::env::remove_var(name) };
Self { name, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => {
unsafe { std::env::set_var(self.name, value) };
}
None => {
unsafe { std::env::remove_var(self.name) };
}
}
}
}
pub fn set_config_dir_env(path: &Path) -> EnvVarGuard {
EnvVarGuard::set(config_dir_env_name(), path.to_string_lossy().as_ref())
}
pub fn unset_config_dir_env() -> EnvVarGuard {
EnvVarGuard::unset(config_dir_env_name())
}
pub fn write_config(dir: &Path, body: &str) -> io::Result<PathBuf> {
let path = dir.join("jira").join("config.toml");
fs::create_dir_all(path.parent().unwrap_or(dir))?;
fs::write(&path, body)?;
Ok(path)
}
fn config_dir_env_name() -> &'static str {
#[cfg(target_os = "windows")]
{
"APPDATA"
}
#[cfg(not(target_os = "windows"))]
{
"XDG_CONFIG_HOME"
}
}
fn clear_stale_lock(path: &Path) {
let Ok(metadata) = fs::metadata(path) else {
return;
};
let Ok(modified) = metadata.modified() else {
return;
};
let Ok(age) = SystemTime::now().duration_since(modified) else {
return;
};
if age >= STALE_LOCK_AGE {
let _ = fs::remove_file(path);
}
}