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