use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use prost::Message;
use crate::broker::lifecycle::names::{validate_service_name, validate_version, PipePathError};
use crate::broker::protocol::{BrokerIsolation, ServiceDefinition};
use crate::broker::secure_dir;
pub const SERVICE_DEF_EXTENSION: &str = "servicedef";
pub const SERVICE_DEF_DIR_ENV: &str = "RUNNING_PROCESS_SERVICE_DEF_DIR";
#[derive(Clone, Debug)]
pub struct ServiceDefinitionLoader {
root: PathBuf,
}
impl ServiceDefinitionLoader {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn default_root() -> Self {
Self::new(service_definition_dir())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn load(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
ensure_loadable_service_definition_dir(&self.root)?;
let path = service_definition_path(&self.root, service_name)?;
let bytes = fs::read(&path)?;
let definition = ServiceDefinition::decode(bytes.as_slice())?;
validate_service_definition_for_service(&definition, service_name)?;
Ok(definition)
}
pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
self.load(service_name)
}
pub fn lookup_or_reload(
&self,
service_name: &str,
) -> Result<ServiceDefinition, ServiceDefinitionError> {
self.load(service_name)
}
}
pub fn service_definition_dir() -> PathBuf {
if let Some(path) = std::env::var_os(SERVICE_DEF_DIR_ENV) {
return PathBuf::from(path);
}
#[cfg(windows)]
{
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"))
.join("running-process")
.join("services")
}
#[cfg(target_os = "macos")]
{
dirs::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join("Library")
.join("Application Support")
.join("running-process")
.join("services")
}
#[cfg(all(unix, not(target_os = "macos")))]
{
if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") {
PathBuf::from(config_home)
.join("running-process")
.join("services")
} else {
dirs::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join(".config")
.join("running-process")
.join("services")
}
}
}
pub fn ensure_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
secure_dir::ensure_private_dir(path)?;
ensure_loadable_service_definition_dir(path)
}
pub fn write_service_definition(
root: &Path,
definition: &ServiceDefinition,
) -> Result<PathBuf, ServiceDefinitionError> {
ensure_service_definition_dir(root)?;
validate_service_definition_for_service(definition, &definition.service_name)?;
let path = service_definition_path(root, &definition.service_name)?;
fs::write(&path, definition.encode_to_vec())?;
Ok(path)
}
pub fn service_definition_path(
root: &Path,
service_name: &str,
) -> Result<PathBuf, ServiceDefinitionError> {
validate_service_name(service_name)?;
Ok(root.join(format!("{service_name}.{SERVICE_DEF_EXTENSION}")))
}
pub fn validate_service_definition_for_service(
definition: &ServiceDefinition,
expected_service: &str,
) -> Result<(), ServiceDefinitionError> {
validate_service_name(expected_service)?;
validate_service_name(&definition.service_name)?;
if definition.service_name != expected_service {
return Err(ServiceDefinitionError::ServiceNameMismatch {
requested: expected_service.into(),
actual: definition.service_name.clone(),
});
}
validate_absolute_path("binary_path", &definition.binary_path)?;
if !definition.per_version_binary_dir.is_empty() {
validate_absolute_path("per_version_binary_dir", &definition.per_version_binary_dir)?;
}
if !definition.min_version.is_empty() {
validate_version(&definition.min_version)?;
}
for version in &definition.version_allow_list {
validate_version(version)?;
}
match BrokerIsolation::try_from(definition.isolation) {
Ok(BrokerIsolation::PrivateBroker) | Ok(BrokerIsolation::SharedBroker) => {
if !definition.explicit_instance.is_empty() {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "explicit_instance must be empty unless isolation is EXPLICIT_INSTANCE",
});
}
}
Ok(BrokerIsolation::ExplicitInstance) => {
if definition.explicit_instance.is_empty() {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "EXPLICIT_INSTANCE requires explicit_instance",
});
}
validate_service_name(&definition.explicit_instance)?;
}
Err(_) => {
return Err(ServiceDefinitionError::InvalidIsolation {
reason: "unknown BrokerIsolation value",
});
}
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ServiceDefinitionError {
#[error("service-definition I/O failed: {0}")]
Io(#[from] io::Error),
#[error("service-definition protobuf decode failed: {0}")]
Decode(#[from] prost::DecodeError),
#[error(transparent)]
InvalidName(#[from] PipePathError),
#[error("service-definition directory has insecure permissions: {0}")]
InsecureDirectory(PathBuf),
#[error("service-definition requested {requested:?} but file declares {actual:?}")]
ServiceNameMismatch {
requested: String,
actual: String,
},
#[error("service-definition {field} is invalid: {path:?} ({reason})")]
InvalidPath {
field: &'static str,
path: String,
reason: &'static str,
},
#[error("service-definition isolation is invalid: {reason}")]
InvalidIsolation {
reason: &'static str,
},
}
fn ensure_loadable_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
if !secure_dir::private_dir_permissions_are_private(path)? {
return Err(ServiceDefinitionError::InsecureDirectory(
path.to_path_buf(),
));
}
Ok(())
}
fn validate_absolute_path(field: &'static str, value: &str) -> Result<(), ServiceDefinitionError> {
if value.is_empty() {
return Err(ServiceDefinitionError::InvalidPath {
field,
path: value.into(),
reason: "must not be empty",
});
}
if !Path::new(value).is_absolute() {
return Err(ServiceDefinitionError::InvalidPath {
field,
path: value.into(),
reason: "must be absolute",
});
}
Ok(())
}