use std::path::PathBuf;
use std::sync::{Mutex, MutexGuard};
static PROCESS_STATE_TEST_LOCK: Mutex<()> = Mutex::new(());
pub(crate) struct ProcessStateTestGuard {
original_dir: Option<PathBuf>,
_lock: MutexGuard<'static, ()>,
}
impl ProcessStateTestGuard {
pub(crate) fn enter(&mut self) -> PathBuf {
let path = cwd_dir();
std::env::set_current_dir(&path).unwrap();
path
}
}
impl Drop for ProcessStateTestGuard {
fn drop(&mut self) {
if let Some(dir) = self.original_dir.take() {
let _ = std::env::set_current_dir(dir);
}
}
}
static CWD_DIRS: Mutex<Vec<tempfile::TempDir>> = Mutex::new(Vec::new());
pub(crate) fn cwd_dir() -> PathBuf {
static REMOVE_AT_EXIT: std::sync::Once = std::sync::Once::new();
REMOVE_AT_EXIT.call_once(|| {
unsafe { libc::atexit(remove_cwd_dirs) };
});
let dir = tempfile::tempdir().unwrap();
let path = dir.path().to_path_buf();
CWD_DIRS
.lock()
.unwrap_or_else(|error| error.into_inner())
.push(dir);
path
}
extern "C" fn remove_cwd_dirs() {
if let Ok(mut dirs) = CWD_DIRS.lock() {
dirs.clear();
}
}
pub(crate) fn process_state_test_lock() -> ProcessStateTestGuard {
let lock = PROCESS_STATE_TEST_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
ProcessStateTestGuard {
original_dir: std::env::current_dir().ok(),
_lock: lock,
}
}
pub(crate) struct KunobiConfigGuard {
_lock: ProcessStateTestGuard,
#[cfg_attr(not(unix), allow(dead_code))]
dir: tempfile::TempDir,
saved: Vec<(&'static str, Option<std::ffi::OsString>)>,
}
impl KunobiConfigGuard {
pub(crate) fn new() -> Self {
let lock = process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let saved = ["HOME", "XDG_CONFIG_HOME"]
.into_iter()
.map(|key| {
let previous = std::env::var_os(key);
unsafe { std::env::set_var(key, dir.path()) };
(key, previous)
})
.collect();
Self {
_lock: lock,
dir,
saved,
}
}
#[cfg(unix)]
pub(crate) fn path(&self) -> &std::path::Path {
self.dir.path()
}
}
impl Drop for KunobiConfigGuard {
fn drop(&mut self) {
for (key, value) in self.saved.drain(..) {
unsafe {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
}
pub(crate) fn without_terminal(command: &mut std::process::Command) -> &mut std::process::Command {
#[cfg(unix)]
{
use std::os::unix::process::CommandExt as _;
unsafe {
command.pre_exec(|| {
libc::setsid();
Ok(())
});
}
}
command
}
pub(crate) fn test_config(cache_dir: PathBuf) -> crate::config::Config {
crate::config::Config {
fallback: None,
key_salt: None,
cc_extra_allowlist_flags: Vec::new(),
local_only: false,
remote_readonly: false,
readonly_store: None,
pull_request_prefix: None,
modified_input_guard: false,
input_predictions: false,
record_sessions: false,
volume_stores: Vec::new(),
windows_hardlink: false,
shared_hardlink_restores: false,
deferred_discovery: true,
out_dir_alias: true,
deferred_durability: false,
daemon_publish: true,
project_rules: crate::config::ProjectRules::default(),
auto_gc: true,
index_auto_compact: true,
auto_clean_orphaned_targets: false,
auto_share_target_files: false,
auto_clean_idle_targets_days: 0,
auto_recover_min_free_bytes: 0,
auto_recover_targets: false,
scheduler_memory_pressure: true,
auto_clean_unused_units_days: 0,
seed_new_targets: false,
build_script_hermetic: false,
gc_evict_shared: false,
storage_layout_advice: true,
heartbeat_secs: 30,
explain_miss: false,
scheduler: true,
test_lease: None,
path_only_env_vars: Vec::new(),
incremental_crates: Vec::new(),
key_env_vars: Vec::new(),
base_dirs: Vec::new(),
runtime_dir: cache_dir.clone(),
cache_dir,
max_size: 1024 * 1024,
remote: None,
remote_error: None,
socket_path_override: None,
disabled: false,
cache_executables: false,
cache_cc_links: false,
trust_codegen_backends: false,
target_liveness: false,
clean_incremental: true,
preserve_incremental: false,
adaptive_incremental: true,
event_log_max_size: 10 * 1024 * 1024,
event_log_keep_lines: 1000,
compression_level: 3,
s3_concurrency: 16,
prefetch_enabled: crate::config::DEFAULT_PREFETCH_ENABLED,
remote_key_listing: false,
remote_key_cache_refresh_secs: crate::config::DEFAULT_REMOTE_KEY_CACHE_REFRESH_SECS,
prefetch_max_keys: crate::config::DEFAULT_PREFETCH_MAX_KEYS,
prefetch_max_bytes: crate::config::DEFAULT_PREFETCH_MAX_BYTES,
prefetch_deadline_secs: crate::config::DEFAULT_PREFETCH_DEADLINE_SECS,
min_store_compile_ms: crate::config::DEFAULT_MIN_STORE_COMPILE_MS,
gc_max_age_hours: crate::config::DEFAULT_GC_MAX_AGE_HOURS,
daemon_idle_timeout_secs: crate::config::DEFAULT_DAEMON_IDLE_TIMEOUT_SECS,
s3_pool_idle_secs: crate::config::DEFAULT_S3_POOL_IDLE_SECS,
remote_restore_timeout_secs: crate::config::DEFAULT_REMOTE_RESTORE_TIMEOUT_SECS,
remote_negative_ttl_secs: crate::config::DEFAULT_REMOTE_NEGATIVE_TTL_SECS,
}
}
#[cfg(unix)]
pub(crate) fn output_retrying_etxtbsy(
command: &mut std::process::Command,
) -> std::io::Result<std::process::Output> {
for _ in 0..ETXTBSY_RETRIES {
match command.output() {
Err(error) if error.raw_os_error() == Some(libc::ETXTBSY) => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
result => return result,
}
}
command.output()
}
#[cfg(unix)]
const ETXTBSY_RETRIES: usize = 50;
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) fn settle_writes(paths: &[&std::path::Path]) {
use crate::cache_key::{
FINE_STAMP_WINDOW_NS, FileFingerprint, stamp_clock_ns, stamp_window_ns, wall_clock_ns,
};
fn collect(path: &std::path::Path, stamps: &mut Vec<i64>) {
if path.is_dir() {
for entry in std::fs::read_dir(path).into_iter().flatten().flatten() {
collect(&entry.path(), stamps);
}
} else if let Ok(stamp) = FileFingerprint::from_path(path) {
stamps.extend([stamp.mtime_ns, stamp.ctime_ns]);
}
}
let now = wall_clock_ns();
let mut stamps = Vec::new();
for path in paths {
collect(path, &mut stamps);
}
let settled_at = stamps
.into_iter()
.filter(|&stamp| stamp <= now)
.map(|stamp| stamp.saturating_add(stamp_window_ns(stamp)))
.fold(
stamp_clock_ns().saturating_add(FINE_STAMP_WINDOW_NS),
i64::max,
);
while stamp_clock_ns() <= settled_at {
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
#[cfg(test)]
mod tests {
use super::{cwd_dir, process_state_test_lock};
#[test]
fn dropping_the_guard_restores_the_current_directory() {
let original = {
let _lock = process_state_test_lock();
std::env::current_dir().unwrap()
};
let scratch = cwd_dir();
{
let _lock = process_state_test_lock();
std::env::set_current_dir(&scratch).unwrap();
assert_ne!(std::env::current_dir().unwrap(), original);
}
let _lock = process_state_test_lock();
assert_eq!(
std::env::current_dir().unwrap(),
original,
"the guard must restore the directory even when the test body never does"
);
}
#[test]
fn an_entered_dir_outlives_the_guard() {
let (original, entered) = {
let mut lock = process_state_test_lock();
let original = std::env::current_dir().unwrap();
let entered = lock.enter();
assert_eq!(
std::env::current_dir().unwrap().canonicalize().unwrap(),
entered.canonicalize().unwrap()
);
(original, entered)
};
let _lock = process_state_test_lock();
assert_eq!(std::env::current_dir().unwrap(), original);
assert!(
entered.is_dir(),
"the entered dir stays until the binary exits"
);
}
#[cfg(target_os = "linux")]
fn busy_script(dir: &std::path::Path) -> (std::path::PathBuf, std::fs::File) {
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
let script = dir.join("busy.sh");
let mut file = std::fs::File::create(&script).unwrap();
file.write_all(b"#!/bin/sh\nexit 0\n").unwrap();
file.set_permissions(std::fs::Permissions::from_mode(0o755))
.unwrap();
(script, file)
}
#[cfg(target_os = "linux")]
#[test]
fn a_spawn_waits_out_a_writer_that_closes() {
let dir = tempfile::tempdir().unwrap();
let (script, writer) = busy_script(dir.path());
let error = std::process::Command::new(&script).output().unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::ETXTBSY));
let closer = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(50));
drop(writer);
});
let output = super::output_retrying_etxtbsy(&mut std::process::Command::new(&script));
closer.join().unwrap();
assert!(output.unwrap().status.success());
}
#[cfg(target_os = "linux")]
#[test]
fn a_writer_that_stays_open_is_a_real_error() {
let dir = tempfile::tempdir().unwrap();
let (script, _writer) = busy_script(dir.path());
let error =
super::output_retrying_etxtbsy(&mut std::process::Command::new(&script)).unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::ETXTBSY));
}
#[cfg(unix)]
#[test]
fn other_spawn_errors_are_not_retried() {
let dir = tempfile::tempdir().unwrap();
let started = std::time::Instant::now();
let error = super::output_retrying_etxtbsy(&mut std::process::Command::new(
dir.path().join("missing"),
))
.unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
assert!(started.elapsed() < std::time::Duration::from_millis(250));
}
}