Skip to main content

sysknife_core/
lib.rs

1//! Shared defaults, low-level constants, and configuration loading for the SysKnife workspace.
2
3use std::path::PathBuf;
4
5pub mod action_family;
6pub mod config;
7pub mod distro;
8
9pub use distro::{detect, detect_distro, parse_os_release, DistroFamily, DistroId};
10
11/// Production socket URI written by the systemd unit (`sysknife-daemon.service`).
12///
13/// This is **not** the dev/test fallback — see [`default_listen_uri`].
14pub const PRODUCTION_LISTEN_URI: &str = "unix:///run/sysknife/daemon.sock";
15
16/// Production SQLite path written by the systemd unit (`sysknife-daemon.service`).
17///
18/// This is **not** the dev/test fallback — see [`default_database_path`].
19pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
20
21/// Resolve the daemon listen URI for the current process.
22///
23/// Order of precedence:
24/// 1. `$SYSKNIFE_LISTEN_URI` (set by systemd unit and `sysknife-setup`)
25/// 2. `$XDG_RUNTIME_DIR/sysknife/daemon.sock` (per-user, follows freedesktop.org spec)
26/// 3. `/tmp/sysknife-$UID.sock` as the absolute last resort
27///
28/// Production deployments set the env var; dev/test invocations get a private
29/// per-user socket without root or `/var/lib` access.
30pub fn default_listen_uri() -> String {
31    if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
32        return uri;
33    }
34    if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
35        let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
36        return format!("unix://{}", p.display());
37    }
38    let uid = current_uid();
39    format!("unix:///tmp/sysknife-{uid}.sock")
40}
41
42/// Resolve the daemon SQLite database path for the current process.
43///
44/// Order of precedence:
45/// 1. `$SYSKNIFE_DATABASE_PATH` (set by systemd unit and `sysknife-setup`)
46/// 2. `$XDG_STATE_HOME/sysknife/daemon.sqlite` (per-user, persistent)
47/// 3. `$HOME/.local/state/sysknife/daemon.sqlite` (XDG fallback)
48/// 4. [`PRODUCTION_DATABASE_PATH`] if `HOME` is unset (production case where
49///    systemd sets the env var anyway, so this branch is rarely hit)
50pub fn default_database_path() -> PathBuf {
51    if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
52        return PathBuf::from(path);
53    }
54    if let Ok(state) = std::env::var("XDG_STATE_HOME") {
55        return PathBuf::from(state).join("sysknife/daemon.sqlite");
56    }
57    if let Ok(home) = std::env::var("HOME") {
58        return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
59    }
60    PathBuf::from(PRODUCTION_DATABASE_PATH)
61}
62
63/// Read the current process's real UID from `/proc/self/status`.
64///
65/// Avoids a libc dep for one syscall. The return value is only used to
66/// disambiguate the per-UID socket name in the last-resort branch of
67/// [`default_listen_uri`]; on read failure we use `0`, which still produces a
68/// valid path (just one shared by any caller in the same fallback case).
69fn current_uid() -> u32 {
70    std::fs::read_to_string("/proc/self/status")
71        .ok()
72        .and_then(|s| {
73            s.lines()
74                .find(|l| l.starts_with("Uid:"))
75                .and_then(|l| l.split_whitespace().nth(1))
76                .and_then(|v| v.parse::<u32>().ok())
77        })
78        .unwrap_or(0)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::{
84        default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH, PRODUCTION_LISTEN_URI,
85    };
86    use std::sync::Mutex;
87
88    static ENV_LOCK: Mutex<()> = Mutex::new(());
89
90    #[test]
91    fn production_constants_are_absolute() {
92        assert!(PRODUCTION_LISTEN_URI.starts_with("unix:///run/"));
93        assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
94    }
95
96    #[test]
97    fn database_env_var_wins() {
98        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
99        unsafe {
100            std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
101        }
102        let p = default_database_path();
103        unsafe {
104            std::env::remove_var("SYSKNIFE_DATABASE_PATH");
105        }
106        assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
107    }
108
109    #[test]
110    fn database_xdg_state_used_when_set() {
111        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
112        unsafe {
113            std::env::remove_var("SYSKNIFE_DATABASE_PATH");
114            std::env::set_var("XDG_STATE_HOME", "/xdg/state");
115        }
116        let p = default_database_path();
117        unsafe {
118            std::env::remove_var("XDG_STATE_HOME");
119        }
120        assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
121    }
122
123    #[test]
124    fn database_falls_back_to_home_local_state() {
125        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
126        unsafe {
127            std::env::remove_var("SYSKNIFE_DATABASE_PATH");
128            std::env::remove_var("XDG_STATE_HOME");
129            std::env::set_var("HOME", "/home/contributor");
130        }
131        let p = default_database_path();
132        unsafe {
133            std::env::remove_var("HOME");
134        }
135        assert_eq!(
136            p.to_str(),
137            Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
138        );
139    }
140
141    #[test]
142    fn database_last_resort_is_production_path() {
143        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
144        // Snapshot HOME so we can restore it (other tests in this binary need it).
145        let saved_home = std::env::var("HOME").ok();
146        unsafe {
147            std::env::remove_var("SYSKNIFE_DATABASE_PATH");
148            std::env::remove_var("XDG_STATE_HOME");
149            std::env::remove_var("HOME");
150        }
151        let p = default_database_path();
152        unsafe {
153            if let Some(h) = saved_home {
154                std::env::set_var("HOME", h);
155            }
156        }
157        assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
158    }
159
160    #[test]
161    fn listen_env_var_wins() {
162        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
163        unsafe {
164            std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
165        }
166        let u = default_listen_uri();
167        unsafe {
168            std::env::remove_var("SYSKNIFE_LISTEN_URI");
169        }
170        assert_eq!(u, "unix:///explicit.sock");
171    }
172
173    #[test]
174    fn listen_uses_xdg_runtime_dir_when_set() {
175        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
176        unsafe {
177            std::env::remove_var("SYSKNIFE_LISTEN_URI");
178            std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
179        }
180        let u = default_listen_uri();
181        unsafe {
182            std::env::remove_var("XDG_RUNTIME_DIR");
183        }
184        assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
185    }
186
187    #[test]
188    fn listen_last_resort_is_per_uid_tmp() {
189        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
190        unsafe {
191            std::env::remove_var("SYSKNIFE_LISTEN_URI");
192            std::env::remove_var("XDG_RUNTIME_DIR");
193        }
194        let u = default_listen_uri();
195        assert!(u.starts_with("unix:///tmp/sysknife-"));
196        assert!(u.ends_with(".sock"));
197    }
198}