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_daemon;
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 #[cfg(unix)]
149 let mut inherited = broker_owned_listener(&endpoint);
150 #[cfg(unix)]
151 if let Some(listener) = inherited.as_ref() {
152 if listener.prepare(&mut command).is_err() {
157 inherited = None;
158 }
159 }
160
161 let mut child = spawn_daemon(&mut command).map_err(BackendLaunchError::Spawn)?;
162
163 let daemon = daemon_identity_for_spawned_process(
164 child.id(),
165 binary_path,
166 endpoint.clone(),
167 self.idle_timeout_secs,
168 )?;
169
170 match BackendHandle::probe_with_service(
171 request.key.service_name.clone(),
172 request.key.service_version.clone(),
173 &endpoint,
174 &daemon,
175 ) {
176 Ok(handle) => {
177 #[cfg(unix)]
184 if let Some(listener) = inherited.as_mut() {
185 listener.disown_endpoint();
186 }
187 Ok(handle)
188 }
189 Err(err) => {
190 let _ = child.kill();
191 Err(BackendLaunchError::BackendHandle(err))
195 }
196 }
197 }
198}
199
200#[cfg(unix)]
205fn broker_owned_listener(
206 endpoint: &Endpoint,
207) -> Option<crate::broker::broker_owned_bind::InheritableListener> {
208 use crate::broker::broker_owned_bind::{launcher_opt_in, support, InheritableListener};
209
210 if !launcher_opt_in() || !support().is_supported() {
211 return None;
212 }
213 InheritableListener::bind(&endpoint.path).ok()
214}
215
216fn configure_backend_command(
217 command: &mut Command,
218 request: &BackendLaunchRequest<'_>,
219 endpoint: &Endpoint,
220) {
221 command
222 .env(BACKEND_ENV_SERVICE_NAME, &request.key.service_name)
223 .env(BACKEND_ENV_SERVICE_VERSION, &request.key.service_version)
224 .env(BACKEND_ENV_ENDPOINT_PATH, &endpoint.path)
225 .env(BACKEND_ENV_ENDPOINT_NAMESPACE, &endpoint.namespace_id)
226 .env(BACKEND_ENV_INSTANCE, request.key.instance.id());
227
228 if !request.trace_context.traceparent.is_empty() {
229 command.env(BACKEND_ENV_TRACEPARENT, &request.trace_context.traceparent);
230 }
231 if !request.trace_context.tracestate.is_empty() {
232 command.env(BACKEND_ENV_TRACESTATE, &request.trace_context.tracestate);
233 }
234 if let Some(session_token) = request.session_token {
235 command.env(BACKEND_ENV_SESSION_TOKEN, hex_encode(session_token));
236 }
237}
238
239fn hex_encode(bytes: &[u8]) -> String {
240 use std::fmt::Write;
241 let mut out = String::with_capacity(bytes.len() * 2);
242 for b in bytes {
243 let _ = write!(out, "{b:02x}");
244 }
245 out
246}
247
248#[derive(Debug, thiserror::Error)]
250pub enum BackendLaunchError {
251 #[error("backend binary_path is empty")]
253 EmptyBinaryPath,
254 #[error("backend per_version_binary_dir is empty")]
256 EmptyPerVersionBinaryDir,
257 #[error("backend binary_path {path:?} could not be canonicalized: {source}")]
259 CanonicalizeBinary {
260 path: PathBuf,
262 source: std::io::Error,
264 },
265 #[error("backend per_version_binary_dir {path:?} could not be canonicalized: {source}")]
267 CanonicalizeBinaryRoot {
268 path: PathBuf,
270 source: std::io::Error,
272 },
273 #[error("backend binary {binary:?} is outside per-version root {root:?}")]
275 BinaryOutsideAllowRoot {
276 binary: PathBuf,
278 root: PathBuf,
280 },
281 #[error("backend endpoint allocator state was poisoned")]
283 AllocatorPoisoned,
284 #[error(transparent)]
286 Endpoint(#[from] BackendEndpointAllocatorError),
287 #[error("backend daemon spawn failed: {0}")]
289 Spawn(std::io::Error),
290 #[error(transparent)]
292 Identity(#[from] IdentityError),
293 #[error(transparent)]
295 BackendHandle(#[from] BackendHandleError),
296 #[error("{0}")]
298 Launcher(String),
299}
300
301fn canonical_backend_binary(
302 service_definition: &ServiceDefinition,
303) -> Result<PathBuf, BackendLaunchError> {
304 if service_definition.binary_path.is_empty() {
305 return Err(BackendLaunchError::EmptyBinaryPath);
306 }
307 if service_definition.per_version_binary_dir.is_empty() {
308 return Err(BackendLaunchError::EmptyPerVersionBinaryDir);
309 }
310
311 let binary = PathBuf::from(&service_definition.binary_path);
312 let binary = std::fs::canonicalize(&binary).map_err(|source| {
313 BackendLaunchError::CanonicalizeBinary {
314 path: binary,
315 source,
316 }
317 })?;
318
319 let root = PathBuf::from(&service_definition.per_version_binary_dir);
320 let root = std::fs::canonicalize(&root)
321 .map_err(|source| BackendLaunchError::CanonicalizeBinaryRoot { path: root, source })?;
322
323 if !binary.starts_with(&root) {
324 return Err(BackendLaunchError::BinaryOutsideAllowRoot { binary, root });
325 }
326
327 Ok(binary)
328}
329
330fn daemon_identity_for_spawned_process(
331 pid: u32,
332 exe_path: PathBuf,
333 ipc_endpoint: Endpoint,
334 idle_timeout_secs: Option<u32>,
335) -> Result<DaemonProcess, IdentityError> {
336 let exe_hash = executable_hash_file(&exe_path)?;
337 let legacy_exe_sha256 = sha256_file(&exe_path)?;
338 Ok(DaemonProcess {
339 pid,
340 exe_path: exe_path.clone(),
341 exe_hash,
342 legacy_exe_sha256,
343 boot_id: host_identity::current_for_path(&exe_path).boot_id,
344 ipc_endpoint,
345 started_at_unix_ms: unix_now_ms(),
346 idle_timeout_secs,
347 })
348}
349
350fn unix_now_ms() -> u64 {
351 SystemTime::now()
352 .duration_since(UNIX_EPOCH)
353 .map(|duration| duration.as_millis() as u64)
354 .unwrap_or(0)
355}
356
357#[cfg(test)]
358mod tests {
359 use std::ffi::OsStr;
360
361 use crate::broker::protocol::ServiceDefinition;
362 use crate::broker::server::{BackendKey, BrokerInstanceKey, TraceContext};
363
364 use super::*;
365
366 fn env_value(command: &Command, name: &str) -> Option<String> {
367 command.get_envs().find_map(|(key, value)| {
368 if key == OsStr::new(name) {
369 value.map(|value| value.to_string_lossy().into_owned())
370 } else {
371 None
372 }
373 })
374 }
375
376 #[test]
377 fn backend_command_environment_forwards_trace_context() {
378 let key = BackendKey::new(BrokerInstanceKey::Shared, "zccache", "1.11.20", "");
379 let service_definition = ServiceDefinition {
380 service_name: "zccache".into(),
381 binary_path: "backend".into(),
382 isolation: 1,
383 explicit_instance: String::new(),
384 per_version_binary_dir: ".".into(),
385 min_version: "1.10.0".into(),
386 version_allow_list: vec!["1.11.20".into()],
387 labels: Default::default(),
388 };
389 let trace_context = TraceContext {
390 request_id: 42,
391 traceparent: "00-11111111111111111111111111111111-2222222222222222-01".into(),
392 tracestate: "vendor=value".into(),
393 };
394 let request = BackendLaunchRequest {
395 key: &key,
396 service_definition: &service_definition,
397 trace_context: &trace_context,
398 session_token: None,
399 };
400 let endpoint = Endpoint {
401 namespace_id: "shared".into(),
402 path: "backend.sock".into(),
403 };
404 let mut command = Command::new("backend");
405
406 configure_backend_command(&mut command, &request, &endpoint);
407
408 assert_eq!(
409 env_value(&command, BACKEND_ENV_SERVICE_NAME).as_deref(),
410 Some("zccache")
411 );
412 assert_eq!(
413 env_value(&command, BACKEND_ENV_SERVICE_VERSION).as_deref(),
414 Some("1.11.20")
415 );
416 assert_eq!(
417 env_value(&command, BACKEND_ENV_ENDPOINT_PATH).as_deref(),
418 Some("backend.sock")
419 );
420 assert_eq!(
421 env_value(&command, BACKEND_ENV_ENDPOINT_NAMESPACE).as_deref(),
422 Some("shared")
423 );
424 assert_eq!(
425 env_value(&command, BACKEND_ENV_INSTANCE).as_deref(),
426 Some("shared")
427 );
428 assert_eq!(
429 env_value(&command, BACKEND_ENV_TRACEPARENT).as_deref(),
430 Some("00-11111111111111111111111111111111-2222222222222222-01")
431 );
432 assert_eq!(
433 env_value(&command, BACKEND_ENV_TRACESTATE).as_deref(),
434 Some("vendor=value")
435 );
436 }
437}