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
22pub 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
30pub 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
41pub 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
85pub 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 tmp_root(&self) -> PathBuf {
168 self.workdir.join("tmp")
169 }
170
171 pub fn component_config_path(&self, component_name: &str) -> PathBuf {
172 self.config_root().join(format!("{component_name}.yaml"))
173 }
174
175 pub fn component_run_dir(&self, component_name: &str) -> PathBuf {
176 self.run_root().join(component_name)
177 }
178
179 pub fn component_data_dir(&self, component_name: &str) -> PathBuf {
180 self.data_root().join(component_name)
181 }
182
183 pub fn component_state_dir(&self, component_name: &str) -> PathBuf {
184 self.state_root().join(component_name)
185 }
186
187 pub fn component_system_dir(&self, component_name: &str) -> PathBuf {
188 self.system_root().join(component_name)
189 }
190
191 pub fn component_log_path(&self, component_name: &str) -> PathBuf {
192 self.log_root().join(format!("{component_name}.log"))
193 }
194
195 pub fn service_data_dir(&self, service_id: &str) -> PathBuf {
196 self.data_root().join("services").join(service_id)
197 }
198
199 pub fn service_run_dir(&self, service_id: &str) -> PathBuf {
200 self.run_root().join("services").join(service_id)
201 }
202
203 pub fn service_log_path(&self, service_id: &str) -> PathBuf {
204 self.log_root()
205 .join(format!("{}.log", normalized_service_log_name(service_id)))
206 }
207
208 pub fn identity_file_path(&self) -> PathBuf {
209 self.system_root().join("device_identity.json")
210 }
211
212 pub fn meowclient_socket_path(&self) -> PathBuf {
213 self.component_run_dir("meowclient").join("meowd.sock")
214 }
215
216 pub fn meowclient_pid_file(&self) -> PathBuf {
217 self.component_run_dir("meowclient").join("meowd.pid")
218 }
219
220 pub fn meowclient_ipc_token_file(&self) -> PathBuf {
221 self.component_run_dir("meowclient").join("meowd.ipc.token")
222 }
223
224 pub fn meowclient_named_pipe_name(&self) -> String {
225 meowclient_named_pipe_name_for_workdir(&self.workdir)
226 }
227
228 pub fn meowclient_endpoint_label(&self) -> String {
229 meowclient_endpoint_label_for_workdir(&self.workdir)
230 }
231}
232
233fn normalized_service_log_name(service_id: &str) -> String {
234 match service_id {
235 "meowcore" | "meow-core" => "meow-core".to_string(),
236 value if value.starts_with("node-") => value.to_string(),
237 value => format!("node-{value}"),
238 }
239}
240
241pub fn set_runtime_workdir(workdir: PathBuf) -> anyhow::Result<()> {
242 match RUNTIME_WORKDIR.get() {
243 Some(existing) if existing == &workdir => Ok(()),
244 Some(existing) => Err(anyhow::anyhow!(
245 "runtime workdir already set to {}, cannot change to {}",
246 existing.display(),
247 workdir.display()
248 )),
249 None => RUNTIME_WORKDIR
250 .set(workdir)
251 .map_err(|_| anyhow::anyhow!("failed to set runtime workdir")),
252 }
253}
254
255pub fn runtime_workdir() -> PathBuf {
256 RUNTIME_WORKDIR
257 .get()
258 .expect("runtime workdir is not initialized at process startup")
259 .clone()
260}
261
262pub fn runtime_paths() -> RuntimePaths {
263 RuntimePaths::new(runtime_workdir())
264}
265
266pub fn resolve_workdir(dev: bool) -> PathBuf {
267 let root = default_runtime_root().expect("failed to resolve default runtime workdir");
268 if dev {
269 root.join("dev")
270 } else {
271 root
272 }
273}
274
275pub fn default_runtime_root() -> anyhow::Result<PathBuf> {
276 #[cfg(windows)]
277 {
278 let local_app_data = std::env::var_os("LOCALAPPDATA")
279 .filter(|value| !value.is_empty())
280 .ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is not set"))?;
281 return Ok(PathBuf::from(local_app_data).join(".meow"));
282 }
283
284 #[cfg(not(windows))]
285 {
286 let home = home_dir().ok_or_else(|| anyhow::anyhow!("HOME is not set"))?;
287 Ok(home.join(".meow"))
288 }
289}
290
291#[cfg(not(windows))]
292fn home_dir() -> Option<PathBuf> {
293 std::env::var_os("HOME")
294 .filter(|value| !value.is_empty())
295 .map(PathBuf::from)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn service_log_paths_use_standard_log_names() {
304 let paths = RuntimePaths::for_workdir("/tmp/meow-runtime-test");
305
306 assert_eq!(
307 paths.service_log_path("camera"),
308 PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
309 );
310 assert_eq!(
311 paths.service_log_path("node-camera"),
312 PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
313 );
314 assert_eq!(
315 paths.service_log_path("meowcore"),
316 PathBuf::from("/tmp/meow-runtime-test/log/meow-core.log")
317 );
318 }
319
320 #[test]
321 fn tmp_root_is_workdir_scoped() {
322 let paths = RuntimePaths::for_workdir("/tmp/meow-runtime-test");
323 assert_eq!(
324 paths.tmp_root(),
325 PathBuf::from("/tmp/meow-runtime-test/tmp")
326 );
327 }
328
329 #[cfg(not(windows))]
330 #[test]
331 fn default_runtime_root_uses_dot_meow_under_home() {
332 let root = default_runtime_root().unwrap();
333 assert_eq!(
334 root.file_name().and_then(|value| value.to_str()),
335 Some(".meow")
336 );
337 }
338}