Skip to main content

runtime_paths/
lib.rs

1mod endpoint;
2#[cfg(unix)]
3mod unix_process;
4#[cfg(windows)]
5mod windows_process;
6
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::OnceLock;
10
11pub use endpoint::{
12    meowclient_endpoint_label_for_workdir, meowclient_named_pipe_name_for_workdir,
13    workdir_identity_hash,
14};
15#[cfg(unix)]
16pub use unix_process::{
17    clear_stale_unix_socket, is_pid_alive, process_executable_path, unix_peer_pid,
18};
19#[cfg(windows)]
20pub use windows_process::{is_pid_alive, process_executable_path};
21
22/// Compare process image paths, preferring canonical forms when available.
23pub fn paths_match(actual: &Path, expected: &Path) -> bool {
24    match (actual.canonicalize(), expected.canonicalize()) {
25        (Ok(actual), Ok(expected)) => actual == expected,
26        _ => actual == expected,
27    }
28}
29
30/// True when `pid` is alive and its executable matches `expected_binary`.
31pub fn pid_matches_executable(pid: u32, expected_binary: &Path) -> bool {
32    if !is_pid_alive(pid) {
33        return false;
34    }
35    match process_executable_path(pid) {
36        Some(path) => paths_match(&path, expected_binary),
37        None => false,
38    }
39}
40
41/// If `pid_file` names a live process whose executable matches `expected_binary`,
42/// returns its pid. Missing files return `Ok(None)`. Dead processes have their pid
43/// file removed and return `Ok(None)`. Live processes with a different executable
44/// are left untouched and return `Ok(None)`.
45pub fn owned_daemon_pid_from_file(
46    pid_file: &Path,
47    expected_binary: &Path,
48) -> Result<Option<u32>, String> {
49    let raw = match fs::read_to_string(pid_file) {
50        Ok(value) => value,
51        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
52        Err(err) => {
53            return Err(format!(
54                "failed to read daemon pid file {}: {}",
55                pid_file.display(),
56                err
57            ))
58        }
59    };
60    let pid = raw
61        .trim()
62        .parse::<u32>()
63        .map_err(|err| format!("invalid daemon pid in {}: {}", pid_file.display(), err))?;
64
65    if !is_pid_alive(pid) {
66        match fs::remove_file(pid_file) {
67            Ok(()) => {}
68            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
69            Err(err) => {
70                return Err(format!(
71                    "failed to remove stale daemon pid file {}: {}",
72                    pid_file.display(),
73                    err
74                ))
75            }
76        }
77        return Ok(None);
78    }
79    if !pid_matches_executable(pid, expected_binary) {
80        return Ok(None);
81    }
82    Ok(Some(pid))
83}
84
85/// Convenience wrapper: verify the default meowclient pid file against `expected_binary`.
86pub fn owned_meowclient_pid(expected_binary: &Path) -> Result<Option<u32>, String> {
87    owned_daemon_pid_from_file(&runtime_paths().meowclient_pid_file(), expected_binary)
88}
89
90static RUNTIME_WORKDIR: OnceLock<PathBuf> = OnceLock::new();
91
92#[derive(Debug, Clone)]
93pub struct RuntimePaths {
94    workdir: PathBuf,
95}
96
97impl RuntimePaths {
98    pub fn new(workdir: PathBuf) -> Self {
99        Self { workdir }
100    }
101
102    pub fn for_workdir(workdir: impl AsRef<Path>) -> Self {
103        Self::new(workdir.as_ref().to_path_buf())
104    }
105
106    pub fn workdir(&self) -> &Path {
107        &self.workdir
108    }
109
110    pub fn system_root(&self) -> PathBuf {
111        self.workdir.join("system")
112    }
113
114    pub fn services_root(&self) -> PathBuf {
115        self.service_packages_root()
116    }
117
118    pub fn packages_root(&self) -> PathBuf {
119        self.workdir.join("packages")
120    }
121
122    pub fn service_packages_root(&self) -> PathBuf {
123        self.packages_root().join("services")
124    }
125
126    pub fn service_package_dir(&self, service_id: &str) -> PathBuf {
127        self.service_packages_root().join(service_id)
128    }
129
130    pub fn config_root(&self) -> PathBuf {
131        self.workdir.join("config")
132    }
133
134    pub fn run_root(&self) -> PathBuf {
135        self.workdir.join("run")
136    }
137
138    pub fn log_root(&self) -> PathBuf {
139        self.workdir.join("log")
140    }
141
142    pub fn data_root(&self) -> PathBuf {
143        self.workdir.join("data")
144    }
145
146    pub fn state_root(&self) -> PathBuf {
147        self.workdir.join("state")
148    }
149
150    pub fn downloads_root(&self) -> PathBuf {
151        self.workdir.join("downloads")
152    }
153
154    pub fn tools_root(&self) -> PathBuf {
155        self.workdir.join("tools")
156    }
157
158    pub fn downloads_tools_root(&self) -> PathBuf {
159        self.downloads_root().join("tools")
160    }
161
162    pub fn artifacts_root(&self) -> PathBuf {
163        self.workdir.join("artifacts")
164    }
165
166    pub fn component_config_path(&self, component_name: &str) -> PathBuf {
167        self.config_root().join(format!("{component_name}.yaml"))
168    }
169
170    pub fn component_run_dir(&self, component_name: &str) -> PathBuf {
171        self.run_root().join(component_name)
172    }
173
174    pub fn component_data_dir(&self, component_name: &str) -> PathBuf {
175        self.data_root().join(component_name)
176    }
177
178    pub fn component_state_dir(&self, component_name: &str) -> PathBuf {
179        self.state_root().join(component_name)
180    }
181
182    pub fn component_system_dir(&self, component_name: &str) -> PathBuf {
183        self.system_root().join(component_name)
184    }
185
186    pub fn component_log_path(&self, component_name: &str) -> PathBuf {
187        self.log_root().join(format!("{component_name}.log"))
188    }
189
190    pub fn service_data_dir(&self, service_id: &str) -> PathBuf {
191        self.data_root().join("services").join(service_id)
192    }
193
194    pub fn service_run_dir(&self, service_id: &str) -> PathBuf {
195        self.run_root().join("services").join(service_id)
196    }
197
198    pub fn service_log_path(&self, service_id: &str) -> PathBuf {
199        self.log_root()
200            .join(format!("{}.log", normalized_service_log_name(service_id)))
201    }
202
203    pub fn identity_file_path(&self) -> PathBuf {
204        self.system_root().join("device_identity.json")
205    }
206
207    pub fn meowclient_socket_path(&self) -> PathBuf {
208        self.component_run_dir("meowclient").join("meowd.sock")
209    }
210
211    pub fn meowclient_pid_file(&self) -> PathBuf {
212        self.component_run_dir("meowclient").join("meowd.pid")
213    }
214
215    pub fn meowclient_ipc_token_file(&self) -> PathBuf {
216        self.component_run_dir("meowclient").join("meowd.ipc.token")
217    }
218
219    pub fn meowclient_named_pipe_name(&self) -> String {
220        meowclient_named_pipe_name_for_workdir(&self.workdir)
221    }
222
223    pub fn meowclient_endpoint_label(&self) -> String {
224        meowclient_endpoint_label_for_workdir(&self.workdir)
225    }
226}
227
228fn normalized_service_log_name(service_id: &str) -> String {
229    match service_id {
230        "meowcore" | "meow-core" => "meow-core".to_string(),
231        value if value.starts_with("node-") => value.to_string(),
232        value => format!("node-{value}"),
233    }
234}
235
236pub fn set_runtime_workdir(workdir: PathBuf) -> anyhow::Result<()> {
237    match RUNTIME_WORKDIR.get() {
238        Some(existing) if existing == &workdir => Ok(()),
239        Some(existing) => Err(anyhow::anyhow!(
240            "runtime workdir already set to {}, cannot change to {}",
241            existing.display(),
242            workdir.display()
243        )),
244        None => RUNTIME_WORKDIR
245            .set(workdir)
246            .map_err(|_| anyhow::anyhow!("failed to set runtime workdir")),
247    }
248}
249
250pub fn runtime_workdir() -> PathBuf {
251    RUNTIME_WORKDIR
252        .get()
253        .expect("runtime workdir is not initialized at process startup")
254        .clone()
255}
256
257pub fn runtime_paths() -> RuntimePaths {
258    RuntimePaths::new(runtime_workdir())
259}
260
261pub fn resolve_workdir(dev: bool) -> PathBuf {
262    let root = default_runtime_root().expect("failed to resolve default runtime workdir");
263    if dev {
264        root.join("dev")
265    } else {
266        root
267    }
268}
269
270pub fn default_runtime_root() -> anyhow::Result<PathBuf> {
271    #[cfg(windows)]
272    {
273        let local_app_data = std::env::var_os("LOCALAPPDATA")
274            .filter(|value| !value.is_empty())
275            .ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is not set"))?;
276        return Ok(PathBuf::from(local_app_data).join(".meow"));
277    }
278
279    #[cfg(not(windows))]
280    {
281        let home = home_dir().ok_or_else(|| anyhow::anyhow!("HOME is not set"))?;
282        Ok(home.join(".meow"))
283    }
284}
285
286#[cfg(not(windows))]
287fn home_dir() -> Option<PathBuf> {
288    std::env::var_os("HOME")
289        .filter(|value| !value.is_empty())
290        .map(PathBuf::from)
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn service_log_paths_use_standard_log_names() {
299        let paths = RuntimePaths::for_workdir("/tmp/meow-runtime-test");
300
301        assert_eq!(
302            paths.service_log_path("camera"),
303            PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
304        );
305        assert_eq!(
306            paths.service_log_path("node-camera"),
307            PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
308        );
309        assert_eq!(
310            paths.service_log_path("meowcore"),
311            PathBuf::from("/tmp/meow-runtime-test/log/meow-core.log")
312        );
313    }
314
315    #[cfg(not(windows))]
316    #[test]
317    fn default_runtime_root_uses_dot_meow_under_home() {
318        let root = default_runtime_root().unwrap();
319        assert_eq!(
320            root.file_name().and_then(|value| value.to_str()),
321            Some(".meow")
322        );
323    }
324}