running-process 4.10.16

Subprocess and PTY runtime for the running-process project
Documentation
//! Service-definition file loading for the v1 broker.
//!
//! The loader intentionally re-reads from disk for each `lookup_or_reload`
//! call. That gives Phase 4's Hello path reload-on-Hello semantics without
//! coupling the validation rules to the later async accept loop.

use std::fs;
use std::path::{Path, PathBuf};

use prost::Message;

use crate::daemon_registration::protocol::{BrokerIsolation, ServiceDefinition};
use crate::daemon_registration::validation::{validate_service_name, validate_version};
pub use crate::daemon_registration_common::service_definition::{
    ensure_service_definition_dir, service_definition_dir, ServiceDefinitionError,
    SERVICE_DEF_DIR_ENV,
};

/// Service-definition file extension.
pub const SERVICE_DEF_EXTENSION: &str = "servicedef";

/// Loader rooted at one service-definition directory.
#[derive(Clone, Debug)]
pub struct ServiceDefinitionLoader {
    root: PathBuf,
}

impl ServiceDefinitionLoader {
    /// Create a loader for `root`.
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    /// Create a loader for the platform default service-definition directory.
    pub fn default_root() -> Self {
        Self::new(service_definition_dir())
    }

    /// Directory this loader reads from.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Load and validate one service definition from disk.
    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)
    }

    /// Reload one service definition from disk.
    pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
        self.load(service_name)
    }

    /// Lookup that always re-reads the service-definition file.
    pub fn lookup_or_reload(
        &self,
        service_name: &str,
    ) -> Result<ServiceDefinition, ServiceDefinitionError> {
        self.load(service_name)
    }
}

/// Validate and write one `.servicedef` file into `root`.
///
/// Consumer installers and development tools should use this helper instead of
/// duplicating protobuf serialization and service-definition path logic.
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)
}

/// Compute the file path for one service definition.
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}")))
}

/// Validate one decoded service definition against the requested service.
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(())
}

fn ensure_loadable_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
    if !crate::daemon_registration_common::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(())
}