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 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}