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