mod endpoint;
mod env;
pub use env::RuntimeEnv;
#[cfg(unix)]
mod unix_process;
#[cfg(windows)]
mod windows_process;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
pub use endpoint::{
meowclient_endpoint_label_for_workdir, meowclient_named_pipe_name_for_workdir,
workdir_identity_hash,
};
#[cfg(unix)]
pub use unix_process::{
clear_stale_unix_socket, is_pid_alive, process_executable_path, unix_peer_pid,
};
#[cfg(windows)]
pub use windows_process::{is_pid_alive, process_executable_path};
pub fn paths_match(actual: &Path, expected: &Path) -> bool {
match (actual.canonicalize(), expected.canonicalize()) {
(Ok(actual), Ok(expected)) => actual == expected,
_ => actual == expected,
}
}
pub fn pid_matches_executable(pid: u32, expected_binary: &Path) -> bool {
if !is_pid_alive(pid) {
return false;
}
match process_executable_path(pid) {
Some(path) => paths_match(&path, expected_binary),
None => false,
}
}
pub fn owned_daemon_pid_from_file(
pid_file: &Path,
expected_binary: &Path,
) -> Result<Option<u32>, String> {
let raw = match fs::read_to_string(pid_file) {
Ok(value) => value,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
return Err(format!(
"failed to read daemon pid file {}: {}",
pid_file.display(),
err
))
}
};
let pid = raw
.trim()
.parse::<u32>()
.map_err(|err| format!("invalid daemon pid in {}: {}", pid_file.display(), err))?;
if !is_pid_alive(pid) {
match fs::remove_file(pid_file) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
return Err(format!(
"failed to remove stale daemon pid file {}: {}",
pid_file.display(),
err
))
}
}
return Ok(None);
}
if !pid_matches_executable(pid, expected_binary) {
return Ok(None);
}
Ok(Some(pid))
}
pub fn owned_meowclient_pid(expected_binary: &Path) -> Result<Option<u32>, String> {
owned_daemon_pid_from_file(&runtime_paths().meowclient_pid_file(), expected_binary)
}
static RUNTIME_WORKDIR: OnceLock<(PathBuf, RuntimeEnv)> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct RuntimePaths {
workdir: PathBuf,
}
impl RuntimePaths {
pub fn new(workdir: PathBuf) -> Self {
Self { workdir }
}
pub fn for_workdir(workdir: impl AsRef<Path>) -> Self {
Self::new(workdir.as_ref().to_path_buf())
}
pub fn workdir(&self) -> &Path {
&self.workdir
}
pub fn system_root(&self) -> PathBuf {
self.workdir.join("system")
}
pub fn services_root(&self) -> PathBuf {
self.service_packages_root()
}
pub fn packages_root(&self) -> PathBuf {
self.workdir.join("packages")
}
pub fn service_packages_root(&self) -> PathBuf {
self.packages_root().join("services")
}
pub fn service_package_dir(&self, service_id: &str) -> PathBuf {
self.service_packages_root().join(service_id)
}
pub fn config_root(&self) -> PathBuf {
self.workdir.join("config")
}
pub fn run_root(&self) -> PathBuf {
self.workdir.join("run")
}
pub fn log_root(&self) -> PathBuf {
self.workdir.join("log")
}
pub fn data_root(&self) -> PathBuf {
self.workdir.join("data")
}
pub fn state_root(&self) -> PathBuf {
self.workdir.join("state")
}
pub fn downloads_root(&self) -> PathBuf {
self.workdir.join("downloads")
}
pub fn tools_root(&self) -> PathBuf {
self.workdir.join("tools")
}
pub fn downloads_tools_root(&self) -> PathBuf {
self.downloads_root().join("tools")
}
pub fn artifacts_root(&self) -> PathBuf {
self.workdir.join("artifacts")
}
pub fn tmp_root(&self) -> PathBuf {
self.workdir.join("tmp")
}
pub fn component_config_path(&self, component_name: &str) -> PathBuf {
self.config_root().join(format!("{component_name}.yaml"))
}
pub fn component_run_dir(&self, component_name: &str) -> PathBuf {
self.run_root().join(component_name)
}
pub fn component_data_dir(&self, component_name: &str) -> PathBuf {
self.data_root().join(component_name)
}
pub fn component_state_dir(&self, component_name: &str) -> PathBuf {
self.state_root().join(component_name)
}
pub fn component_system_dir(&self, component_name: &str) -> PathBuf {
self.system_root().join(component_name)
}
pub fn component_log_path(&self, component_name: &str) -> PathBuf {
self.log_root().join(format!("{component_name}.log"))
}
pub fn service_data_dir(&self, service_id: &str) -> PathBuf {
self.data_root().join("services").join(service_id)
}
pub fn service_run_dir(&self, service_id: &str) -> PathBuf {
self.run_root().join("services").join(service_id)
}
pub fn service_log_path(&self, service_id: &str) -> PathBuf {
self.log_root()
.join(format!("{}.log", normalized_service_log_name(service_id)))
}
pub fn identity_file_path(&self) -> PathBuf {
self.system_root().join("device_identity.json")
}
pub fn meowclient_socket_path(&self) -> PathBuf {
self.component_run_dir("meowclient").join("meowd.sock")
}
pub fn meowclient_pid_file(&self) -> PathBuf {
self.component_run_dir("meowclient").join("meowd.pid")
}
pub fn meowclient_ipc_token_file(&self) -> PathBuf {
self.component_run_dir("meowclient").join("meowd.ipc.token")
}
pub fn meowclient_named_pipe_name(&self) -> String {
meowclient_named_pipe_name_for_workdir(&self.workdir)
}
pub fn meowclient_endpoint_label(&self) -> String {
meowclient_endpoint_label_for_workdir(&self.workdir)
}
}
fn normalized_service_log_name(service_id: &str) -> String {
match service_id {
"meowcore" | "meow-core" => "meow-core".to_string(),
value if value.starts_with("node-") => value.to_string(),
value => format!("node-{value}"),
}
}
pub fn set_runtime_workdir(workdir: PathBuf) -> anyhow::Result<()> {
set_runtime_context(workdir, RuntimeEnv::Prod)
}
pub fn set_runtime_context(workdir: PathBuf, env: RuntimeEnv) -> anyhow::Result<()> {
anyhow::ensure!(workdir.is_absolute(), "runtime workdir must be absolute");
match RUNTIME_WORKDIR.get() {
Some((existing, existing_env)) if existing == &workdir && *existing_env == env => Ok(()),
Some((existing, _)) => Err(anyhow::anyhow!(
"runtime workdir/environment already set to {}, cannot change to {}",
existing.display(),
workdir.display()
)),
None => RUNTIME_WORKDIR
.set((workdir, env))
.map_err(|_| anyhow::anyhow!("failed to set runtime workdir")),
}
}
pub fn runtime_workdir() -> PathBuf {
RUNTIME_WORKDIR
.get()
.expect("runtime workdir is not initialized at process startup")
.0
.clone()
}
pub fn runtime_env() -> RuntimeEnv {
RUNTIME_WORKDIR
.get()
.map(|(_, env)| *env)
.unwrap_or_default()
}
pub fn runtime_paths() -> RuntimePaths {
RuntimePaths::new(runtime_workdir())
}
pub fn resolve_runtime_workdir(home: Option<&Path>, env: RuntimeEnv) -> anyhow::Result<PathBuf> {
let home = match home {
Some(home) => home.to_path_buf(),
None => default_runtime_root()?,
};
anyhow::ensure!(home.is_absolute(), "runtime home must be absolute");
let workdir = if env.is_prod() {
home.clone()
} else {
home.join(env.as_str())
};
if !env.is_prod() {
match std::fs::symlink_metadata(&workdir) {
Ok(metadata) => anyhow::ensure!(
!metadata.file_type().is_symlink(),
"Dev/E2E workdir must not redirect to another environment"
),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
Ok(workdir)
}
pub fn resolve_workdir(dev: bool) -> PathBuf {
resolve_runtime_workdir(
None,
if dev {
RuntimeEnv::Dev
} else {
RuntimeEnv::Prod
},
)
.expect("failed to resolve default runtime workdir")
}
pub fn default_runtime_root() -> anyhow::Result<PathBuf> {
#[cfg(windows)]
{
let local_app_data = std::env::var_os("LOCALAPPDATA")
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is not set"))?;
return Ok(PathBuf::from(local_app_data).join(".meow"));
}
#[cfg(not(windows))]
{
let home = home_dir().ok_or_else(|| anyhow::anyhow!("HOME is not set"))?;
Ok(home.join(".meow"))
}
}
#[cfg(not(windows))]
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_log_paths_use_standard_log_names() {
let paths = RuntimePaths::for_workdir("/tmp/meow-runtime-test");
assert_eq!(
paths.service_log_path("camera"),
PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
);
assert_eq!(
paths.service_log_path("node-camera"),
PathBuf::from("/tmp/meow-runtime-test/log/node-camera.log")
);
assert_eq!(
paths.service_log_path("meowcore"),
PathBuf::from("/tmp/meow-runtime-test/log/meow-core.log")
);
}
#[test]
fn tmp_root_is_workdir_scoped() {
let paths = RuntimePaths::for_workdir("/tmp/meow-runtime-test");
assert_eq!(
paths.tmp_root(),
PathBuf::from("/tmp/meow-runtime-test/tmp")
);
}
#[cfg(not(windows))]
#[test]
fn default_runtime_root_uses_dot_meow_under_home() {
let root = default_runtime_root().unwrap();
assert_eq!(
root.file_name().and_then(|value| value.to_str()),
Some(".meow")
);
}
}