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