1use once_cell::sync::Lazy;
2pub use std::env::*;
3use std::path::PathBuf;
4
5pub static PITCHFORK_BIN: Lazy<PathBuf> = Lazy::new(|| {
6 current_exe()
7 .and_then(|p| p.canonicalize())
8 .unwrap_or_else(|e| {
9 eprintln!("Warning: Could not determine pitchfork binary path: {e}");
10 args()
11 .next()
12 .map(PathBuf::from)
13 .unwrap_or_else(|| PathBuf::from("pitchfork"))
14 })
15});
16pub static CWD: Lazy<PathBuf> = Lazy::new(|| current_dir().unwrap_or_else(|_| PathBuf::from(".")));
17
18pub static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| {
19 #[cfg(unix)]
29 if nix::unistd::Uid::effective().is_root()
30 && let Ok(sudo_user) = std::env::var("SUDO_USER")
31 && let Some(home) = home_dir_for_user(&sudo_user)
32 {
33 return home;
34 }
35 dirs::home_dir().unwrap_or_else(|| {
36 eprintln!("Warning: Could not determine home directory");
37 PathBuf::from("/tmp")
38 })
39});
40pub static PITCHFORK_CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| {
41 var_path("PITCHFORK_CONFIG_DIR").unwrap_or(HOME_DIR.join(".config").join("pitchfork"))
42});
43pub static PITCHFORK_GLOBAL_CONFIG_USER: Lazy<PathBuf> =
44 Lazy::new(|| PITCHFORK_CONFIG_DIR.join("config.toml"));
45pub static PITCHFORK_GLOBAL_CONFIG_SYSTEM: Lazy<PathBuf> =
46 Lazy::new(|| PathBuf::from("/etc/pitchfork/config.toml"));
47pub static PITCHFORK_STATE_DIR: Lazy<PathBuf> = Lazy::new(|| {
48 if let Some(p) = var_path("PITCHFORK_STATE_DIR") {
49 return p;
50 }
51 #[cfg(unix)]
52 if nix::unistd::Uid::effective().is_root()
53 && let Some(home) = configured_supervisor_user_home_dir()
54 {
55 return home.join(".local").join("state").join("pitchfork");
56 }
57 #[cfg(unix)]
60 if nix::unistd::Uid::effective().is_root() {
61 return HOME_DIR.join(".local").join("state").join("pitchfork");
62 }
63 dirs::state_dir()
64 .unwrap_or_else(|| HOME_DIR.join(".local").join("state"))
65 .join("pitchfork")
66});
67pub static PITCHFORK_STATE_FILE: Lazy<PathBuf> =
68 Lazy::new(|| PITCHFORK_STATE_DIR.join("state.toml"));
69pub static PITCHFORK_HOSTS_FILE: Lazy<PathBuf> = Lazy::new(|| {
74 if let Some(p) = var_path("PITCHFORK_HOSTS_FILE") {
75 return p;
76 }
77 if cfg!(windows) {
78 let system_root = var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string());
79 PathBuf::from(system_root)
80 .join("System32")
81 .join("drivers")
82 .join("etc")
83 .join("hosts")
84 } else {
85 PathBuf::from("/etc/hosts")
86 }
87});
88pub static PITCHFORK_LOG: Lazy<log::LevelFilter> =
89 Lazy::new(|| var_log_level("PITCHFORK_LOG").unwrap_or(log::LevelFilter::Info));
90pub static PITCHFORK_LOG_FILE_LEVEL: Lazy<log::LevelFilter> =
91 Lazy::new(|| var_log_level("PITCHFORK_LOG_FILE_LEVEL").unwrap_or(*PITCHFORK_LOG));
92pub static PITCHFORK_LOGS_DIR: Lazy<PathBuf> =
93 Lazy::new(|| var_path("PITCHFORK_LOGS_DIR").unwrap_or(PITCHFORK_STATE_DIR.join("logs")));
94pub static PITCHFORK_LOG_FILE: Lazy<PathBuf> =
95 Lazy::new(|| PITCHFORK_LOGS_DIR.join("pitchfork").join("pitchfork.log"));
96#[cfg(unix)]
100pub static IPC_SOCK_DIR: Lazy<PathBuf> = Lazy::new(|| PITCHFORK_STATE_DIR.join("sock"));
101#[cfg(unix)]
102pub static IPC_SOCK_MAIN: Lazy<PathBuf> = Lazy::new(|| IPC_SOCK_DIR.join("main.sock"));
103
104pub static ORIGINAL_PATH: Lazy<Option<String>> = Lazy::new(|| var("PATH").ok());
106pub static IPC_JSON: Lazy<bool> = Lazy::new(|| !var_false("IPC_JSON"));
107
108pub fn expand_tilde(path: impl AsRef<std::path::Path>) -> PathBuf {
114 expand_tilde_for_user(path, None)
115}
116
117pub fn expand_tilde_for_user(path: impl AsRef<std::path::Path>, user: Option<&str>) -> PathBuf {
125 let path = path.as_ref();
126 match path.strip_prefix("~") {
127 Ok(rest) => home_dir_for_effective_user(user).join(rest),
128 Err(_) => path.to_path_buf(),
129 }
130}
131
132fn var_path(name: &str) -> Option<PathBuf> {
133 var(name).map(expand_tilde).ok()
134}
135
136fn var_log_level(name: &str) -> Option<log::LevelFilter> {
137 var(name).ok().and_then(|level| level.parse().ok())
138}
139
140fn var_false(name: &str) -> bool {
141 var(name)
142 .map(|val| val.to_lowercase())
143 .map(|val| val == "false" || val == "0")
144 .unwrap_or(false)
145}
146
147#[cfg(unix)]
157fn home_dir_for_user(username: &str) -> Option<PathBuf> {
158 nix::unistd::User::from_name(username)
159 .ok()
160 .flatten()
161 .map(|u| u.dir)
162}
163
164#[cfg(unix)]
166fn home_dir_by_user_spec(user: &str) -> Option<PathBuf> {
167 if user.chars().all(|c| c.is_ascii_digit()) {
168 let uid = user.parse::<u32>().ok()?;
169 nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid))
170 .ok()
171 .flatten()
172 .map(|u| u.dir)
173 } else {
174 home_dir_for_user(user)
175 }
176}
177
178#[cfg(unix)]
182pub(crate) fn home_dir_for_effective_user(user: Option<&str>) -> PathBuf {
183 let user = user.map(str::trim).filter(|u| !u.is_empty());
184 match user {
185 Some(u) => home_dir_by_user_spec(u).unwrap_or_else(|| HOME_DIR.clone()),
186 None => HOME_DIR.clone(),
187 }
188}
189
190#[cfg(not(unix))]
191pub(crate) fn home_dir_for_effective_user(_user: Option<&str>) -> PathBuf {
192 HOME_DIR.clone()
193}
194
195#[cfg(unix)]
196fn configured_supervisor_user_home_dir() -> Option<PathBuf> {
197 let s = crate::settings::settings();
198 let user = s.supervisor.user.trim();
199 if user.is_empty() {
200 return None;
201 }
202 home_dir_by_user_spec(user)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::path::Path;
209
210 #[test]
211 fn expand_tilde_replaces_home_prefix() {
212 assert_eq!(
213 expand_tilde("~/projects/api"),
214 HOME_DIR.join("projects/api")
215 );
216 assert_eq!(expand_tilde("~"), *HOME_DIR);
217 }
218
219 #[test]
220 fn expand_tilde_leaves_other_paths_unchanged() {
221 assert_eq!(
222 expand_tilde("/srv/projects/api"),
223 Path::new("/srv/projects/api")
224 );
225 assert_eq!(expand_tilde("projects/api"), Path::new("projects/api"));
226 assert_eq!(expand_tilde("~other/api"), Path::new("~other/api"));
227 }
228
229 #[test]
230 fn expand_tilde_for_user_none_uses_supervisor_home() {
231 assert_eq!(expand_tilde_for_user("~/data", None), HOME_DIR.join("data"));
232 }
233
234 #[test]
235 fn expand_tilde_for_user_empty_uses_supervisor_home() {
236 assert_eq!(
237 expand_tilde_for_user("~/data", Some("")),
238 HOME_DIR.join("data")
239 );
240 }
241
242 #[test]
243 fn expand_tilde_for_user_nonexistent_falls_back_to_supervisor_home() {
244 assert_eq!(
245 expand_tilde_for_user("~/data", Some("nonexistent_user_xyz")),
246 HOME_DIR.join("data")
247 );
248 }
249
250 #[test]
251 fn expand_tilde_for_user_leaves_non_tilde_unchanged() {
252 assert_eq!(
253 expand_tilde_for_user("/srv/api", Some("postgres")),
254 Path::new("/srv/api")
255 );
256 }
257}