Skip to main content

running_process/client/
paths.rs

1//! Shared path computation for daemon socket, PID file, database, and shadow directory.
2//!
3//! Both the server and client modules use these functions to agree on where
4//! the daemon listens and where auxiliary files are stored.
5
6use std::path::PathBuf;
7
8/// Returns the local socket name the daemon listens on.
9///
10/// - **Linux/macOS**: `$XDG_RUNTIME_DIR/running-process/daemon{-hash}.sock`
11///   (fallback: `/tmp/running-process-{uid}/daemon{-hash}.sock`)
12/// - **Windows**: `\\.\pipe\running-process-daemon-{username}{-hash}`
13///
14/// On Windows the returned string is a full named pipe path that should be
15/// passed to [`interprocess::local_socket::ToNsName::to_ns_name`] with
16/// [`GenericNamespaced`](interprocess::local_socket::GenericNamespaced).
17/// On Unix it is a filesystem path for
18/// [`interprocess::local_socket::ToFsName::to_fs_name`] with
19/// [`GenericFilePath`](interprocess::local_socket::GenericFilePath).
20pub fn socket_path(scope_hash: Option<&str>) -> String {
21    #[cfg(unix)]
22    {
23        // Ensure the directory exists.
24        let _ = std::fs::create_dir_all(runtime_dir_unix());
25    }
26    socket_path_view(scope_hash)
27}
28
29/// Read-only variant of [`socket_path`]: derives the same endpoint string
30/// without creating any directory. Used by read-only inspectors (#391).
31pub fn socket_path_view(scope_hash: Option<&str>) -> String {
32    let suffix = match scope_hash {
33        Some(h) => format!("-{h}"),
34        None => String::new(),
35    };
36
37    #[cfg(windows)]
38    {
39        let username = std::env::var("USERNAME").unwrap_or_else(|_| "unknown".into());
40        format!(r"\\.\pipe\running-process-daemon-{username}{suffix}")
41    }
42
43    #[cfg(unix)]
44    {
45        format!("{}/daemon{suffix}.sock", runtime_dir_unix().display())
46    }
47}
48
49/// Build an `interprocess` local socket [`interprocess::local_socket::Name`]
50/// from the path returned by [`socket_path`].
51///
52/// This must use the same name-type dispatch as the server so that client
53/// and server agree on the actual IPC endpoint.
54pub fn make_socket_name(path: &str) -> std::io::Result<interprocess::local_socket::Name<'_>> {
55    use interprocess::local_socket::prelude::*;
56
57    #[cfg(unix)]
58    {
59        use interprocess::local_socket::GenericFilePath;
60        path.to_fs_name::<GenericFilePath>()
61    }
62
63    #[cfg(windows)]
64    {
65        use interprocess::local_socket::GenericNamespaced;
66        path.to_ns_name::<GenericNamespaced>()
67    }
68}
69
70/// Returns the path to the daemon PID file.
71///
72/// - **Linux/macOS**: same directory as the socket, with `.pid` extension.
73/// - **Windows**: `%LOCALAPPDATA%\running-process\daemon{-hash}.pid`
74pub fn pid_file_path(scope_hash: Option<&str>) -> PathBuf {
75    let path = pid_file_path_view(scope_hash);
76    if let Some(parent) = path.parent() {
77        let _ = std::fs::create_dir_all(parent);
78    }
79    path
80}
81
82/// Read-only variant of [`pid_file_path`]: derives the same path without
83/// creating any directory. Used by read-only inspectors (#391).
84pub fn pid_file_path_view(scope_hash: Option<&str>) -> PathBuf {
85    let suffix = match scope_hash {
86        Some(h) => format!("-{h}"),
87        None => String::new(),
88    };
89
90    #[cfg(windows)]
91    {
92        local_app_data_dir().join(format!("daemon{suffix}.pid"))
93    }
94
95    #[cfg(unix)]
96    {
97        runtime_dir_unix().join(format!("daemon{suffix}.pid"))
98    }
99}
100
101/// Returns the path to the daemon SQLite database.
102///
103/// - **Linux/macOS**: `$XDG_STATE_HOME/running-process/tracked-pids{-hash}.sqlite3`
104///   (fallback: `~/.local/state/running-process/tracked-pids{-hash}.sqlite3`)
105/// - **Windows**: `%LOCALAPPDATA%\running-process\tracked-pids{-hash}.sqlite3`
106pub fn db_path(scope_hash: Option<&str>) -> PathBuf {
107    let path = db_path_view(scope_hash);
108    if let Some(parent) = path.parent() {
109        let _ = std::fs::create_dir_all(parent);
110    }
111    path
112}
113
114/// Read-only variant of [`db_path`]: derives the same path without creating
115/// any directory. Used by read-only inspectors (#391).
116pub fn db_path_view(scope_hash: Option<&str>) -> PathBuf {
117    let suffix = match scope_hash {
118        Some(h) => format!("-{h}"),
119        None => String::new(),
120    };
121    data_dir().join(format!("tracked-pids{suffix}.sqlite3"))
122}
123
124/// Returns the shadow directory used for ephemeral run data.
125///
126/// - **Windows**: `%LOCALAPPDATA%\running-process\run\`
127/// - **Linux**: `$XDG_RUNTIME_DIR/running-process/run/`
128/// - **macOS**: `$HOME/Library/Caches/running-process/run/`
129pub fn shadow_dir() -> PathBuf {
130    let dir = shadow_dir_view();
131    let _ = std::fs::create_dir_all(&dir);
132    dir
133}
134
135/// Read-only variant of [`shadow_dir`]: derives the same path without
136/// creating any directory. Used by read-only inspectors (#391).
137pub fn shadow_dir_view() -> PathBuf {
138    #[cfg(windows)]
139    {
140        local_app_data_dir().join("run")
141    }
142
143    #[cfg(target_os = "macos")]
144    {
145        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
146        home.join("Library/Caches/running-process/run")
147    }
148
149    #[cfg(all(unix, not(target_os = "macos")))]
150    {
151        runtime_dir_unix().join("run")
152    }
153}
154
155/// Returns the daemon data directory (where the SQLite tracking database
156/// lives) WITHOUT creating it. Read-only callers (doctor, status probes)
157/// use this; [`db_path`] keeps its create-on-derive behavior.
158pub fn data_dir() -> PathBuf {
159    #[cfg(windows)]
160    {
161        local_app_data_dir()
162    }
163
164    #[cfg(unix)]
165    {
166        state_dir_unix()
167    }
168}
169
170// ---------------------------------------------------------------------------
171// Platform helpers
172// ---------------------------------------------------------------------------
173
174#[cfg(windows)]
175fn local_app_data_dir() -> PathBuf {
176    dirs::data_local_dir()
177        .unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"))
178        .join("running-process")
179}
180
181#[cfg(unix)]
182fn runtime_dir_unix() -> PathBuf {
183    if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR") {
184        PathBuf::from(d).join("running-process")
185    } else {
186        // Fallback: /tmp/running-process-{uid}
187        let uid = unsafe { libc::getuid() };
188        PathBuf::from(format!("/tmp/running-process-{uid}"))
189    }
190}
191
192#[cfg(unix)]
193fn state_dir_unix() -> PathBuf {
194    if let Some(d) = std::env::var_os("XDG_STATE_HOME") {
195        PathBuf::from(d).join("running-process")
196    } else if let Some(home) = dirs::home_dir() {
197        home.join(".local/state/running-process")
198    } else {
199        PathBuf::from("/tmp/running-process-state")
200    }
201}