running_process/broker/server/
backend_launcher.rs1use std::collections::HashMap;
8use std::path::PathBuf;
9use std::process::Command;
10use std::sync::Mutex;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::broker::backend_handle::{BackendHandle, BackendHandleError, DaemonProcess};
14use crate::broker::backend_lifecycle::identity::{executable_hash_file, IdentityError};
15use crate::broker::host_identity;
16use crate::broker::lifecycle::sid::{user_sid_hash, SidError};
17use crate::broker::protocol::{Endpoint, ServiceDefinition};
18use crate::spawn_daemon;
19
20use super::backend_endpoint_allocator::{BackendEndpointAllocator, BackendEndpointAllocatorError};
21use super::backend_registry::BackendKey;
22use super::trace_context::TraceContext;
23
24pub const BACKEND_ENV_SERVICE_NAME: &str = "RUNNING_PROCESS_BROKER_V1_SERVICE_NAME";
27pub const BACKEND_ENV_SERVICE_VERSION: &str = "RUNNING_PROCESS_BROKER_V1_SERVICE_VERSION";
29pub const BACKEND_ENV_ENDPOINT_PATH: &str = "RUNNING_PROCESS_BROKER_V1_BACKEND_PIPE";
31pub const BACKEND_ENV_ENDPOINT_NAMESPACE: &str = "RUNNING_PROCESS_BROKER_V1_BACKEND_NAMESPACE";
33pub const BACKEND_ENV_INSTANCE: &str = "RUNNING_PROCESS_BROKER_V1_INSTANCE";
35pub const BACKEND_ENV_TRACEPARENT: &str = "RUNNING_PROCESS_BROKER_V1_TRACEPARENT";
37pub const BACKEND_ENV_TRACESTATE: &str = "RUNNING_PROCESS_BROKER_V1_TRACESTATE";
39pub const BACKEND_ENV_SESSION_TOKEN: &str = "RUNNING_PROCESS_BROKER_V1_SESSION_TOKEN";
44
45pub struct BackendLaunchRequest<'a> {
48 pub key: &'a BackendKey,
50 pub service_definition: &'a ServiceDefinition,
52 pub trace_context: &'a TraceContext,
54 pub session_token: Option<&'a [u8]>,
59}
60
61pub trait BackendLauncher: Send + Sync {
63 fn launch(
65 &self,
66 request: &BackendLaunchRequest<'_>,
67 ) -> Result<BackendHandle, BackendLaunchError>;
68}
69
70#[derive(Debug)]
77pub struct CommandBackendLauncher {
78 user_sid_hash: String,
79 allocators: Mutex<HashMap<String, BackendEndpointAllocator>>,
80 idle_timeout_secs: Option<u32>,
81}
82
83impl CommandBackendLauncher {
84 pub fn for_current_user() -> Result<Self, SidError> {
86 Ok(Self::new(user_sid_hash()?))
87 }
88
89 pub fn new(user_sid_hash: impl Into<String>) -> Self {
91 Self {
92 user_sid_hash: user_sid_hash.into(),
93 allocators: Mutex::new(HashMap::new()),
94 idle_timeout_secs: Some(30),
95 }
96 }
97
98 pub fn with_idle_timeout_secs(mut self, idle_timeout_secs: Option<u32>) -> Self {
100 self.idle_timeout_secs = idle_timeout_secs;
101 self
102 }
103
104 fn allocate_endpoint(
105 &self,
106 request: &BackendLaunchRequest<'_>,
107 ) -> Result<Endpoint, BackendLaunchError> {
108 let namespace_id = request.key.instance.id();
109 let mut allocators = self
110 .allocators
111 .lock()
112 .map_err(|_| BackendLaunchError::AllocatorPoisoned)?;
113 let allocator = allocators
114 .entry(namespace_id.clone())
115 .or_insert_with(|| BackendEndpointAllocator::new(&self.user_sid_hash, namespace_id));
116 Ok(allocator.allocate()?)
117 }
118}
119
120impl BackendLauncher for CommandBackendLauncher {
121 fn launch(
122 &self,
123 request: &BackendLaunchRequest<'_>,
124 ) -> Result<BackendHandle, BackendLaunchError> {
125 let endpoint = self.allocate_endpoint(request)?;
126 let binary_path = canonical_backend_binary(request.service_definition)?;
127 let mut command = Command::new(&binary_path);
128 configure_backend_command(&mut command, request, &endpoint);
129
130 #[cfg(unix)]
147 let mut inherited = broker_owned_listener(&endpoint);
148 #[cfg(unix)]
149 if let Some(listener) = inherited.as_ref() {
150 if listener.prepare(&mut command).is_err() {
155 inherited = None;
156 }
157 }
158
159 let mut child = spawn_daemon(&mut command).map_err(BackendLaunchError::Spawn)?;
160
161 let daemon = daemon_identity_for_spawned_process(
162 child.id(),
163 binary_path,
164 endpoint.clone(),
165 self.idle_timeout_secs,
166 )?;
167
168 match BackendHandle::probe_with_service(
169 request.key.service_name.clone(),
170 request.key.service_version.clone(),
171 &endpoint,
172 &daemon,
173 ) {
174 Ok(handle) => {
175 #[cfg(unix)]
182 if let Some(listener) = inherited.as_mut() {
183 listener.disown_endpoint();
184 }
185 Ok(handle)
186 }
187 Err(err) => {
188 let _ = child.kill();
189 Err(BackendLaunchError::BackendHandle(err))
193 }
194 }
195 }
196}
197
198#[cfg(unix)]
203fn broker_owned_listener(
204 endpoint: &Endpoint,
205) -> Option<crate::broker::broker_owned_bind::InheritableListener> {
206 use crate::broker::broker_owned_bind::{launcher_opt_in, support, InheritableListener};
207
208 if !launcher_opt_in() || !support().is_supported() {
209 return None;
210 }
211 InheritableListener::bind(&endpoint.path).ok()
212}
213
214fn configure_backend_command(
215 command: &mut Command,
216 request: &BackendLaunchRequest<'_>,
217 endpoint: &Endpoint,
218) {
219 command
220 .env(BACKEND_ENV_SERVICE_NAME, &request.key.service_name)
221 .env(BACKEND_ENV_SERVICE_VERSION, &request.key.service_version)
222 .env(BACKEND_ENV_ENDPOINT_PATH, &endpoint.path)
223 .env(BACKEND_ENV_ENDPOINT_NAMESPACE, &endpoint.namespace_id)
224 .env(BACKEND_ENV_INSTANCE, request.key.instance.id());
225
226 if !request.trace_context.traceparent.is_empty() {
227 command.env(BACKEND_ENV_TRACEPARENT, &request.trace_context.traceparent);
228 }
229 if !request.trace_context.tracestate.is_empty() {
230 command.env(BACKEND_ENV_TRACESTATE, &request.trace_context.tracestate);
231 }
232 if let Some(session_token) = request.session_token {
233 command.env(BACKEND_ENV_SESSION_TOKEN, hex_encode(session_token));
234 }
235}
236
237fn hex_encode(bytes: &[u8]) -> String {
238 use std::fmt::Write;
239 let mut out = String::with_capacity(bytes.len() * 2);
240 for b in bytes {
241 let _ = write!(out, "{b:02x}");
242 }
243 out
244}
245
246#[derive(Debug, thiserror::Error)]
248pub enum BackendLaunchError {
249 #[error("backend binary_path is empty")]
251 EmptyBinaryPath,
252 #[error("backend per_version_binary_dir is empty")]
254 EmptyPerVersionBinaryDir,
255 #[error("backend binary_path {path:?} could not be canonicalized: {source}")]
257 CanonicalizeBinary {
258 path: PathBuf,
260 source: std::io::Error,
262 },
263 #[error("backend per_version_binary_dir {path:?} could not be canonicalized: {source}")]
265 CanonicalizeBinaryRoot {
266 path: PathBuf,
268 source: std::io::Error,
270 },
271 #[error("backend binary {binary:?} is outside per-version root {root:?}")]
273 BinaryOutsideAllowRoot {
274 binary: PathBuf,
276 root: PathBuf,
278 },
279 #[error("backend endpoint allocator state was poisoned")]
281 AllocatorPoisoned,
282 #[error(transparent)]
284 Endpoint(#[from] BackendEndpointAllocatorError),
285 #[error("backend daemon spawn failed: {0}")]
287 Spawn(std::io::Error),
288 #[error(transparent)]
290 Identity(#[from] IdentityError),
291 #[error(transparent)]
293 BackendHandle(#[from] BackendHandleError),
294 #[error("{0}")]
296 Launcher(String),
297}
298
299fn canonical_backend_binary(
300 service_definition: &ServiceDefinition,
301) -> Result<PathBuf, BackendLaunchError> {
302 if service_definition.binary_path.is_empty() {
303 return Err(BackendLaunchError::EmptyBinaryPath);
304 }
305 if service_definition.per_version_binary_dir.is_empty() {
306 return Err(BackendLaunchError::EmptyPerVersionBinaryDir);
307 }
308
309 let binary = PathBuf::from(&service_definition.binary_path);
310 let binary = std::fs::canonicalize(&binary).map_err(|source| {
311 BackendLaunchError::CanonicalizeBinary {
312 path: binary,
313 source,
314 }
315 })?;
316
317 let root = PathBuf::from(&service_definition.per_version_binary_dir);
318 let root = std::fs::canonicalize(&root)
319 .map_err(|source| BackendLaunchError::CanonicalizeBinaryRoot { path: root, source })?;
320
321 if !binary.starts_with(&root) {
322 return Err(BackendLaunchError::BinaryOutsideAllowRoot { binary, root });
323 }
324
325 Ok(binary)
326}
327
328fn daemon_identity_for_spawned_process(
329 pid: u32,
330 exe_path: PathBuf,
331 ipc_endpoint: Endpoint,
332 idle_timeout_secs: Option<u32>,
333) -> Result<DaemonProcess, IdentityError> {
334 let exe_hash = executable_hash_file(&exe_path)?;
335 Ok(DaemonProcess {
336 pid,
337 exe_path: exe_path.clone(),
338 exe_hash,
339 boot_id: host_identity::current_for_path(&exe_path).boot_id,
340 ipc_endpoint,
341 started_at_unix_ms: unix_now_ms(),
342 idle_timeout_secs,
343 })
344}
345
346fn unix_now_ms() -> u64 {
347 SystemTime::now()
348 .duration_since(UNIX_EPOCH)
349 .map(|duration| duration.as_millis() as u64)
350 .unwrap_or(0)
351}
352
353#[cfg(test)]
354mod tests {
355 use std::ffi::OsStr;
356
357 use crate::broker::protocol::ServiceDefinition;
358 use crate::broker::server::{BackendKey, BrokerInstanceKey, TraceContext};
359
360 use super::*;
361
362 fn env_value(command: &Command, name: &str) -> Option<String> {
363 command.get_envs().find_map(|(key, value)| {
364 if key == OsStr::new(name) {
365 value.map(|value| value.to_string_lossy().into_owned())
366 } else {
367 None
368 }
369 })
370 }
371
372 #[test]
373 fn backend_command_environment_forwards_trace_context() {
374 let key = BackendKey::new(BrokerInstanceKey::Shared, "zccache", "1.11.20", "");
375 let service_definition = ServiceDefinition {
376 service_name: "zccache".into(),
377 binary_path: "backend".into(),
378 isolation: 1,
379 explicit_instance: String::new(),
380 per_version_binary_dir: ".".into(),
381 min_version: "1.10.0".into(),
382 version_allow_list: vec!["1.11.20".into()],
383 labels: Default::default(),
384 };
385 let trace_context = TraceContext {
386 request_id: 42,
387 traceparent: "00-11111111111111111111111111111111-2222222222222222-01".into(),
388 tracestate: "vendor=value".into(),
389 };
390 let request = BackendLaunchRequest {
391 key: &key,
392 service_definition: &service_definition,
393 trace_context: &trace_context,
394 session_token: None,
395 };
396 let endpoint = Endpoint {
397 namespace_id: "shared".into(),
398 path: "backend.sock".into(),
399 };
400 let mut command = Command::new("backend");
401
402 configure_backend_command(&mut command, &request, &endpoint);
403
404 assert_eq!(
405 env_value(&command, BACKEND_ENV_SERVICE_NAME).as_deref(),
406 Some("zccache")
407 );
408 assert_eq!(
409 env_value(&command, BACKEND_ENV_SERVICE_VERSION).as_deref(),
410 Some("1.11.20")
411 );
412 assert_eq!(
413 env_value(&command, BACKEND_ENV_ENDPOINT_PATH).as_deref(),
414 Some("backend.sock")
415 );
416 assert_eq!(
417 env_value(&command, BACKEND_ENV_ENDPOINT_NAMESPACE).as_deref(),
418 Some("shared")
419 );
420 assert_eq!(
421 env_value(&command, BACKEND_ENV_INSTANCE).as_deref(),
422 Some("shared")
423 );
424 assert_eq!(
425 env_value(&command, BACKEND_ENV_TRACEPARENT).as_deref(),
426 Some("00-11111111111111111111111111111111-2222222222222222-01")
427 );
428 assert_eq!(
429 env_value(&command, BACKEND_ENV_TRACESTATE).as_deref(),
430 Some("vendor=value")
431 );
432 }
433}