Skip to main content

tau_agent_base/
paths.rs

1//! Shared XDG path resolution for tau directories.
2//!
3//! When `$HOME` is unset (e.g. in containers or minimal environments), the
4//! fallback paths use subdirectories under `/tmp`:
5//!
6//! - config: `/tmp/tau-config/`
7//! - data:   `/tmp/tau-data/`
8//! - runtime: `/tmp/tau-{PID}/`
9//!
10//! This differs from the pre-extraction per-file fallbacks (e.g.
11//! `/tmp/tau-auth.json`, `/tmp/tau.db`) which were flat files in `/tmp`.
12//! The subdirectory approach is intentional: it keeps tau files namespaced
13//! under their own directories even in the fallback case, avoids collisions
14//! with unrelated files, and allows `create_dir_all` to work uniformly
15//! (callers always join a filename onto a directory).
16
17use std::path::PathBuf;
18
19/// Returns the tau config directory (`$XDG_CONFIG_HOME/tau` or `~/.config/tau`).
20///
21/// Fallback when `$HOME` is unset: `/tmp/tau-config`.
22pub fn config_dir() -> PathBuf {
23    if let Ok(config) = std::env::var("XDG_CONFIG_HOME") {
24        PathBuf::from(config).join("tau")
25    } else if let Ok(home) = std::env::var("HOME") {
26        PathBuf::from(home).join(".config").join("tau")
27    } else {
28        PathBuf::from("/tmp").join("tau-config")
29    }
30}
31
32/// Returns the tau data directory (`$XDG_DATA_HOME/tau` or `~/.local/share/tau`).
33///
34/// Fallback when `$HOME` is unset: `/tmp/tau-data`.
35pub fn data_dir() -> PathBuf {
36    if let Ok(data) = std::env::var("XDG_DATA_HOME") {
37        PathBuf::from(data).join("tau")
38    } else if let Ok(home) = std::env::var("HOME") {
39        PathBuf::from(home).join(".local").join("share").join("tau")
40    } else {
41        PathBuf::from("/tmp").join("tau-data")
42    }
43}
44
45/// Returns the tau runtime directory (`$XDG_RUNTIME_DIR/tau` or `~/.tau`).
46///
47/// Fallback when `$HOME` is unset: `/tmp/tau-{PID}` (per-process).
48pub fn runtime_dir() -> PathBuf {
49    if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
50        PathBuf::from(dir).join("tau")
51    } else if let Ok(home) = std::env::var("HOME") {
52        PathBuf::from(home).join(".tau")
53    } else {
54        PathBuf::from("/tmp").join(format!("tau-{}", std::process::id()))
55    }
56}
57
58/// Returns the tau state directory (`$XDG_STATE_HOME/tau` or `~/.local/state/tau`).
59///
60/// Fallback when `$HOME` is unset: `/tmp/tau-state`.
61///
62/// State directory is for data that survives restarts but is not
63/// user-editable config: logs, crash dumps, internal checkpoints.
64pub fn state_dir() -> PathBuf {
65    if let Ok(state) = std::env::var("XDG_STATE_HOME") {
66        PathBuf::from(state).join("tau")
67    } else if let Ok(home) = std::env::var("HOME") {
68        PathBuf::from(home).join(".local").join("state").join("tau")
69    } else {
70        PathBuf::from("/tmp").join("tau-state")
71    }
72}
73
74/// Returns the tau logs directory (`state_dir()/logs`).
75pub fn logs_dir() -> PathBuf {
76    state_dir().join("logs")
77}
78
79/// Returns the default socket path for the tau server.
80pub fn socket_path() -> PathBuf {
81    runtime_dir().join("tau.sock")
82}
83
84/// Returns the PID file path next to the socket.
85pub fn pid_path() -> PathBuf {
86    let mut p = socket_path();
87    p.set_file_name("tau.pid");
88    p
89}
90
91/// Returns the operator config directory for a project.
92///
93/// `~/.config/tau/projects/{name}/`
94pub fn project_config_dir(name: &str) -> PathBuf {
95    config_dir().join("projects").join(name)
96}
97
98/// Check if a server is already running by trying to connect.
99pub fn is_running() -> bool {
100    std::os::unix::net::UnixStream::connect(socket_path()).is_ok()
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    // Tests in this module mutate process-global env vars. We share the
108    // crate-level `TEST_ENV_MUTEX` with `config_chain` and `project` so
109    // env-var mutations stay serialized across sibling test modules —
110    // otherwise e.g. config_chain's HOME/XDG_CONFIG_HOME flip can race
111    // with paths::state_dir_falls_back_to_tmp_when_neither_set.
112    use crate::TEST_ENV_MUTEX as ENV_LOCK;
113
114    struct EnvSnapshot {
115        xdg_state: Option<String>,
116        xdg_config: Option<String>,
117        xdg_data: Option<String>,
118        xdg_runtime: Option<String>,
119        home: Option<String>,
120    }
121
122    impl EnvSnapshot {
123        fn capture() -> Self {
124            Self {
125                xdg_state: std::env::var("XDG_STATE_HOME").ok(),
126                xdg_config: std::env::var("XDG_CONFIG_HOME").ok(),
127                xdg_data: std::env::var("XDG_DATA_HOME").ok(),
128                xdg_runtime: std::env::var("XDG_RUNTIME_DIR").ok(),
129                home: std::env::var("HOME").ok(),
130            }
131        }
132
133        fn restore(self) {
134            fn set(k: &str, v: Option<String>) {
135                // SAFETY: serialized by ENV_LOCK; no other threads should be
136                // touching these env vars for the duration of a test.
137                unsafe {
138                    match v {
139                        Some(v) => std::env::set_var(k, v),
140                        None => std::env::remove_var(k),
141                    }
142                }
143            }
144            set("XDG_STATE_HOME", self.xdg_state);
145            set("XDG_CONFIG_HOME", self.xdg_config);
146            set("XDG_DATA_HOME", self.xdg_data);
147            set("XDG_RUNTIME_DIR", self.xdg_runtime);
148            set("HOME", self.home);
149        }
150    }
151
152    fn set_var(k: &str, v: &str) {
153        // SAFETY: serialized by ENV_LOCK.
154        unsafe {
155            std::env::set_var(k, v);
156        }
157    }
158
159    fn remove_var(k: &str) {
160        // SAFETY: serialized by ENV_LOCK.
161        unsafe {
162            std::env::remove_var(k);
163        }
164    }
165
166    #[test]
167    fn state_dir_uses_xdg_state_home_when_set() {
168        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
169        let snap = EnvSnapshot::capture();
170        set_var("XDG_STATE_HOME", "/custom/state");
171        set_var("HOME", "/home/ignored");
172        assert_eq!(state_dir(), PathBuf::from("/custom/state/tau"));
173        assert_eq!(logs_dir(), PathBuf::from("/custom/state/tau/logs"));
174        snap.restore();
175    }
176
177    #[test]
178    fn state_dir_uses_home_when_xdg_unset() {
179        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
180        let snap = EnvSnapshot::capture();
181        remove_var("XDG_STATE_HOME");
182        set_var("HOME", "/home/alice");
183        assert_eq!(state_dir(), PathBuf::from("/home/alice/.local/state/tau"));
184        assert_eq!(
185            logs_dir(),
186            PathBuf::from("/home/alice/.local/state/tau/logs")
187        );
188        snap.restore();
189    }
190
191    #[test]
192    fn state_dir_falls_back_to_tmp_when_neither_set() {
193        let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
194        let snap = EnvSnapshot::capture();
195        remove_var("XDG_STATE_HOME");
196        remove_var("HOME");
197        assert_eq!(state_dir(), PathBuf::from("/tmp/tau-state"));
198        assert_eq!(logs_dir(), PathBuf::from("/tmp/tau-state/logs"));
199        snap.restore();
200    }
201}