mechutil 0.8.8

Utility structures and functions for mechatronics applications.
Documentation
//! Shared autocore filesystem locations.
//!
//! Every autocore process resolves well-known directories through this module
//! so the directory policy lives in exactly one place. The config directory is
//! the sibling of the log directory resolved by [`crate::run_log::resolve_log_dir`]
//! and follows the same env → platform-default → writable-fallback strategy, so
//! a privileged install lands in `/srv/autocore/config` while an unprivileged
//! dev run transparently falls back to `./target/autocore/config` without each
//! binary special-casing debug builds.

use std::fs::{self, OpenOptions};
use std::path::{Path, PathBuf};

/// Resolve the autocore config directory, shared by the server, every module,
/// and registered tools. Resolution order:
///
///   1. `AUTOCORE_CONFIG_DIR` env var (always wins; handy for tests / dev).
///   2. Platform default: Linux `/srv/autocore/config`,
///      Windows `%PROGRAMDATA%\ADC\autocore\config`.
///   3. If the default isn't writable (e.g. an unprivileged dev run where
///      `/srv` is root-owned), fall back to `./target/autocore/config`.
///
/// The returned directory is created if it does not yet exist (best effort).
pub fn resolve_config_dir() -> PathBuf {
    if let Ok(p) = std::env::var("AUTOCORE_CONFIG_DIR") {
        let p = PathBuf::from(p);
        let _ = fs::create_dir_all(&p);
        return p;
    }
    let preferred = if cfg!(target_os = "windows") {
        match std::env::var("PROGRAMDATA") {
            Ok(d) => PathBuf::from(d).join("ADC").join("autocore").join("config"),
            Err(_) => PathBuf::from("./target/autocore/config"),
        }
    } else {
        PathBuf::from("/srv/autocore/config")
    };
    if dir_is_writable(&preferred) {
        return preferred;
    }
    eprintln!(
        "WARN: cannot write to {:?}; falling back to ./target/autocore/config",
        preferred,
    );
    let fallback = PathBuf::from("./target/autocore/config");
    let _ = fs::create_dir_all(&fallback);
    fallback
}

/// True if `dir` exists (or can be created) and a probe file can be written in
/// it. Used by [`resolve_config_dir`] to pick a writable location.
fn dir_is_writable(dir: &Path) -> bool {
    if fs::create_dir_all(dir).is_err() {
        return false;
    }
    let probe = dir.join(".config_writable_probe");
    match OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(&probe)
    {
        Ok(_) => {
            let _ = fs::remove_file(&probe);
            true
        }
        Err(_) => false,
    }
}

/// Test-only support shared across modules that exercise `AUTOCORE_CONFIG_DIR`.
/// The env var is process-global, so every test that sets it must serialize on
/// this lock or parallel tests will read each other's value.
#[cfg(test)]
pub(crate) mod test_support {
    use std::path::PathBuf;
    use std::sync::{Mutex, MutexGuard};

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    /// Run `f` with `AUTOCORE_CONFIG_DIR` pointed at a fresh temp dir, holding
    /// the global env lock for the whole closure. Recovers from a poisoned lock
    /// so one failing test doesn't cascade into the rest.
    pub fn with_temp_config<F: FnOnce()>(tag: &str, f: F) {
        let _guard: MutexGuard<()> = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
        let dir =
            std::env::temp_dir().join(format!("mechutil_cfg_{}_{}", tag, std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        // SAFETY: serialized by ENV_LOCK; no other test touches the var concurrently.
        unsafe { std::env::set_var("AUTOCORE_CONFIG_DIR", &dir) };
        f();
        unsafe { std::env::remove_var("AUTOCORE_CONFIG_DIR") };
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// The temp config dir that [`with_temp_config`] set for the current closure.
    pub fn current_dir() -> PathBuf {
        PathBuf::from(std::env::var("AUTOCORE_CONFIG_DIR").expect("within with_temp_config"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn env_override_wins_and_is_created() {
        test_support::with_temp_config("paths", || {
            let dir = test_support::current_dir();
            let resolved = resolve_config_dir();
            assert_eq!(resolved, dir);
            assert!(resolved.is_dir(), "resolve_config_dir should create the dir");
        });
    }
}