running_process/broker/server/
service_def_loader.rs1use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10
11use prost::Message;
12
13use crate::broker::lifecycle::names::{validate_service_name, validate_version, PipePathError};
14use crate::broker::protocol::{BrokerIsolation, ServiceDefinition};
15use crate::broker::secure_dir;
16
17pub const SERVICE_DEF_EXTENSION: &str = "servicedef";
19
20pub const SERVICE_DEF_DIR_ENV: &str = "RUNNING_PROCESS_SERVICE_DEF_DIR";
22
23#[derive(Clone, Debug)]
25pub struct ServiceDefinitionLoader {
26 root: PathBuf,
27}
28
29impl ServiceDefinitionLoader {
30 pub fn new(root: impl Into<PathBuf>) -> Self {
32 Self { root: root.into() }
33 }
34
35 pub fn default_root() -> Self {
37 Self::new(service_definition_dir())
38 }
39
40 pub fn root(&self) -> &Path {
42 &self.root
43 }
44
45 pub fn load(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
47 ensure_loadable_service_definition_dir(&self.root)?;
48 let path = service_definition_path(&self.root, service_name)?;
49 let bytes = fs::read(&path)?;
50 let definition = ServiceDefinition::decode(bytes.as_slice())?;
51 validate_service_definition_for_service(&definition, service_name)?;
52 Ok(definition)
53 }
54
55 pub fn reload(&self, service_name: &str) -> Result<ServiceDefinition, ServiceDefinitionError> {
57 self.load(service_name)
58 }
59
60 pub fn lookup_or_reload(
62 &self,
63 service_name: &str,
64 ) -> Result<ServiceDefinition, ServiceDefinitionError> {
65 self.load(service_name)
66 }
67}
68
69pub fn service_definition_dir() -> PathBuf {
71 if let Some(path) = std::env::var_os(SERVICE_DEF_DIR_ENV) {
72 return PathBuf::from(path);
73 }
74
75 #[cfg(windows)]
76 {
77 dirs::config_dir()
78 .unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"))
79 .join("running-process")
80 .join("services")
81 }
82 #[cfg(target_os = "macos")]
83 {
84 dirs::home_dir()
85 .unwrap_or_else(std::env::temp_dir)
86 .join("Library")
87 .join("Application Support")
88 .join("running-process")
89 .join("services")
90 }
91 #[cfg(all(unix, not(target_os = "macos")))]
92 {
93 if let Some(config_home) = std::env::var_os("XDG_CONFIG_HOME") {
94 PathBuf::from(config_home)
95 .join("running-process")
96 .join("services")
97 } else {
98 dirs::home_dir()
99 .unwrap_or_else(std::env::temp_dir)
100 .join(".config")
101 .join("running-process")
102 .join("services")
103 }
104 }
105}
106
107pub fn ensure_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
109 secure_dir::ensure_private_dir(path)?;
110 ensure_loadable_service_definition_dir(path)
111}
112
113pub fn write_service_definition(
118 root: &Path,
119 definition: &ServiceDefinition,
120) -> Result<PathBuf, ServiceDefinitionError> {
121 ensure_service_definition_dir(root)?;
122 validate_service_definition_for_service(definition, &definition.service_name)?;
123 let path = service_definition_path(root, &definition.service_name)?;
124 fs::write(&path, definition.encode_to_vec())?;
125 Ok(path)
126}
127
128pub fn service_definition_path(
130 root: &Path,
131 service_name: &str,
132) -> Result<PathBuf, ServiceDefinitionError> {
133 validate_service_name(service_name)?;
134 Ok(root.join(format!("{service_name}.{SERVICE_DEF_EXTENSION}")))
135}
136
137pub fn validate_service_definition_for_service(
139 definition: &ServiceDefinition,
140 expected_service: &str,
141) -> Result<(), ServiceDefinitionError> {
142 validate_service_name(expected_service)?;
143 validate_service_name(&definition.service_name)?;
144 if definition.service_name != expected_service {
145 return Err(ServiceDefinitionError::ServiceNameMismatch {
146 requested: expected_service.into(),
147 actual: definition.service_name.clone(),
148 });
149 }
150 validate_absolute_path("binary_path", &definition.binary_path)?;
151 if !definition.per_version_binary_dir.is_empty() {
152 validate_absolute_path("per_version_binary_dir", &definition.per_version_binary_dir)?;
153 }
154 if !definition.min_version.is_empty() {
155 validate_version(&definition.min_version)?;
156 }
157 for version in &definition.version_allow_list {
158 validate_version(version)?;
159 }
160
161 match BrokerIsolation::try_from(definition.isolation) {
162 Ok(BrokerIsolation::PrivateBroker) | Ok(BrokerIsolation::SharedBroker) => {
163 if !definition.explicit_instance.is_empty() {
164 return Err(ServiceDefinitionError::InvalidIsolation {
165 reason: "explicit_instance must be empty unless isolation is EXPLICIT_INSTANCE",
166 });
167 }
168 }
169 Ok(BrokerIsolation::ExplicitInstance) => {
170 if definition.explicit_instance.is_empty() {
171 return Err(ServiceDefinitionError::InvalidIsolation {
172 reason: "EXPLICIT_INSTANCE requires explicit_instance",
173 });
174 }
175 validate_service_name(&definition.explicit_instance)?;
176 }
177 Err(_) => {
178 return Err(ServiceDefinitionError::InvalidIsolation {
179 reason: "unknown BrokerIsolation value",
180 });
181 }
182 }
183
184 Ok(())
185}
186
187#[derive(Debug, thiserror::Error)]
189pub enum ServiceDefinitionError {
190 #[error("service-definition I/O failed: {0}")]
192 Io(#[from] io::Error),
193 #[error("service-definition protobuf decode failed: {0}")]
195 Decode(#[from] prost::DecodeError),
196 #[error(transparent)]
198 InvalidName(#[from] PipePathError),
199 #[error("service-definition directory has insecure permissions: {0}")]
201 InsecureDirectory(PathBuf),
202 #[error("service-definition requested {requested:?} but file declares {actual:?}")]
204 ServiceNameMismatch {
205 requested: String,
207 actual: String,
209 },
210 #[error("service-definition {field} is invalid: {path:?} ({reason})")]
212 InvalidPath {
213 field: &'static str,
215 path: String,
217 reason: &'static str,
219 },
220 #[error("service-definition isolation is invalid: {reason}")]
222 InvalidIsolation {
223 reason: &'static str,
225 },
226}
227
228fn ensure_loadable_service_definition_dir(path: &Path) -> Result<(), ServiceDefinitionError> {
229 if !secure_dir::private_dir_permissions_are_private(path)? {
230 return Err(ServiceDefinitionError::InsecureDirectory(
231 path.to_path_buf(),
232 ));
233 }
234 Ok(())
235}
236
237fn validate_absolute_path(field: &'static str, value: &str) -> Result<(), ServiceDefinitionError> {
238 if value.is_empty() {
239 return Err(ServiceDefinitionError::InvalidPath {
240 field,
241 path: value.into(),
242 reason: "must not be empty",
243 });
244 }
245 if !Path::new(value).is_absolute() {
246 return Err(ServiceDefinitionError::InvalidPath {
247 field,
248 path: value.into(),
249 reason: "must be absolute",
250 });
251 }
252 Ok(())
253}