Skip to main content

running_process/broker/server/
backend_launcher.rs

1//! Backend launch abstraction for Hello registry misses.
2//!
3//! The router owns admission control and registry insertion. Launchers own the
4//! platform-specific act of starting or discovering a backend and returning a
5//! verified [`BackendHandle`].
6
7use 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
26/// Environment variable containing the logical service name for a launched
27/// backend.
28pub const BACKEND_ENV_SERVICE_NAME: &str = "RUNNING_PROCESS_BROKER_V1_SERVICE_NAME";
29/// Environment variable containing the negotiated service version.
30pub const BACKEND_ENV_SERVICE_VERSION: &str = "RUNNING_PROCESS_BROKER_V1_SERVICE_VERSION";
31/// Environment variable containing the backend IPC endpoint path.
32pub const BACKEND_ENV_ENDPOINT_PATH: &str = "RUNNING_PROCESS_BROKER_V1_BACKEND_PIPE";
33/// Environment variable containing the backend endpoint namespace.
34pub const BACKEND_ENV_ENDPOINT_NAMESPACE: &str = "RUNNING_PROCESS_BROKER_V1_BACKEND_NAMESPACE";
35/// Environment variable containing the broker instance id.
36pub const BACKEND_ENV_INSTANCE: &str = "RUNNING_PROCESS_BROKER_V1_INSTANCE";
37/// Environment variable containing the incoming W3C traceparent value.
38pub const BACKEND_ENV_TRACEPARENT: &str = "RUNNING_PROCESS_BROKER_V1_TRACEPARENT";
39/// Environment variable containing the incoming W3C tracestate value.
40pub const BACKEND_ENV_TRACESTATE: &str = "RUNNING_PROCESS_BROKER_V1_TRACESTATE";
41/// Environment variable containing this daemon's composite session token
42/// (zackees/soldr#2361 Phase 2, #2363), hex-encoded. Set only when the
43/// router launching this backend was configured with a session-token
44/// authority -- absent otherwise, matching every other opt-in field here.
45pub const BACKEND_ENV_SESSION_TOKEN: &str = "RUNNING_PROCESS_BROKER_V1_SESSION_TOKEN";
46
47/// Inputs supplied to a backend launcher after Hello validation and budget
48/// admission.
49pub struct BackendLaunchRequest<'a> {
50    /// Backend key being launched.
51    pub key: &'a BackendKey,
52    /// Service definition that authorized the requested backend.
53    pub service_definition: &'a ServiceDefinition,
54    /// Trace context from the Hello frame that triggered this launch.
55    pub trace_context: &'a TraceContext,
56    /// This daemon's freshly-minted composite session token
57    /// (zackees/soldr#2361 Phase 2), `broker_half ‖ daemon_half`, if the
58    /// router was configured with a session-token authority. `None` when
59    /// no authority is configured -- the default, unchanged behavior.
60    pub session_token: Option<&'a [u8]>,
61}
62
63/// Launches or discovers one backend and returns a verified handle.
64pub trait BackendLauncher: Send + Sync {
65    /// Launch the requested backend.
66    fn launch(
67        &self,
68        request: &BackendLaunchRequest<'_>,
69    ) -> Result<BackendHandle, BackendLaunchError>;
70}
71
72/// Command-based backend launcher.
73///
74/// This launcher allocates the canonical v1 backend endpoint, starts
75/// `ServiceDefinition.binary_path` as a detached daemon, passes the selected
76/// endpoint through environment variables, and verifies the spawned process
77/// identity before returning a [`BackendHandle`].
78#[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    /// Build a launcher for the current user.
87    pub fn for_current_user() -> Result<Self, SidError> {
88        Ok(Self::new(user_sid_hash()?))
89    }
90
91    /// Build a launcher with an explicit 16-hex user SID hash.
92    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    /// Override the idle timeout recorded in the verified daemon identity.
101    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        // Broker-owned bind (#500 slice 32), opt-in and off by default.
133        //
134        // When enabled and supported, the broker binds the endpoint before
135        // spawning, so it is listening — and clients queue in the accept
136        // backlog — before the daemon's `main` runs. The daemon adopts it in
137        // `bootstrap` rather than binding a second listener.
138        //
139        // Unix-only, and cfg'd rather than stubbed: there is no Windows
140        // listener object to hand over, so there is nothing for this block to
141        // do there. See `broker_owned_bind`'s module docs.
142        //
143        // A bind failure is not fatal. The endpoint is freshly allocated, so
144        // failing to claim it is unexpected rather than a conflict worth
145        // aborting a launch over — falling back gives the spawn-then-probe
146        // behaviour this launcher has always had, and the probe below still
147        // gates success either way.
148        let mut inherited = broker_owned_listener(&endpoint);
149        let mut inheritance = None;
150        if let Some(listener) = inherited.as_ref() {
151            // Publishing the descriptor must happen before the spawn. Failing
152            // here would leave the child inheriting nothing while the broker
153            // holds a listener nobody serves, so drop ours and let the daemon
154            // bind for itself.
155            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                // Only now does a child genuinely own the endpoint. Disowning
182                // any earlier — right after `spawn`, as the first revision of
183                // this did — leaks the socket file on every failed launch:
184                // the probe fails, the child is killed, and the listener drops
185                // with its reclaim guard already released. Dead daemon,
186                // orphaned socket.
187                if let Some(listener) = inherited.as_mut() {
188                    listener.disown_endpoint();
189                }
190                Ok(handle)
191            }
192            Err(err) => {
193                let _ = child.kill();
194                // `inherited` drops here with its reclaim guard still armed,
195                // so the socket file goes with it. Nothing is serving that
196                // endpoint — the child we just killed was the only candidate.
197                Err(BackendLaunchError::BackendHandle(err))
198            }
199        }
200    }
201}
202
203/// Bind the endpoint in the broker, when opted in and supported.
204///
205/// `None` means the daemon binds for itself — the path this launcher has
206/// always taken, and the default.
207fn 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/// Errors raised while launching a backend.
251#[derive(Debug, thiserror::Error)]
252pub enum BackendLaunchError {
253    /// The service definition did not include a backend binary path.
254    #[error("backend binary_path is empty")]
255    EmptyBinaryPath,
256    /// The service definition did not include the per-version allow-list root.
257    #[error("backend per_version_binary_dir is empty")]
258    EmptyPerVersionBinaryDir,
259    /// The backend binary path could not be canonicalized.
260    #[error("backend binary_path {path:?} could not be canonicalized: {source}")]
261    CanonicalizeBinary {
262        /// Path that failed canonicalization.
263        path: PathBuf,
264        /// Filesystem error.
265        source: std::io::Error,
266    },
267    /// The backend allow-list root could not be canonicalized.
268    #[error("backend per_version_binary_dir {path:?} could not be canonicalized: {source}")]
269    CanonicalizeBinaryRoot {
270        /// Root path that failed canonicalization.
271        path: PathBuf,
272        /// Filesystem error.
273        source: std::io::Error,
274    },
275    /// The binary was outside the configured per-version allow-list root.
276    #[error("backend binary {binary:?} is outside per-version root {root:?}")]
277    BinaryOutsideAllowRoot {
278        /// Canonical backend binary path.
279        binary: PathBuf,
280        /// Canonical allow-list root.
281        root: PathBuf,
282    },
283    /// Endpoint allocator state was poisoned.
284    #[error("backend endpoint allocator state was poisoned")]
285    AllocatorPoisoned,
286    /// Canonical endpoint allocation failed.
287    #[error(transparent)]
288    Endpoint(#[from] BackendEndpointAllocatorError),
289    /// Detached process creation failed.
290    #[error("backend daemon spawn failed: {0}")]
291    Spawn(std::io::Error),
292    /// Spawned daemon identity construction failed.
293    #[error(transparent)]
294    Identity(#[from] IdentityError),
295    /// Spawned daemon verification failed.
296    #[error(transparent)]
297    BackendHandle(#[from] BackendHandleError),
298    /// Test or custom launcher failure.
299    #[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}