#[cfg(test)]
use super::info::TransportInfo;
use {
super::info::InstanceInfo,
reovim_arch::process_exists,
std::{io, path::PathBuf},
};
pub struct InstanceRegistry {
registry_dir: PathBuf,
}
impl InstanceRegistry {
#[must_use]
pub fn new() -> Self {
Self {
registry_dir: Self::default_registry_dir(),
}
}
#[must_use]
pub const fn with_dir(registry_dir: PathBuf) -> Self {
Self { registry_dir }
}
#[must_use]
pub fn default_registry_dir() -> PathBuf {
#[cfg(unix)]
{
reovim_arch::dirs::runtime_dir().map_or_else(
#[cfg_attr(coverage_nightly, coverage(off))]
|| {
let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
PathBuf::from(format!("/tmp/reovim-{user}"))
},
|d| d.join("reovim"),
)
}
#[cfg(windows)]
{
reovim_arch::dirs::data_local_dir().map_or_else(
|| PathBuf::from(r"C:\ProgramData\reovim\run"),
|d| d.join("reovim").join("run"),
)
}
}
#[must_use]
pub const fn registry_dir(&self) -> &PathBuf {
&self.registry_dir
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn register(&self, info: &InstanceInfo) -> io::Result<()> {
Self::validate_name(&info.name)?;
std::fs::create_dir_all(&self.registry_dir)?;
let path = self.instance_path(&info.name);
if path.exists() {
if let Ok(Some(existing)) = self.get_internal(&info.name, false)
&& process_exists(existing.pid)
{
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("Instance '{}' already exists (PID {})", info.name, existing.pid),
));
}
let _ = std::fs::remove_file(&path);
}
let json = serde_json::to_string_pretty(info)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
std::fs::write(path, json)
}
pub fn unregister(&self, name: &str) -> io::Result<()> {
let path = self.instance_path(name);
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
pub fn list(&self) -> io::Result<Vec<InstanceInfo>> {
let mut instances = Vec::new();
if !self.registry_dir.exists() {
return Ok(instances);
}
for entry in std::fs::read_dir(&self.registry_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let Ok(json) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(info) = serde_json::from_str::<InstanceInfo>(&json) else {
let _ = std::fs::remove_file(&path);
continue;
};
if process_exists(info.pid) {
instances.push(info);
} else {
let _ = std::fs::remove_file(&path);
}
}
Ok(instances)
}
pub fn get(&self, name: &str) -> io::Result<Option<InstanceInfo>> {
self.get_internal(name, true)
}
fn get_internal(&self, name: &str, cleanup_stale: bool) -> io::Result<Option<InstanceInfo>> {
let path = self.instance_path(name);
if !path.exists() {
return Ok(None);
}
let json = std::fs::read_to_string(&path)?;
let info: InstanceInfo = serde_json::from_str(&json)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if !process_exists(info.pid) {
if cleanup_stale {
let _ = std::fs::remove_file(&path);
}
return Ok(None);
}
Ok(Some(info))
}
pub fn validate_name(name: &str) -> io::Result<()> {
if name.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Instance name cannot be empty",
));
}
if name.len() > 63 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Instance name cannot exceed 63 characters",
));
}
if !name.as_bytes()[0].is_ascii_alphanumeric() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Instance name must start with alphanumeric character",
));
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Instance name contains invalid character: '{c}'"),
));
}
}
Ok(())
}
fn instance_path(&self, name: &str) -> PathBuf {
self.registry_dir.join(format!("{name}.json"))
}
}
impl Default for InstanceRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "registry_tests.rs"]
mod tests;