Skip to main content

capsule_runtime/
child_launch.rs

1//! Launching a delegated child as an operating-system process of the parent's own runtime.
2//!
3//! The daemon referees and hands back an approval; this is what turns that approval into a
4//! running capsule. The parent's runtime composes a directory for the child beneath its own
5//! accessible workdir, builds the child's environment from the child's declaration rather than
6//! its own, and starts the `mur` binary as a subprocess. The child's runtime registers with the
7//! daemon for itself (see [`crate::registration`]), so the parent never states what the child
8//! holds — it only names which artifact is running.
9//!
10//! Three properties fall out of the child being a process rather than a thread:
11//!
12//! * A daemon crash takes no child with it. Nothing the child needs to keep running lives in the
13//!   daemon's address space.
14//! * Each child has its own process environment and working directory, so a native subprocess
15//!   started by one child inherits nothing a sibling shares.
16//! * A child that declares the `sealed` containment floor enters a mount namespace of its own,
17//!   because the containment machinery installs per process and the child *is* a process.
18//!
19//! **A child joins its parent's formation; it never starts one.** A parent session that belongs to
20//! a formation hands its id to every child as [`FORMATION_ID_ENV`], beside the spawner handle and
21//! independent of it, so a launch that names no lineage still carries it. A parent in no
22//! formation hands none. Inheriting the id is not a grant: the child gets no formation channel, no
23//! formation token, no callee and no formation lifeline, so its door has no membership and refuses
24//! every formation token.
25//!
26//! **A child dies with the process that spawned it.** Every child, in a formation or not, is
27//! handed a spawner lifeline of its own ([`crate::lifeline::ChildLifeline`], named by
28//! [`SPAWNER_LIFELINE_ENV`]): the read end of a pipe whose only write end this process holds.
29//! When this process ends, by any means, the kernel closes the write end and the child winds down
30//! as a first `SIGTERM` would wind it down. A child that delegates hands its own children a
31//! lifeline of its own the same way, so a grandchild ends once its parent has. A launch whose
32//! lifeline cannot be created is refused rather than started without one.
33//!
34//! **The approval travels on the child's standard input.** Not on the argument vector and not in
35//! the environment: both are readable from `/proc/<pid>` by any process running as the same user,
36//! which is exactly what a sibling capsule's shell tool is.
37
38use std::io::{BufRead, BufReader, Write};
39use std::path::{Path, PathBuf};
40use std::process::{Child, Command, ExitStatus, Stdio};
41use std::sync::{mpsc, Arc, Condvar, Mutex};
42use std::time::{Duration, Instant};
43
44use crate::delegation::{
45    self, CompletionAddress, DelegationOutcome, DelegationStatus, Reporter, Spawner, SpawnerHandle,
46    SPAWNER_ENV,
47};
48use crate::errors::RuntimeError;
49use crate::formation::{FormationId, FORMATION_ID_ENV};
50use crate::host_warnings::HOST_WARNINGS_REPORTED_ENV;
51use crate::lifeline::SPAWNER_LIFELINE_ENV;
52use crate::mac_token;
53use crate::spawn_credential::SpawnApproval;
54
55/// Environment variable naming the `mur` binary to launch children with.
56///
57/// Defaults to [`std::env::current_exe`], which is correct in production: the process doing the
58/// launching *is* `mur`. A test harness that runs inside its own test binary sets this to the
59/// built `mur` instead.
60pub const MUR_BINARY_ENV: &str = "MURMUR_MUR_BINARY";
61
62/// The hidden `mur run` flag that puts every door token the session minted on its `--json`
63/// readiness line, under `tokens`. Passed only by a launcher reading that line on a pipe nothing
64/// else holds — a parent runtime launching a delegated child, and `mur run --roster` launching a
65/// peer — because it is the one `mur run` output that carries a token.
66pub const READINESS_TOKENS_FLAG: &str = "--readiness-tokens";
67
68/// How long the parent waits for a child to print its `--json` launch line.
69///
70/// Generous, because it bounds more than a handshake: a script capsule prints the line when its
71/// run has *finished*, and an agent capsule compiles its driver component before it binds a port,
72/// which on a cold host is the slowest thing in a launch.
73const CHILD_READY_TIMEOUT: Duration = Duration::from_secs(180);
74
75/// Owner-only, so a sibling capsule running as the same user cannot list or read a child's
76/// directory even though both sit under one parent.
77#[cfg(unix)]
78const CHILD_DIR_MODE: u32 = 0o700;
79
80/// How many of the child's last stderr lines are kept to explain a launch that never reported.
81///
82/// The child's diagnostics are the operator's, so every line is echoed to this process's stderr as
83/// it arrives; this bound is only what a *failure* quotes back, so a child that logged for an hour
84/// before dying does not turn into an unbounded error string.
85const CHILD_STDERR_TAIL_LINES: usize = 20;
86
87/// How long a post-mortem read of the tail waits for the drain to reach the end of the pipe.
88///
89/// Bounds a thread being scheduled, not the child: by the time anything asks for a dead child's
90/// tail the exit has already closed the write end, so the drain is at most one wake-up away.
91/// A pipe some surviving grandchild still holds open would never reach EOF at all, and the bound
92/// is what stops that inheriting process from wedging the parent's launch or its watcher.
93const CHILD_STDERR_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
94
95/// How often the completion watcher asks whether the child is still running.
96///
97/// It polls rather than blocking on `wait`, because the process handle is shared with
98/// [`LaunchedChild::shutdown`] and [`Drop`]: a watcher blocked inside `wait` would hold the lock
99/// those need, and a second `wait` on one child is not a thing two owners can both do.
100const CHILD_WATCH_INTERVAL: Duration = Duration::from_millis(100);
101
102/// How long a child sent `SIGTERM` on purpose is given to wind down before it is `SIGKILL`ed.
103///
104/// The child's own `SIGTERM` teardown cancels its task and writes `session_end`, so within this
105/// grace its trace says how it ended. Kept under `mur stop`'s default `--timeout`, so a parent
106/// that is stopping can end its children and still exit inside its own grace.
107///
108/// Shorter than the child's own [`crate::runtime::TERMINATE_TEARDOWN_DEADLINE`]: a child whose
109/// teardown runs past this grace (a long async-hook drain) is `SIGKILL`ed before it writes
110/// `session_end`, and `mur trace show` reports its trace as ended without one.
111pub const CHILD_END_GRACE: Duration = Duration::from_secs(5);
112
113/// How often a child that has been sent `SIGTERM` is polled for its exit during
114/// [`CHILD_END_GRACE`].
115const CHILD_END_POLL: Duration = Duration::from_millis(20);
116
117/// What the parent's runtime needs in order to start one approved child.
118///
119/// Carries no manifest and no capability declaration: what the child holds is decided by the
120/// daemon from the child's own registry manifest, both at the `POST /spawn` that granted `grant`
121/// and again at the `POST /register` the child performs for itself.
122#[derive(Debug)]
123pub struct ChildLaunchRequest {
124    /// The parent's own accessible workdir. The child's directory is composed beneath it, which
125    /// is what keeps the child inside the single preopen the parent's WASI layer already has.
126    pub parent_accessible_workdir: PathBuf,
127    pub capsule_name: String,
128    pub capsule_version: String,
129    /// The approval `POST /spawn` returned, spent by the child's own registration.
130    pub grant: SpawnApproval,
131    /// The child's `capabilities.env.allow`. Names listed here are copied from the parent's
132    /// process environment into the child's; every other host variable the parent holds is
133    /// absent from the child, because the child's environment is built from a cleared one.
134    pub child_env_allow: Vec<String>,
135    /// Base URL of the daemon the child registers with.
136    pub roost_url: String,
137    /// The session this child belongs to, or `None` for a launch that is not a delegation.
138    ///
139    /// `Some` is what turns the launch into a delegation: the launcher mints a delegation id and
140    /// injects a [`SpawnerHandle`] as [`SPAWNER_ENV`], which is where the child reads the lineage
141    /// it records in its own `session_start`. The watcher that reports for a child which could
142    /// not report for itself starts only when that spawner also names a
143    /// [`CompletionAddress`] — a parent waiting on its own connection wants no completion.
144    /// `None` injects nothing and starts nothing.
145    pub spawner: Option<Spawner>,
146    /// How long the completion watcher observes this child before ending it, or `None` to observe
147    /// it for as long as it runs.
148    ///
149    /// Meaningful only alongside a [`CompletionAddress`], because the watcher that enforces it
150    /// starts only for a spawner that names one. On expiry the watcher stops the process and
151    /// posts a [`DelegationStatus::Terminated`] outcome naming the bound, which is the only thing
152    /// that tells a parent about a child that never ends.
153    pub completion_deadline: Option<Duration>,
154    /// The parent session's formation, which the child joins: injected as [`FORMATION_ID_ENV`]
155    /// when `Some`, whether or not [`Self::spawner`] is. Inherited from the parent and never
156    /// minted here; `None` — a parent in no formation — injects nothing.
157    pub formation_id: Option<FormationId>,
158}
159
160/// How a child's process ended, as the watcher sees it.
161enum Ending {
162    /// The process exited on its own. `None` when it was reaped by someone who did not keep the
163    /// status.
164    Exited(Option<ExitStatus>),
165    /// The delegation was ended on purpose rather than by the child: by the parent, through
166    /// [`LaunchedChild::shutdown`] or `Drop`, or by the completion watcher at its deadline.
167    Deliberate,
168}
169
170/// The one owner of the child's process handle.
171///
172/// Shared behind a lock between [`LaunchedChild`] — which kills and reaps — and the completion
173/// watcher, which only ever asks whether the process is still there. Single ownership of the
174/// `Child` is what keeps `pid()`, `shutdown()` and `Drop` behaving as they did: nothing else
175/// takes the handle, and nothing else waits on it.
176struct ChildProcess {
177    /// `None` once [`LaunchedChild::shutdown`] or `Drop` has reaped the process.
178    child: Option<Child>,
179    /// Set before the kill by whoever ended the delegation on purpose, so a crash is never read
180    /// as a termination. It does not say *who*: the completion watcher sets it at its own
181    /// deadline too, and tells the two apart by a flag of its own.
182    deliberate: bool,
183    /// The exit status, once anyone has observed it.
184    status: Option<ExitStatus>,
185    /// When `SIGTERM` was sent, so a child signalled ahead of its reap — see
186    /// [`LaunchedChild::begin_ending`] — is given one [`CHILD_END_GRACE`] in all, not one per
187    /// call.
188    terminated_at: Option<Instant>,
189    /// The write end of the child's spawner lifeline, open for as long as the child is this
190    /// process's to end. Closed once the child is observed ended, when it is ended deliberately
191    /// — by [`LaunchedChild::shutdown`], `Drop` or the completion watcher's deadline — or when
192    /// this process exits. Nothing writes to it.
193    #[cfg(unix)]
194    lifeline: Option<crate::lifeline::ChildLifeline>,
195}
196
197impl ChildProcess {
198    /// Close the write end of the child's spawner lifeline, if this still holds it.
199    fn close_lifeline(&mut self) {
200        #[cfg(unix)]
201        if let Some(mut lifeline) = self.lifeline.take() {
202            lifeline.close();
203        }
204    }
205
206    /// How the process ended, or `None` while it is still running.
207    ///
208    /// Reaps a process that exited on its own, which is what makes a later `wait` in `shutdown`
209    /// return the cached status rather than block.
210    fn poll(&mut self) -> Option<Ending> {
211        if self.deliberate {
212            // A child still winding down from `SIGTERM` has not ended yet: whatever it records
213            // meanwhile must land before the watcher records the ending over it.
214            return (!self.is_being_ended()).then_some(Ending::Deliberate);
215        }
216        let Some(child) = self.child.as_mut() else {
217            // Reaped by `Drop` after [`LaunchedChild::has_exited`] had already seen the exit:
218            // the child ended on its own.
219            return Some(Ending::Exited(self.status));
220        };
221        let ending = match child.try_wait() {
222            Ok(Some(status)) => {
223                self.status = Some(status);
224                Ending::Exited(Some(status))
225            }
226            Ok(None) => return None,
227            // The handle is unusable; treating the child as gone is the only outcome that lets
228            // the delegation be reported at all.
229            Err(_) => Ending::Exited(self.status),
230        };
231        self.close_lifeline();
232        Some(ending)
233    }
234
235    /// Mark the ending deliberate and send `SIGTERM` to a child that has not exited, once. A
236    /// child already seen exited ended on its own and stays so; one found exited here is reaped
237    /// instead, so the signal can never reach a reused pid: an unreaped child's pid stays its own.
238    ///
239    /// Marked under the same lock as the signal, so the watcher never reads a deliberate ending
240    /// that has not been signalled.
241    #[allow(unsafe_code)]
242    fn terminate(&mut self) {
243        if self.status.is_none() {
244            self.deliberate = true;
245        }
246        if self.terminated_at.is_some() {
247            return;
248        }
249        let Some(child) = self.child.as_mut() else {
250            return;
251        };
252        if let Ok(Some(status)) = child.try_wait() {
253            self.status = Some(status);
254            return;
255        }
256        #[cfg(unix)]
257        if let Ok(pid) = libc::pid_t::try_from(child.id()) {
258            // SAFETY: `kill` takes no pointers. `pid` is this process's own unreaped child:
259            // `try_wait` above saw it running, under the lock every reap of it takes, so the pid
260            // cannot have been reused by an unrelated process.
261            unsafe {
262                libc::kill(pid, libc::SIGTERM);
263            }
264        }
265        #[cfg(not(unix))]
266        let _ = child.kill();
267        self.terminated_at = Some(Instant::now());
268    }
269
270    /// Whether the child has been sent `SIGTERM` on purpose and not yet reaped: it is winding
271    /// down, and anything it reports meanwhile is about an ending its parent chose.
272    fn is_being_ended(&self) -> bool {
273        self.deliberate && self.status.is_none() && self.terminated_at.is_some()
274    }
275}
276
277/// End the child: `SIGTERM`, up to [`CHILD_END_GRACE`] from that signal for it to exit, then
278/// `SIGKILL`; reap it, then close its lifeline. A child that has already exited is only reaped.
279/// Idempotent.
280///
281/// The lock is held only to signal and to record the reap, never across the grace, so `pid`,
282/// `poll` and [`LaunchedChild::is_being_ended`] still answer while the child winds down. A second
283/// call during that wait returns at once and leaves the reap to the first. The lifeline stays
284/// open until the child is reaped, so a child winding down from `SIGTERM` never also reads its
285/// spawner as gone.
286fn end_process(process: &Mutex<ChildProcess>) -> Result<(), std::io::Error> {
287    let (mut child, signalled) = {
288        let mut process = lock(process);
289        process.terminate();
290        let Some(child) = process.child.take() else {
291            // Taken by an ending still waiting out the grace, which closes the lifeline itself
292            // once it has reaped the child.
293            if process.status.is_some() || process.terminated_at.is_none() {
294                process.close_lifeline();
295            }
296            return Ok(());
297        };
298        (child, process.terminated_at)
299    };
300    let waited = match signalled {
301        None => child.wait(),
302        Some(signalled) => loop {
303            match child.try_wait() {
304                Ok(Some(status)) => break Ok(status),
305                Ok(None) if signalled.elapsed() < CHILD_END_GRACE => {
306                    std::thread::sleep(CHILD_END_POLL);
307                }
308                // Past the grace, or the handle cannot be polled: `kill` on an already-exited
309                // process is not an error worth surfacing, and the wait is what retires the entry
310                // in the process table.
311                _ => {
312                    let _ = child.kill();
313                    break child.wait();
314                }
315            }
316        },
317    };
318    let mut process = lock(process);
319    process.close_lifeline();
320    match waited {
321        Ok(status) => {
322            process.status = Some(status);
323            Ok(())
324        }
325        // The handle is gone and nothing is left to wait for: the ending is over, with no status.
326        Err(error) => {
327            process.terminated_at = None;
328            Err(error)
329        }
330    }
331}
332
333/// A running child capsule and the handles to end it.
334pub struct LaunchedChild {
335    /// The directory the parent created for this child, and passed as its `--workdir`. The
336    /// child's own session artifacts land at `<workdir>/.murmur/<session_id>/`.
337    pub workdir: PathBuf,
338    /// The session id the child's runtime minted for itself.
339    pub session_id: String,
340    /// The child's A2A endpoint, `http://host:port`. Empty for a script capsule, which binds no
341    /// port and has already finished by the time it reports.
342    pub capsule_url: String,
343    /// The argument vector the parent built, in order, starting with the binary.
344    ///
345    /// Exposed so a caller — a test, notably — can assert what was passed without reading
346    /// `/proc`. It carries no token: the approval goes in on standard input.
347    pub argv: Vec<String>,
348    /// The complete environment the parent built for this child, in insertion order. Also carries
349    /// no token.
350    pub env: Vec<(String, String)>,
351    /// The id this launch's completion reports under, `None` when no spawner was supplied.
352    ///
353    /// Minted here, injected into the child as part of [`SPAWNER_ENV`], and echoed back on the
354    /// completion — the one value that joins a delegation to the task it produces at the parent.
355    pub delegation_id: Option<String>,
356    /// The operator token of a child that declares `network.authentication`, read from
357    /// `tokens.operator` on its readiness line. The parent presents it on every call to the
358    /// child's door. Never printed by `Debug`.
359    pub door_token: Option<crate::door_auth::DoorToken>,
360    /// The formation the child reports it joined, read from `formation_id` on its readiness
361    /// line. `None` when the line carries no such key, or a value that is not a formation id.
362    pub formation_id: Option<FormationId>,
363    process: Arc<Mutex<ChildProcess>>,
364    /// The child's last [`CHILD_STDERR_TAIL_LINES`] lines, retained so a crash can say why.
365    stderr_tail: Arc<StderrTail>,
366    /// When the child process was started, for the completion's `duration_ms`.
367    started: Instant,
368    /// The completion watcher's thread, for a launch whose spawner names an address.
369    watcher: Option<std::thread::JoinHandle<()>>,
370}
371
372impl std::fmt::Debug for LaunchedChild {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        f.debug_struct("LaunchedChild")
375            .field("workdir", &self.workdir)
376            .field("session_id", &self.session_id)
377            .field("capsule_url", &self.capsule_url)
378            .field("delegation_id", &self.delegation_id)
379            .field("formation_id", &self.formation_id)
380            .field(
381                "door_token",
382                &self.door_token.as_ref().map(|_| "<redacted>"),
383            )
384            .field("pid", &self.pid())
385            .finish()
386    }
387}
388
389impl LaunchedChild {
390    /// The child's operating-system process id. `0` once it has been reaped.
391    pub fn pid(&self) -> u32 {
392        lock(&self.process)
393            .child
394            .as_ref()
395            .map(Child::id)
396            .unwrap_or(0)
397    }
398
399    /// The child's last [`CHILD_STDERR_TAIL_LINES`] lines, oldest first.
400    ///
401    /// The same lines a crash completion quotes back, exposed so a caller can read what the child
402    /// said without scraping the stderr they were already echoed to.
403    pub fn stderr_tail(&self) -> Vec<String> {
404        self.stderr_tail.lines()
405    }
406
407    /// Mark the ending deliberate and send the child `SIGTERM`, without waiting for it.
408    ///
409    /// A parent ending several children calls this on each before [`Self::shutdown`] or `Drop` on
410    /// any, so they wind down together and the whole ending waits one [`CHILD_END_GRACE`] rather
411    /// than one per child. A child already exited is left to be reaped.
412    pub fn begin_ending(&self) {
413        lock(&self.process).terminate();
414    }
415
416    /// End the child — `SIGTERM`, up to [`CHILD_END_GRACE`] to exit, then `SIGKILL` — and reap
417    /// it. Idempotent: a second call, or a call after `Drop` has already run, does nothing.
418    ///
419    /// Marks the ending as deliberate, so the watcher records the delegation as `terminated` and
420    /// posts nothing: the only party that would be told is the party that did it.
421    pub fn shutdown(&mut self) -> Result<(), RuntimeError> {
422        end_process(&self.process).map_err(|error| {
423            RuntimeError::Runtime(format!("failed to reap child capsule: {error}"))
424        })
425    }
426
427    /// Whether the child is winding down from a deliberate `SIGTERM` and has not been reaped.
428    ///
429    /// The door asks this of a completion that arrives for a delegation still in the set: such a
430    /// completion is the child's own account of a cancel its parent's side chose, posted before
431    /// the launcher's `terminated` record replaces it, and it is not the delegation's outcome.
432    pub fn is_being_ended(&self) -> bool {
433        lock(&self.process).is_being_ended()
434    }
435
436    /// Whether the completion watcher behind this child has finished: it has seen the child end
437    /// and recorded, and posted, whatever it records and posts. `true` for a child no watcher
438    /// observes.
439    ///
440    /// A delegating task that ends a child asks this before it lets the child's handle go, so the
441    /// child's `completion.json` says how it ended before the parent's own process can exit.
442    pub fn watcher_finished(&self) -> bool {
443        self.watcher
444            .as_ref()
445            .is_none_or(std::thread::JoinHandle::is_finished)
446    }
447
448    /// Whether the child's process has ended, by its own exit or by being ended. The completion
449    /// watcher's own poll: a process that exited on its own is reaped and its lifeline closed.
450    ///
451    /// Never blocks: a delegating task asks this while it gives a child that has just reported
452    /// its outcome a moment to finish exiting.
453    pub fn has_exited(&self) -> bool {
454        lock(&self.process).poll().is_some()
455    }
456}
457
458impl Drop for LaunchedChild {
459    /// Ends and reaps as [`LaunchedChild::shutdown`] does, so a parent that returns early —
460    /// including by panicking — leaves no orphaned capsule process behind holding a port and a
461    /// directory. Deliberate on the same terms. A child that has already exited is only reaped.
462    fn drop(&mut self) {
463        let _ = end_process(&self.process);
464    }
465}
466
467/// `workdir` named from `root`, falling back to the absolute path when it sits outside it.
468///
469/// One rule, used by the delegating plane wherever it names a child's directory to the parent's
470/// agent or to the parent's trace.
471pub(crate) fn workdir_relative_to(workdir: &Path, root: &Path) -> String {
472    workdir
473        .strip_prefix(root)
474        .unwrap_or(workdir)
475        .to_string_lossy()
476        .replace('\\', "/")
477}
478
479/// A mutex this crate holds only for the length of one field access, so a poisoned lock is
480/// recovered from rather than turned into a panic in a `Drop`.
481fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
482    mutex
483        .lock()
484        .unwrap_or_else(|poisoned| poisoned.into_inner())
485}
486
487/// Where a child of this parent goes: `<parent accessible workdir>/.murmur/children/<name>-<16 hex>`.
488///
489/// The 16 hex characters are fresh per call, so delegating the same capsule name and version twice
490/// yields two directories rather than one shared one. Beneath `.murmur/`, alongside the parent's
491/// own session directories, so a child's tree is inside the parent's single preopen and is pruned
492/// with it.
493pub fn child_workdir_for(parent_accessible_workdir: &Path, capsule_name: &str) -> PathBuf {
494    let suffix = mac_token::random_hex(8).unwrap_or_else(|_| {
495        // The OS CSPRNG is not expected to fail; a nanosecond clock reading still distinguishes
496        // two delegations rather than collapsing them onto one directory.
497        format!(
498            "{:016x}",
499            std::time::SystemTime::now()
500                .duration_since(std::time::UNIX_EPOCH)
501                .map(|elapsed| elapsed.as_nanos() as u64)
502                .unwrap_or(0)
503        )
504    });
505    parent_accessible_workdir
506        .join(".murmur")
507        .join("children")
508        .join(format!("{capsule_name}-{suffix}"))
509}
510
511/// Create the child's directory, start `mur` on it, and wait for the child to report itself.
512///
513/// The child is handed a spawner lifeline of its own; a launch whose lifeline cannot be created
514/// starts nothing.
515pub fn launch_child_capsule(request: ChildLaunchRequest) -> Result<LaunchedChild, RuntimeError> {
516    // First, so a launch that cannot have one creates nothing: a child without a lifeline would
517    // outlive this process.
518    #[cfg(unix)]
519    let mut lifeline = crate::lifeline::ChildLifeline::new().map_err(|error| {
520        RuntimeError::Runtime(format!(
521            "the child capsule's spawner lifeline could not be created: {error}"
522        ))
523    })?;
524    let workdir = child_workdir_for(&request.parent_accessible_workdir, &request.capsule_name);
525    create_child_dir(&workdir)?;
526
527    let binary = mur_binary()?;
528    let argv: Vec<String> = vec![
529        binary.display().to_string(),
530        "run".to_string(),
531        "--capsule".to_string(),
532        request.capsule_name.clone(),
533        "--capsule-version".to_string(),
534        request.capsule_version.clone(),
535        "--workdir".to_string(),
536        workdir.display().to_string(),
537        "--json".to_string(),
538        READINESS_TOKENS_FLAG.to_string(),
539        "--no-env-file".to_string(),
540        "--spawn-grant-stdin".to_string(),
541    ];
542    // Minted here, once per launch: a caller holding one `Spawner` that delegates twice gets two
543    // delegation ids, and neither child can report under the other's.
544    let handle = request
545        .spawner
546        .as_ref()
547        .map(|spawner| SpawnerHandle::for_delegation(spawner, delegation::new_delegation_id()));
548    #[cfg_attr(not(unix), allow(unused_mut))]
549    let mut env = child_environment(&request, handle.as_ref());
550    // Recorded among the runtime-owned names, just ahead of the spawner handle it sits beside
551    // (or the formation id, or the end), exactly as `hand_to` sets it in the child.
552    #[cfg(unix)]
553    if let Some((name, value)) = lifeline.env_pair() {
554        let at = env
555            .iter()
556            .position(|(key, _)| key == SPAWNER_ENV || key == FORMATION_ID_ENV)
557            .unwrap_or(env.len());
558        env.insert(at, (name.to_string(), value));
559    }
560    let started = Instant::now();
561
562    // The grant's one and only appearance outside the parent's memory: one line on a pipe that is
563    // closed immediately afterwards. Failing to write it is fatal — the child would otherwise sit
564    // waiting on a line that will never come.
565    let StartedProcess {
566        child,
567        stderr_tail,
568        stdin_written: write_result,
569    } = start_process(&ProcessLaunch {
570        binary: &binary,
571        args: &argv[1..],
572        cwd: Some(&workdir),
573        env: ProcessEnv::Cleared(&env),
574        stdin_line: Some(request.grant.expose()),
575        stderr_prefix: None,
576        inherit_output: false,
577        own_process_group: false,
578        inherit_fd: None,
579        #[cfg(unix)]
580        lifeline: Some(Lifeline::Child(&lifeline)),
581    })
582    .map_err(|error| {
583        RuntimeError::Runtime(format!(
584            "failed to start the child capsule process '{}': {error}",
585            binary.display()
586        ))
587    })?;
588    #[cfg(unix)]
589    lifeline.spawned();
590    let write_result =
591        write_result.map_err(|error| format!("failed to hand the child its launch grant: {error}"));
592    let mut launched = LaunchedChild {
593        workdir,
594        session_id: String::new(),
595        capsule_url: String::new(),
596        argv,
597        env,
598        delegation_id: handle.as_ref().map(|handle| handle.delegation_id.clone()),
599        door_token: None,
600        formation_id: None,
601        process: Arc::new(Mutex::new(ChildProcess {
602            child: Some(child),
603            deliberate: false,
604            status: None,
605            terminated_at: None,
606            #[cfg(unix)]
607            lifeline: Some(lifeline),
608        })),
609        stderr_tail: Arc::clone(&stderr_tail),
610        started,
611        watcher: None,
612    };
613    if let Err(reason) = write_result {
614        return Err(RuntimeError::Runtime(reason));
615    }
616
617    let line = first_json_line(&mut launched, &stderr_tail)?;
618    let report: serde_json::Value = serde_json::from_str(&line).map_err(|error| {
619        RuntimeError::Runtime(format!(
620            "the child capsule's first --json line did not parse: {error}"
621        ))
622    })?;
623    let session_id = report
624        .get("session_id")
625        .and_then(serde_json::Value::as_str)
626        .ok_or_else(|| {
627            RuntimeError::Runtime(
628                "the child capsule's --json line carried no session_id".to_string(),
629            )
630        })?;
631    let url = report
632        .get("url")
633        .and_then(serde_json::Value::as_str)
634        .unwrap_or_default();
635
636    launched.session_id = session_id.to_string();
637    launched.door_token = readiness_door_token(&report);
638    launched.formation_id = report
639        .get("formation_id")
640        .and_then(serde_json::Value::as_str)
641        .and_then(|id| FormationId::parse(id).ok());
642    // A script capsule binds no port and reports an empty url; promoting that to `http://` would
643    // manufacture an address nothing answers on.
644    launched.capsule_url = if url.is_empty() {
645        String::new()
646    } else {
647        format!("http://{url}")
648    };
649
650    // Started only for a launch somebody wants told about. A lineage-only handle names no
651    // address, so a child whose parent waits on the connection it already holds is launched with
652    // no watcher behind it and posts nothing anywhere.
653    if let Some(handle) = handle {
654        if let Some(address) = handle.report_to.clone() {
655            launched.watcher = watch_for_completion(
656                &launched,
657                handle,
658                address,
659                &request.capsule_name,
660                &request.capsule_version,
661                request.completion_deadline,
662            );
663        }
664    }
665    Ok(launched)
666}
667
668/// The operator token a child's `--json` readiness line carries under `tokens.operator`, present
669/// only when the child declares `network.authentication` and was launched with
670/// [`READINESS_TOKENS_FLAG`].
671pub(crate) fn readiness_door_token(
672    report: &serde_json::Value,
673) -> Option<crate::door_auth::DoorToken> {
674    report
675        .pointer("/tokens/operator")
676        .and_then(serde_json::Value::as_str)
677        .filter(|token| !token.is_empty())
678        .map(|token| crate::door_auth::DoorToken::new(token.to_string()))
679}
680
681/// Report for a child that ended without reporting for itself, and stop one that never ends.
682///
683/// Polls the shared process handle rather than blocking on `wait`, so it never contends with
684/// [`LaunchedChild::shutdown`] or `Drop` for ownership of the `Child`. The handle stays with the
685/// delegating task, which ends the child through it; this thread observes, and reaps a child that
686/// exits on its own.
687///
688/// `deadline` bounds how long the child is watched, from the instant this watcher starts — which
689/// is the instant the child reported itself ready, not the instant its process was spawned.
690/// Getting to that point is bounded separately by [`CHILD_READY_TIMEOUT`], and folding a cold
691/// host's staging time into the delegation deadline would end a child for being slow to start
692/// rather than slow to work. On expiry the watcher stops the child itself and posts a
693/// [`DelegationStatus::Terminated`] outcome naming the bound — the deadline is the watcher's own,
694/// held in a local flag rather than read back off [`ChildProcess::deliberate`], so an ending the
695/// *parent* chose stays distinguishable from one this thread imposed. `None` watches for as long
696/// as the child runs.
697///
698/// What it does when the process ends on its own is decided by what the child left behind:
699///
700/// * A completion the child already delivered is left alone — exactly one completion per
701///   delegation reaches the parent.
702/// * A completion the child recorded but could not deliver is retried once, and the file is
703///   rewritten with the result. Nothing is retried after that.
704/// * No completion at all means the child died without a word: the watcher builds one with
705///   `status: crashed`, carrying the exit status and the child's bounded stderr tail, and posts
706///   it in the child's place.
707/// * An ending the parent chose is recorded as `terminated` and posted to nobody, because the
708///   only party that would be told is the party that did it.
709fn watch_for_completion(
710    launched: &LaunchedChild,
711    handle: SpawnerHandle,
712    address: CompletionAddress,
713    capsule_name: &str,
714    capsule_version: &str,
715    deadline: Option<Duration>,
716) -> Option<std::thread::JoinHandle<()>> {
717    let process = Arc::clone(&launched.process);
718    let stderr_tail = Arc::clone(&launched.stderr_tail);
719    let workdir = launched.workdir.clone();
720    let session_id = launched.session_id.clone();
721    let capsule_name = capsule_name.to_string();
722    let capsule_version = capsule_version.to_string();
723    let started = launched.started;
724
725    let watching_from = Instant::now();
726    let spawned = std::thread::Builder::new().spawn(move || {
727        let expires_at = deadline.map(|bound| watching_from + bound);
728        // Set here and read nowhere else: `ChildProcess::deliberate` is about to be set too, by
729        // this thread, so it can no longer say who chose the ending.
730        let mut expired = false;
731        let ending = loop {
732            if expires_at.is_some_and(|at| Instant::now() >= at) {
733                let _ = end_process(&process);
734                expired = true;
735                break Ending::Deliberate;
736            }
737            if let Some(ending) = lock(&process).poll() {
738                break ending;
739            }
740            std::thread::sleep(CHILD_WATCH_INTERVAL);
741        };
742        let duration_ms = started.elapsed().as_millis().try_into().unwrap_or(u64::MAX);
743
744        // A child ended on purpose may have recorded its own `canceled` completion while it wound
745        // down from `SIGTERM`. That record is not the outcome: the delegation was ended, and the
746        // launcher's record below is written over it.
747        let recorded = match ending {
748            Ending::Exited(_) => delegation::read_completion(&workdir),
749            Ending::Deliberate => None,
750        };
751        if let Some(recorded) = recorded {
752            if recorded.delivered {
753                return;
754            }
755            let retried = delegation::report_completion(&handle, &address, recorded, &workdir);
756            if !retried.delivered {
757                // One retry, then the file and the line above it are the record.
758                crate::runtime_err!(
759                    "[capsule-runtime] delegation {}: the child's completion is undelivered after one retry",
760                    handle.delegation_id
761                );
762            }
763            return;
764        }
765
766        let outcome = |status: DelegationStatus, detail: String| DelegationOutcome {
767            delegation_id: handle.delegation_id.clone(),
768            capsule_name: capsule_name.clone(),
769            capsule_version: capsule_version.clone(),
770            session_id: session_id.clone(),
771            status,
772            // The launcher reports for a child that recorded nothing, so it makes no claim about
773            // a result file it never saw named.
774            result_path: None,
775            workdir: workdir.display().to_string(),
776            duration_ms,
777            detail: Some(detail),
778            reported_by: Reporter::Launcher,
779            delivered: false,
780            delivery_error: None,
781        };
782
783        match ending {
784            // The deadline is the parent's runtime acting on the parent's behalf while the parent
785            // is elsewhere, so unlike an ending the parent chose by hand this one is posted: the
786            // delegating agent asked for an outcome and this is the outcome.
787            Ending::Deliberate if expired => {
788                let bound = deadline.map(|bound| bound.as_secs()).unwrap_or_default();
789                delegation::report_completion(
790                    &handle,
791                    &address,
792                    outcome(
793                        DelegationStatus::Terminated,
794                        format!(
795                            "the capsule was still running after {bound}s and was ended at the \
796                             delegation deadline"
797                        ),
798                    ),
799                    &workdir,
800                );
801            }
802            Ending::Deliberate => {
803                delegation::record_terminated(
804                    &workdir,
805                    outcome(
806                        DelegationStatus::Terminated,
807                        delegation::PARENT_ENDED_DETAIL.to_string(),
808                    ),
809                );
810            }
811            Ending::Exited(status) => {
812                let status_text = match status {
813                    Some(status) => status.to_string(),
814                    None => "unknown exit status".to_string(),
815                };
816                let tail = stderr_tail.lines_at_end().join("\n");
817                let detail = if tail.is_empty() {
818                    format!(
819                        "the child process ended without recording a completion ({status_text})"
820                    )
821                } else {
822                    format!(
823                        "the child process ended without recording a completion ({status_text}); \
824                         its last stderr lines were:\n{tail}"
825                    )
826                };
827                delegation::report_completion(
828                    &handle,
829                    &address,
830                    outcome(DelegationStatus::Crashed, detail),
831                    &workdir,
832                );
833            }
834        }
835    });
836    match spawned {
837        Ok(watcher) => Some(watcher),
838        Err(error) => {
839            crate::runtime_err!(
840                "[capsule-runtime] delegation {}: the child's completion watcher could not be \
841                 started: {error}",
842                launched.delegation_id.as_deref().unwrap_or_default()
843            );
844            None
845        }
846    }
847}
848
849/// The child's complete environment, built from a cleared one.
850///
851/// `capabilities.env.allow` is the child's own, not the parent's: a sibling's declaration reaches
852/// nothing here, and a variable the parent holds but the child did not declare is simply absent.
853/// The runtime-owned names — `PATH`, `HOME`, `MURMUR_ROOST_URL`,
854/// [`HOST_WARNINGS_REPORTED_ENV`], [`SPAWNER_ENV`] on a delegated launch and [`FORMATION_ID_ENV`]
855/// for a parent in a formation, in that order — are applied last, so a child cannot displace the
856/// daemon URL it is required to register with, the handle it reports its outcome to, or the
857/// formation it joins, by allowlisting the name. [`HOST_WARNINGS_REPORTED_ENV`] is always `1`:
858/// the host-level warnings were printed by the process the operator started, so a child does not
859/// repeat them, and its manifest cannot choose otherwise.
860///
861/// Two kinds of name are never copied from this process's environment, whatever the child
862/// allowlists:
863///
864/// * A formation member's names ([`crate::formation::is_member_grant_env`]): inheriting a
865///   formation is not a grant, so a member's child holds no channel, no token, no callee and no
866///   formation lifeline.
867/// * [`SPAWNER_LIFELINE_ENV`]: a value here names a descriptor of this process's own, the lifeline
868///   its own spawner handed it. The child holds a spawner lifeline of its own instead, which
869///   [`launch_child_capsule`] hands it alongside this environment.
870pub(crate) fn child_environment(
871    request: &ChildLaunchRequest,
872    handle: Option<&SpawnerHandle>,
873) -> Vec<(String, String)> {
874    let mut env: Vec<(String, String)> = Vec::new();
875    for key in &request.child_env_allow {
876        if let Ok(value) = std::env::var(key) {
877            env.push((key.clone(), value));
878        }
879    }
880    env.retain(|(key, _)| {
881        !matches!(key.as_str(), "PATH" | "HOME" | "MURMUR_ROOST_URL")
882            && key != SPAWNER_ENV
883            && key != FORMATION_ID_ENV
884            && key != SPAWNER_LIFELINE_ENV
885            && key != HOST_WARNINGS_REPORTED_ENV
886            && !crate::formation::is_member_grant_env(key)
887    });
888
889    if let Ok(path) = std::env::var("PATH") {
890        env.push(("PATH".to_string(), path));
891    }
892    if let Ok(home) = std::env::var("HOME") {
893        env.push(("HOME".to_string(), home));
894    }
895    env.push(("MURMUR_ROOST_URL".to_string(), request.roost_url.clone()));
896    env.push(crate::host_warnings::reported_env_pair());
897    // Last, with the other runtime-owned names, and only for a launch that has a spawner: a child
898    // nobody wants told holds no handle at all, whatever this process's own environment carries
899    // and whatever the child declared.
900    if let Some(handle) = handle {
901        env.push((SPAWNER_ENV.to_string(), handle.to_env_value()));
902    }
903    // A parent in no formation hands none on, whatever this process's own environment carries.
904    if let Some(formation_id) = &request.formation_id {
905        let (name, value) = formation_id.env_pair();
906        env.push((name.to_string(), value));
907    }
908    env
909}
910
911/// The binary a child is started from: [`MUR_BINARY_ENV`] when set, else this process's own image.
912pub(crate) fn mur_binary() -> Result<PathBuf, RuntimeError> {
913    if let Some(path) = std::env::var_os(MUR_BINARY_ENV) {
914        if !path.is_empty() {
915            return Ok(PathBuf::from(path));
916        }
917    }
918    std::env::current_exe().map_err(|error| {
919        RuntimeError::Runtime(format!(
920            "cannot locate the mur binary to launch a child capsule with: {error}"
921        ))
922    })
923}
924
925fn create_child_dir(workdir: &Path) -> Result<(), RuntimeError> {
926    std::fs::create_dir_all(workdir).map_err(|source| RuntimeError::CreateWorkdir {
927        path: workdir.display().to_string(),
928        source,
929    })?;
930    #[cfg(unix)]
931    {
932        use std::os::unix::fs::PermissionsExt;
933        std::fs::set_permissions(workdir, std::fs::Permissions::from_mode(CHILD_DIR_MODE))
934            .map_err(|source| RuntimeError::CreateWorkdir {
935                path: workdir.display().to_string(),
936                source,
937            })?;
938    }
939    Ok(())
940}
941
942/// The child's stderr as the parent keeps it: every line echoed to this process's stderr as it
943/// arrives, with the last [`CHILD_STDERR_TAIL_LINES`] retained so a child that dies before
944/// reporting can say why.
945pub(crate) struct StderrTail {
946    state: Mutex<StderrState>,
947    /// Notified once [`StderrState::at_eof`] is set.
948    drained: Condvar,
949}
950
951#[derive(Default)]
952struct StderrState {
953    lines: Vec<String>,
954    /// Whether the pipe has been read to its end, which the child's exit is what causes.
955    at_eof: bool,
956}
957
958impl StderrTail {
959    /// What the child has said so far, without waiting for anything. For a child that is still
960    /// running, which is the only state in which "so far" is the question being asked.
961    pub(crate) fn lines(&self) -> Vec<String> {
962        lock(&self.state).lines.clone()
963    }
964
965    /// What a child that has ended left behind, read only once the pipe is at its end.
966    ///
967    /// The exit that closes the child's stdout closes its stderr in the same breath, so the thread
968    /// that notices the ending and the thread still draining stderr are racing, and the tail is
969    /// empty for as long as the drain has not caught up. Reading it unsynchronised turns "refused
970    /// because the declared floor is unmeetable here" into a refusal that names no reason —
971    /// the one case the tail is kept for. Bounded by [`CHILD_STDERR_DRAIN_TIMEOUT`].
972    pub(crate) fn lines_at_end(&self) -> Vec<String> {
973        let (state, _) = self
974            .drained
975            .wait_timeout_while(lock(&self.state), CHILD_STDERR_DRAIN_TIMEOUT, |state| {
976                !state.at_eof
977            })
978            .unwrap_or_else(|poisoned| poisoned.into_inner());
979        state.lines.clone()
980    }
981
982    /// Mark the pipe finished and wake every post-mortem reader waiting on it.
983    fn finish(&self) {
984        lock(&self.state).at_eof = true;
985        self.drained.notify_all();
986    }
987}
988
989/// Start draining the child's stderr into a [`StderrTail`].
990///
991/// The child's diagnostics belong to the operator running the parent, so nothing is swallowed —
992/// this only remembers, in addition to printing. `prefix`, when given, is written in front of
993/// every echoed line and kept off the remembered ones.
994fn drain_stderr(child: &mut Child, prefix: Option<&str>) -> Arc<StderrTail> {
995    let tail = Arc::new(StderrTail {
996        state: Mutex::new(StderrState::default()),
997        drained: Condvar::new(),
998    });
999    let Some(stderr) = child.stderr.take() else {
1000        // No pipe is an end already reached: a reader that waited here would wait out the whole
1001        // bound for lines that can never arrive.
1002        tail.finish();
1003        return tail;
1004    };
1005    let collector = Arc::clone(&tail);
1006    let prefix = prefix.unwrap_or_default().to_string();
1007    std::thread::spawn(move || {
1008        for line in BufReader::new(stderr).lines().map_while(Result::ok) {
1009            crate::runtime_err!("{prefix}{line}");
1010            let mut state = lock(&collector.state);
1011            if state.lines.len() == CHILD_STDERR_TAIL_LINES {
1012                state.lines.remove(0);
1013            }
1014            state.lines.push(line);
1015        }
1016        collector.finish();
1017    });
1018    tail
1019}
1020
1021/// Read the child's first standard-output line, then keep draining the pipe.
1022///
1023/// The drain matters: a child whose stdout pipe fills blocks forever, and a capsule that logs
1024/// after its launch line would otherwise deadlock against a parent that had stopped reading.
1025fn first_json_line(
1026    launched: &mut LaunchedChild,
1027    stderr_tail: &StderrTail,
1028) -> Result<String, RuntimeError> {
1029    let stdout = lock(&launched.process)
1030        .child
1031        .as_mut()
1032        .and_then(|child| child.stdout.take())
1033        .ok_or_else(|| {
1034            RuntimeError::Runtime("the child process exposed no standard output".to_string())
1035        })?;
1036
1037    let rx = relay_readiness_line(stdout, AfterReadiness::Stdout);
1038
1039    match rx.recv_timeout(CHILD_READY_TIMEOUT) {
1040        Ok(Some(line)) => Ok(line),
1041        Ok(None) => {
1042            let status = lock(&launched.process)
1043                .child
1044                .as_mut()
1045                .and_then(|child| child.wait().ok())
1046                .map(|status| status.to_string())
1047                .unwrap_or_else(|| "unknown".to_string());
1048            // The child's own refusal — an unmeetable containment floor, a registration the
1049            // daemon declined — is written to its stderr and is the only thing that explains this
1050            // failure, so it is quoted rather than replaced by a generic "did not start".
1051            let reason = stderr_tail.lines_at_end().join("\n");
1052            Err(RuntimeError::Runtime(format!(
1053                "the child capsule '{}' exited without reporting a launch line ({status}): {reason}",
1054                launched.workdir.display()
1055            )))
1056        }
1057        Err(_) => Err(RuntimeError::Runtime(format!(
1058            "the child capsule '{}' did not report a launch line within {}s",
1059            launched.workdir.display(),
1060            CHILD_READY_TIMEOUT.as_secs()
1061        ))),
1062    }
1063}
1064
1065/// How a launched `mur run` process's environment is built.
1066pub(crate) enum ProcessEnv<'a> {
1067    /// Cleared, then exactly these pairs: a delegated child, which holds nothing of the parent's
1068    /// that it did not declare.
1069    Cleared(&'a [(String, String)]),
1070    /// This process's own, with `set` applied over it and every name in `remove` taken out: a
1071    /// formation member, which the operator launched and which reads the operator's environment.
1072    Inherited {
1073        set: &'a [(String, String)],
1074        remove: &'a [&'a str],
1075    },
1076}
1077
1078/// Where a launched process's standard output goes once its readiness line has been read.
1079pub(crate) enum AfterReadiness {
1080    /// To this process's standard output, as written.
1081    Stdout,
1082    /// To this process's standard error, one line at a time behind this prefix.
1083    PrefixedStderr(String),
1084}
1085
1086/// One `mur run` subprocess to start.
1087pub(crate) struct ProcessLaunch<'a> {
1088    pub(crate) binary: &'a Path,
1089    /// The argument vector after the binary.
1090    pub(crate) args: &'a [String],
1091    /// The working directory, or `None` for this process's own.
1092    pub(crate) cwd: Option<&'a Path>,
1093    pub(crate) env: ProcessEnv<'a>,
1094    /// One line written to the process's standard input, which is then closed; `None` gives it a
1095    /// null standard input.
1096    pub(crate) stdin_line: Option<&'a str>,
1097    /// Written in front of every stderr line echoed to this process's stderr.
1098    pub(crate) stderr_prefix: Option<&'a str>,
1099    /// Hand the process this process's own standard output and error rather than pipes. Its
1100    /// stdout is then not read here, and its stderr tail stays empty.
1101    pub(crate) inherit_output: bool,
1102    /// Start the process as the leader of a process group of its own, so a signal meant for this
1103    /// process's group — a terminal's `^C` — does not reach it, and its whole tree can be
1104    /// signalled at once.
1105    pub(crate) own_process_group: bool,
1106    /// One descriptor of this process's to hand the process at the same number: a formation
1107    /// member's channel. It is close-on-exec here, so no other process this one starts inherits
1108    /// it, and the flag is cleared in the started process alone, just before it execs.
1109    pub(crate) inherit_fd: Option<i32>,
1110    /// The lifeline whose read end the process is handed. Every lifeline end is created
1111    /// close-on-exec, and this is the one the process's spawn clears it on.
1112    #[cfg(unix)]
1113    pub(crate) lifeline: Option<Lifeline<'a>>,
1114}
1115
1116/// The lifeline a started process is handed: a formation member's, or a delegated child's.
1117#[cfg(unix)]
1118#[derive(Clone, Copy)]
1119pub(crate) enum Lifeline<'a> {
1120    Member(&'a crate::lifeline::MemberLifeline),
1121    Child(&'a crate::lifeline::ChildLifeline),
1122}
1123
1124#[cfg(unix)]
1125impl Lifeline<'_> {
1126    fn hand_to(self, command: &mut Command) {
1127        match self {
1128            Lifeline::Member(lifeline) => lifeline.hand_to(command),
1129            Lifeline::Child(lifeline) => lifeline.hand_to(command),
1130        }
1131    }
1132}
1133
1134/// A process [`start_process`] started, with its stderr already being drained.
1135pub(crate) struct StartedProcess {
1136    /// Its standard output, unless inherited, is still piped and unread.
1137    pub(crate) child: Child,
1138    pub(crate) stderr_tail: Arc<StderrTail>,
1139    /// Whether [`ProcessLaunch::stdin_line`] was written: `Ok` when there was none to write.
1140    pub(crate) stdin_written: Result<(), String>,
1141}
1142
1143/// Start one `mur run` subprocess: spawn it, write its standard-input line, and start draining its
1144/// stderr. The one spawn site for a delegated child and a formation member alike.
1145pub(crate) fn start_process(launch: &ProcessLaunch<'_>) -> std::io::Result<StartedProcess> {
1146    let mut command = Command::new(launch.binary);
1147    command
1148        .args(launch.args)
1149        .stdin(if launch.stdin_line.is_some() {
1150            Stdio::piped()
1151        } else {
1152            Stdio::null()
1153        })
1154        .stdout(if launch.inherit_output {
1155            Stdio::inherit()
1156        } else {
1157            Stdio::piped()
1158        })
1159        .stderr(if launch.inherit_output {
1160            Stdio::inherit()
1161        } else {
1162            Stdio::piped()
1163        });
1164    if let Some(cwd) = launch.cwd {
1165        command.current_dir(cwd);
1166    }
1167    match launch.env {
1168        ProcessEnv::Cleared(pairs) => {
1169            command.env_clear();
1170            for (key, value) in pairs {
1171                command.env(key, value);
1172            }
1173        }
1174        ProcessEnv::Inherited { set, remove } => {
1175            for name in remove {
1176                command.env_remove(name);
1177            }
1178            for (key, value) in set {
1179                command.env(key, value);
1180            }
1181        }
1182    }
1183    #[cfg(unix)]
1184    if launch.own_process_group {
1185        use std::os::unix::process::CommandExt;
1186        command.process_group(0);
1187    }
1188    #[cfg(unix)]
1189    if let Some(fd) = launch.inherit_fd {
1190        keep_across_exec(&mut command, fd);
1191    }
1192    // No `apply_fd_hygiene` here: it fails the spawn outright on a kernel without
1193    // `CLOSE_RANGE_CLOEXEC`, and a lifeline needs none, since both ends are created close-on-exec.
1194    // The lifeline's `pre_exec` is the last one registered.
1195    #[cfg(unix)]
1196    if let Some(lifeline) = launch.lifeline {
1197        lifeline.hand_to(&mut command);
1198    }
1199
1200    let mut child = command.spawn()?;
1201    let stdin_written = match launch.stdin_line {
1202        None => Ok(()),
1203        Some(line) => child
1204            .stdin
1205            .take()
1206            .ok_or_else(|| "the process exposed no standard input".to_string())
1207            .and_then(|mut stdin| writeln!(stdin, "{line}").map_err(|error| error.to_string())),
1208    };
1209    let stderr_tail = drain_stderr(&mut child, launch.stderr_prefix);
1210    Ok(StartedProcess {
1211        child,
1212        stderr_tail,
1213        stdin_written,
1214    })
1215}
1216
1217/// Clear `FD_CLOEXEC` on `fd` in the started process alone, after everything else that runs before
1218/// its `exec`, so that process inherits `fd` and no other does.
1219#[cfg(unix)]
1220#[allow(unsafe_code)]
1221fn keep_across_exec(command: &mut Command, fd: i32) {
1222    use std::os::unix::process::CommandExt;
1223    // SAFETY: the closure runs in the forked child between `fork` and `exec`, where only
1224    // async-signal-safe calls are allowed; it makes two `fcntl` calls on an integer descriptor,
1225    // which allocate nothing and take no lock.
1226    unsafe {
1227        command.pre_exec(move || {
1228            let flags = libc::fcntl(fd, libc::F_GETFD);
1229            if flags == -1 || libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) == -1 {
1230                return Err(std::io::Error::last_os_error());
1231            }
1232            Ok(())
1233        });
1234    }
1235}
1236
1237/// Read a launched process's first standard-output line on a thread of its own, and hand it back
1238/// on the returned channel: `Some(line)` without its line ending, or `None` when the stream ended
1239/// first.
1240///
1241/// The thread then keeps draining the pipe to `after` for as long as the process lives. The drain
1242/// matters: a process whose stdout pipe fills blocks forever, and a capsule that logs after its
1243/// launch line would otherwise deadlock against a reader that had stopped reading. This thread
1244/// outlives the readiness line by the whole life of the process, so it is the one relay most
1245/// likely to meet a reader that has already gone: it relays each line and keeps draining either
1246/// way.
1247pub(crate) fn relay_readiness_line(
1248    stdout: std::process::ChildStdout,
1249    after: AfterReadiness,
1250) -> mpsc::Receiver<Option<String>> {
1251    let (tx, rx) = mpsc::channel::<Option<String>>();
1252    std::thread::spawn(move || {
1253        let mut reader = BufReader::new(stdout);
1254        let mut line = String::new();
1255        let first = match reader.read_line(&mut line) {
1256            Ok(0) | Err(_) => None,
1257            Ok(_) => Some(line.trim_end().to_string()),
1258        };
1259        let _ = tx.send(first);
1260        let mut rest = String::new();
1261        while let Ok(read) = reader.read_line(&mut rest) {
1262            if read == 0 {
1263                break;
1264            }
1265            match &after {
1266                AfterReadiness::Stdout => {
1267                    crate::diagnostic::raw_to_stdout(&rest);
1268                }
1269                AfterReadiness::PrefixedStderr(prefix) => {
1270                    crate::runtime_err!("{prefix}{}", rest.trim_end_matches(['\r', '\n']));
1271                }
1272            }
1273            rest.clear();
1274        }
1275    });
1276    rx
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281    use super::*;
1282
1283    #[test]
1284    fn a_child_directory_is_unique_per_delegation() {
1285        let parent = PathBuf::from("/tmp/parent");
1286        let first = child_workdir_for(&parent, "worker");
1287        let second = child_workdir_for(&parent, "worker");
1288
1289        assert_ne!(first, second);
1290        assert_eq!(
1291            first.parent(),
1292            Some(parent.join(".murmur").join("children").as_path())
1293        );
1294        assert!(first
1295            .file_name()
1296            .unwrap()
1297            .to_string_lossy()
1298            .starts_with("worker-"));
1299        // `<name>-<16 hex>`
1300        assert_eq!(
1301            first.file_name().unwrap().to_string_lossy().len(),
1302            "worker-".len() + 16
1303        );
1304    }
1305
1306    fn request(env_allow: &[&str], spawner: Option<Spawner>) -> ChildLaunchRequest {
1307        ChildLaunchRequest {
1308            parent_accessible_workdir: PathBuf::from("/tmp/parent"),
1309            capsule_name: "worker".to_string(),
1310            capsule_version: "0.1.0".to_string(),
1311            grant: SpawnApproval::new("msa1.token".to_string()),
1312            child_env_allow: env_allow.iter().map(|name| name.to_string()).collect(),
1313            roost_url: "http://127.0.0.1:7700".to_string(),
1314            spawner,
1315            completion_deadline: None,
1316            formation_id: None,
1317        }
1318    }
1319
1320    /// A handle naming some *other* delegation, for the two cases that prove a child cannot be
1321    /// handed this process's own.
1322    ///
1323    /// Readable rather than nonsense, because `SPAWNER_ENV` is process-wide and every other test
1324    /// in this binary shares it: an unreadable value here refuses an unrelated `stage_session`
1325    /// running beside it.
1326    fn decoy() -> String {
1327        SpawnerHandle {
1328            session_id: "ses_decoy".to_string(),
1329            context_id: "ctx_decoy".to_string(),
1330            delegation_id: "dlg_decoy".to_string(),
1331            report_to: None,
1332        }
1333        .to_env_value()
1334    }
1335
1336    fn spawner() -> Spawner {
1337        Spawner {
1338            session_id: "ses_parent".to_string(),
1339            context_id: "ctx_parent".to_string(),
1340            report_to: Some(CompletionAddress {
1341                url: "http://127.0.0.1:7000".to_string(),
1342            }),
1343        }
1344    }
1345
1346    #[test]
1347    fn a_childs_environment_is_built_from_its_own_declaration() {
1348        std::env::set_var("MURMUR_CHILD_LAUNCH_TEST_A", "a");
1349        std::env::set_var("MURMUR_CHILD_LAUNCH_TEST_B", "b");
1350
1351        let env = child_environment(&request(&["MURMUR_CHILD_LAUNCH_TEST_A"], None), None);
1352        let names: Vec<&str> = env.iter().map(|(key, _)| key.as_str()).collect();
1353
1354        assert!(names.contains(&"MURMUR_CHILD_LAUNCH_TEST_A"));
1355        assert!(!names.contains(&"MURMUR_CHILD_LAUNCH_TEST_B"));
1356        assert!(names.contains(&"MURMUR_ROOST_URL"));
1357        assert!(!env.iter().any(|(_, value)| value.contains("msa1.token")));
1358    }
1359
1360    #[test]
1361    fn a_runtime_owned_name_cannot_be_displaced_by_a_declaration() {
1362        std::env::set_var("MURMUR_ROOST_URL", "http://127.0.0.1:1");
1363
1364        let env = child_environment(&request(&["MURMUR_ROOST_URL"], None), None);
1365        let urls: Vec<&String> = env
1366            .iter()
1367            .filter(|(key, _)| key == "MURMUR_ROOST_URL")
1368            .map(|(_, value)| value)
1369            .collect();
1370
1371        assert_eq!(urls, vec!["http://127.0.0.1:7700"]);
1372    }
1373
1374    /// The injected handle is the one the parent composed, whatever this process's own
1375    /// environment holds and whatever the child declared.
1376    #[test]
1377    fn the_spawner_handle_is_injected_last_and_cannot_be_displaced() {
1378        std::env::set_var(SPAWNER_ENV, decoy());
1379        let handle = SpawnerHandle::for_delegation(&spawner(), "dlg_0001".to_string());
1380
1381        let env = child_environment(&request(&[SPAWNER_ENV], Some(spawner())), Some(&handle));
1382        let injected: Vec<&String> = env
1383            .iter()
1384            .filter(|(key, _)| key == SPAWNER_ENV)
1385            .map(|(_, value)| value)
1386            .collect();
1387
1388        assert_eq!(injected.len(), 1, "{env:?}");
1389        assert_eq!(
1390            SpawnerHandle::parse(injected[0]).expect("the injected value is a handle"),
1391            handle
1392        );
1393        assert_eq!(
1394            env.last().map(|(key, _)| key.as_str()),
1395            Some(SPAWNER_ENV),
1396            "the handle is applied in the runtime-owned tail: {env:?}"
1397        );
1398        std::env::remove_var(SPAWNER_ENV);
1399    }
1400
1401    /// A launch with no spawner injects nothing, even from a decoy the launching process holds.
1402    #[test]
1403    fn a_launch_without_a_spawner_injects_no_handle() {
1404        std::env::set_var(SPAWNER_ENV, decoy());
1405
1406        let env = child_environment(&request(&[SPAWNER_ENV], None), None);
1407
1408        assert!(!env.iter().any(|(key, _)| key == SPAWNER_ENV), "{env:?}");
1409        std::env::remove_var(SPAWNER_ENV);
1410    }
1411
1412    /// A child's formation is the one the parent's request names, applied after the spawner
1413    /// handle, whatever this process's own environment holds and whatever the child declared.
1414    #[test]
1415    fn the_formation_id_is_injected_last_and_cannot_be_displaced() {
1416        let _guard = crate::formation::FORMATION_ENV_LOCK
1417            .lock()
1418            .unwrap_or_else(|poisoned| poisoned.into_inner());
1419        let decoy = FormationId::mint();
1420        std::env::set_var(FORMATION_ID_ENV, decoy.as_str());
1421        let injected = |env: &[(String, String)]| -> Vec<String> {
1422            env.iter()
1423                .filter(|(key, _)| key == FORMATION_ID_ENV)
1424                .map(|(_, value)| value.clone())
1425                .collect()
1426        };
1427
1428        let outside = child_environment(&request(&[FORMATION_ID_ENV], None), None);
1429        assert!(injected(&outside).is_empty(), "{outside:?}");
1430
1431        let other = FormationId::mint();
1432        let handle = SpawnerHandle::for_delegation(&spawner(), "dlg_0001".to_string());
1433        for (spawner, handle) in [(Some(spawner()), Some(&handle)), (None, None)] {
1434            let mut member = request(&[FORMATION_ID_ENV], spawner);
1435            member.formation_id = Some(other.clone());
1436            let env = child_environment(&member, handle);
1437            assert_eq!(injected(&env), vec![other.as_str().to_string()], "{env:?}");
1438            assert_eq!(
1439                env.last(),
1440                Some(&(FORMATION_ID_ENV.to_string(), other.as_str().to_string())),
1441                "the formation id is the last runtime-owned name: {env:?}"
1442            );
1443        }
1444        std::env::remove_var(FORMATION_ID_ENV);
1445    }
1446
1447    /// Every child is told its launcher already printed the host-level warnings, once, whatever
1448    /// this process's own environment holds and whatever the child declared.
1449    #[test]
1450    fn every_child_is_told_host_warnings_were_reported() {
1451        std::env::set_var(HOST_WARNINGS_REPORTED_ENV, "0");
1452        let handle = SpawnerHandle::for_delegation(&spawner(), "dlg_0001".to_string());
1453        for (spawner, handle) in [(Some(spawner()), Some(&handle)), (None, None)] {
1454            for allow in [&[][..], &[HOST_WARNINGS_REPORTED_ENV][..]] {
1455                let env = child_environment(&request(allow, spawner.clone()), handle);
1456                let values: Vec<&str> = env
1457                    .iter()
1458                    .filter(|(key, _)| key == HOST_WARNINGS_REPORTED_ENV)
1459                    .map(|(_, value)| value.as_str())
1460                    .collect();
1461                assert_eq!(values, vec!["1"], "{env:?}");
1462            }
1463        }
1464        std::env::remove_var(HOST_WARNINGS_REPORTED_ENV);
1465    }
1466
1467    /// A child's exit closes its stdout and its stderr together, so the reader that reports the
1468    /// ending can reach the tail before the drain has appended to it. The post-mortem read is the
1469    /// one that must not: an empty tail is a refusal that names no reason.
1470    #[test]
1471    fn a_tail_read_after_the_end_waits_for_the_drain_to_catch_up() {
1472        let tail = Arc::new(StderrTail {
1473            state: Mutex::new(StderrState::default()),
1474            drained: Condvar::new(),
1475        });
1476
1477        let writer = Arc::clone(&tail);
1478        std::thread::spawn(move || {
1479            std::thread::sleep(Duration::from_millis(200));
1480            lock(&writer.state)
1481                .lines
1482                .push("error[E-CAP-003]: not achievable on this host".to_string());
1483            writer.finish();
1484        });
1485
1486        assert!(tail.lines().is_empty());
1487        assert_eq!(
1488            tail.lines_at_end(),
1489            vec!["error[E-CAP-003]: not achievable on this host".to_string()]
1490        );
1491    }
1492
1493    /// A child that exposed no stderr pipe has nothing to wait for, so the bound is never spent on
1494    /// lines that cannot arrive.
1495    #[test]
1496    fn a_tail_with_no_pipe_behind_it_is_at_its_end_already() {
1497        let tail = StderrTail {
1498            state: Mutex::new(StderrState::default()),
1499            drained: Condvar::new(),
1500        };
1501        tail.finish();
1502
1503        let started = Instant::now();
1504        assert!(tail.lines_at_end().is_empty());
1505        assert!(
1506            started.elapsed() < CHILD_STDERR_DRAIN_TIMEOUT,
1507            "{started:?}"
1508        );
1509    }
1510
1511    #[test]
1512    fn child_launch_reads_the_operator_token_from_the_readiness_line() {
1513        let report = serde_json::json!({
1514            "url": "localhost:1",
1515            "session_id": "ses_x",
1516            "tokens": {"operator": "mdt1.a.b", "watcher": "mdt1.c.d"},
1517        });
1518        assert_eq!(
1519            readiness_door_token(&report).map(|token| token.expose().to_string()),
1520            Some("mdt1.a.b".to_string())
1521        );
1522        let public = serde_json::json!({"url": "localhost:1", "session_id": "ses_x"});
1523        assert_eq!(readiness_door_token(&public), None);
1524    }
1525
1526    #[test]
1527    fn child_launch_debug_prints_no_door_token() {
1528        let child = LaunchedChild {
1529            workdir: PathBuf::from("/tmp/child"),
1530            session_id: "ses_child".to_string(),
1531            capsule_url: "http://localhost:1".to_string(),
1532            argv: Vec::new(),
1533            env: Vec::new(),
1534            delegation_id: None,
1535            formation_id: None,
1536            door_token: Some(crate::door_auth::DoorToken::new(
1537                "mdt1.secretpayload.secretmac".to_string(),
1538            )),
1539            process: Arc::new(Mutex::new(ChildProcess {
1540                child: None,
1541                deliberate: false,
1542                status: None,
1543                terminated_at: None,
1544                #[cfg(unix)]
1545                lifeline: None,
1546            })),
1547            stderr_tail: Arc::new(StderrTail {
1548                state: Mutex::new(StderrState::default()),
1549                drained: Condvar::new(),
1550            }),
1551            started: Instant::now(),
1552            watcher: None,
1553        };
1554        let debug = format!("{child:?}");
1555        assert!(!debug.contains("secretpayload"), "{debug}");
1556        assert!(debug.contains("door_token"), "{debug}");
1557    }
1558
1559    /// The code of `start_process`, with its comments taken out.
1560    fn start_process_code() -> String {
1561        let source = include_str!("child_launch.rs");
1562        let start = source
1563            .find("pub(crate) fn start_process(")
1564            .expect("start_process is defined here");
1565        let end = start
1566            + source[start..]
1567                .find("\n}\n")
1568                .expect("start_process ends at a closing brace");
1569        source[start..end]
1570            .lines()
1571            .filter(|line| !line.trim_start().starts_with("//"))
1572            .collect::<Vec<_>>()
1573            .join("\n")
1574    }
1575
1576    /// The one spawn site for delegated children and formation members registers no fd hygiene:
1577    /// `mark_inherited_fds_cloexec` is a `close_range(CLOSE_RANGE_CLOEXEC)`, which fails the spawn
1578    /// outright on a kernel below 5.11. The lifeline's two-`fcntl` `pre_exec` is the last one
1579    /// registered.
1580    #[test]
1581    fn start_process_registers_no_fd_hygiene() {
1582        let code = start_process_code();
1583        for name in [
1584            "apply_fd_hygiene",
1585            "mark_inherited_fds_cloexec",
1586            "close_range",
1587        ] {
1588            assert!(!code.contains(name), "start_process names {name}:\n{code}");
1589        }
1590        let handed = code
1591            .rfind("lifeline.hand_to")
1592            .expect("the lifeline is handed");
1593        let kept = code
1594            .find("keep_across_exec")
1595            .expect("an inherited descriptor is kept");
1596        assert!(kept < handed, "the lifeline is registered last:\n{code}");
1597        assert!(
1598            !code[handed..].contains("pre_exec"),
1599            "nothing is registered after the lifeline:\n{code}"
1600        );
1601    }
1602
1603    #[cfg(unix)]
1604    fn process_with_lifeline(program: &str, args: &[&str]) -> (ChildProcess, i32) {
1605        let lifeline = crate::lifeline::ChildLifeline::new().unwrap();
1606        let write = lifeline.write_fd().unwrap();
1607        let child = Command::new(program).args(args).spawn().unwrap();
1608        (
1609            ChildProcess {
1610                child: Some(child),
1611                deliberate: false,
1612                status: None,
1613                terminated_at: None,
1614                lifeline: Some(lifeline),
1615            },
1616            write,
1617        )
1618    }
1619
1620    /// Ending a child deliberately closes the write end of its lifeline after the reap.
1621    #[cfg(unix)]
1622    #[test]
1623    fn ending_a_child_closes_its_lifeline() {
1624        let (process, _) = process_with_lifeline("sleep", &["30"]);
1625        let process = Mutex::new(process);
1626        end_process(&process).unwrap();
1627        assert!(lock(&process).lifeline.is_none());
1628        assert!(lock(&process).status.is_some());
1629        end_process(&process).unwrap();
1630    }
1631
1632    /// A child ended on purpose is sent `SIGTERM` first and given the chance to wind down: a
1633    /// child that exits on the signal is reaped with its own exit status, well inside the grace.
1634    #[cfg(unix)]
1635    #[test]
1636    fn ending_a_child_sends_sigterm_and_waits_for_it_to_wind_down() {
1637        let dir = tempfile::tempdir().unwrap();
1638        let marker = dir.path().join("wound-down");
1639        let script = format!(
1640            "trap 'echo wound-down > {}; exit 7' TERM; sleep 30 & wait",
1641            marker.display()
1642        );
1643        let (process, _) = process_with_lifeline("bash", &["-c", &script]);
1644        let process = Mutex::new(process);
1645        // Long enough for bash to install its trap before the signal arrives.
1646        std::thread::sleep(Duration::from_millis(300));
1647        let began = Instant::now();
1648        end_process(&process).unwrap();
1649        assert!(
1650            began.elapsed() < CHILD_END_GRACE,
1651            "took {:?}",
1652            began.elapsed()
1653        );
1654        assert_eq!(
1655            std::fs::read_to_string(&marker).unwrap().trim(),
1656            "wound-down"
1657        );
1658        assert_eq!(
1659            lock(&process).status.and_then(|status| status.code()),
1660            Some(7)
1661        );
1662        assert!(lock(&process).lifeline.is_none());
1663    }
1664
1665    /// A child that ignores `SIGTERM` is killed once [`CHILD_END_GRACE`] has passed, and no later.
1666    #[cfg(unix)]
1667    #[test]
1668    fn a_child_that_ignores_sigterm_is_killed_at_the_grace() {
1669        use std::os::unix::process::ExitStatusExt as _;
1670        let (process, _) =
1671            process_with_lifeline("bash", &["-c", "trap '' TERM; while :; do sleep 0.1; done"]);
1672        let process = Mutex::new(process);
1673        std::thread::sleep(Duration::from_millis(300));
1674        let began = Instant::now();
1675        end_process(&process).unwrap();
1676        let took = began.elapsed();
1677        assert!(took >= CHILD_END_GRACE, "killed before the grace: {took:?}");
1678        assert!(
1679            took < CHILD_END_GRACE + Duration::from_secs(5),
1680            "took {took:?}"
1681        );
1682        assert_eq!(
1683            lock(&process).status.and_then(|status| status.signal()),
1684            Some(libc::SIGKILL)
1685        );
1686    }
1687
1688    /// Children signalled together with [`LaunchedChild::begin_ending`] share one grace: ending
1689    /// two that both ignore `SIGTERM` takes one [`CHILD_END_GRACE`], not two.
1690    #[cfg(unix)]
1691    #[test]
1692    fn children_signalled_together_wait_one_grace_between_them() {
1693        let wedged = || {
1694            let (process, _) =
1695                process_with_lifeline("bash", &["-c", "trap '' TERM; while :; do sleep 0.1; done"]);
1696            LaunchedChild {
1697                workdir: PathBuf::from("/tmp/child"),
1698                session_id: "ses_child".to_string(),
1699                capsule_url: String::new(),
1700                argv: Vec::new(),
1701                env: Vec::new(),
1702                delegation_id: None,
1703                formation_id: None,
1704                door_token: None,
1705                process: Arc::new(Mutex::new(process)),
1706                stderr_tail: Arc::new(StderrTail {
1707                    state: Mutex::new(StderrState::default()),
1708                    drained: Condvar::new(),
1709                }),
1710                started: Instant::now(),
1711                watcher: None,
1712            }
1713        };
1714        let children = vec![wedged(), wedged()];
1715        std::thread::sleep(Duration::from_millis(300));
1716        let began = Instant::now();
1717        for child in &children {
1718            child.begin_ending();
1719        }
1720        drop(children);
1721        let took = began.elapsed();
1722        assert!(took >= CHILD_END_GRACE, "took {took:?}");
1723        assert!(
1724            took < CHILD_END_GRACE * 2,
1725            "two children waited a grace each: {took:?}"
1726        );
1727    }
1728
1729    /// A child observed to have exited on its own has its lifeline closed by the observer.
1730    #[cfg(unix)]
1731    #[test]
1732    fn a_child_seen_exited_has_its_lifeline_closed() {
1733        let (mut process, _) = process_with_lifeline("true", &[]);
1734        let deadline = Instant::now() + Duration::from_secs(10);
1735        let ending = loop {
1736            if let Some(ending) = process.poll() {
1737                break ending;
1738            }
1739            assert!(Instant::now() < deadline, "`true` never exited");
1740            std::thread::sleep(Duration::from_millis(20));
1741        };
1742        assert!(matches!(ending, Ending::Exited(Some(_))));
1743        assert!(process.lifeline.is_none());
1744    }
1745
1746    /// A delegated child lives exactly as long as its handle or its own run. Dropping the handle
1747    /// of a running child kills it and closes the write end of its lifeline; a child that exits on
1748    /// its own is seen exited by [`LaunchedChild::has_exited`], which closes the write end too,
1749    /// and its handle's drop then only reaps it.
1750    #[cfg(unix)]
1751    #[test]
1752    #[allow(unsafe_code)]
1753    fn a_childs_lifeline_closes_when_its_handle_drops_or_its_end_is_seen() {
1754        let launched = |program: &str, args: &[&str]| {
1755            let (process, write) = process_with_lifeline(program, args);
1756            let process = Arc::new(Mutex::new(process));
1757            let child = LaunchedChild {
1758                workdir: PathBuf::from("/tmp/child"),
1759                session_id: "ses_child".to_string(),
1760                capsule_url: String::new(),
1761                argv: Vec::new(),
1762                env: Vec::new(),
1763                delegation_id: None,
1764                formation_id: None,
1765                door_token: None,
1766                process: Arc::clone(&process),
1767                stderr_tail: Arc::new(StderrTail {
1768                    state: Mutex::new(StderrState::default()),
1769                    drained: Condvar::new(),
1770                }),
1771                started: Instant::now(),
1772                watcher: None,
1773            };
1774            (child, process, write)
1775        };
1776        let is_open = |fd: i32| {
1777            // SAFETY: `F_GETFD` takes and returns integers.
1778            unsafe { libc::fcntl(fd, libc::F_GETFD) >= 0 }
1779        };
1780
1781        // Held: the child runs and the write end stays open for as long as the handle does.
1782        let (child, process, write) = launched("sleep", &["30"]);
1783        assert!(!child.has_exited());
1784        assert!(is_open(write), "a held child's write end is open");
1785        drop(child);
1786        assert!(
1787            lock(&process).lifeline.is_none(),
1788            "dropping the handle closes it"
1789        );
1790        assert!(
1791            lock(&process).deliberate,
1792            "a running child's drop is deliberate"
1793        );
1794        // A status is set only by a `wait` that returned, so the child is reaped and gone.
1795        assert!(lock(&process).status.is_some(), "and reaps the child");
1796
1797        // Ran to its end: seen exited, the write end closes without the handle being dropped.
1798        let (child, process, _) = launched("true", &[]);
1799        let deadline = Instant::now() + Duration::from_secs(10);
1800        while !child.has_exited() {
1801            assert!(Instant::now() < deadline, "`true` never exited");
1802            std::thread::sleep(Duration::from_millis(20));
1803        }
1804        assert!(
1805            lock(&process).lifeline.is_none(),
1806            "seeing the end closes it"
1807        );
1808        drop(child);
1809        assert!(
1810            !lock(&process).deliberate,
1811            "a child that ended on its own is not recorded as ended by its parent"
1812        );
1813    }
1814
1815    /// A child its parent ends on purpose is recorded `terminated` by the launcher, in the words
1816    /// every such record uses, and posted to nobody: the address here answers nothing.
1817    #[cfg(unix)]
1818    #[test]
1819    fn a_child_its_parent_ends_is_recorded_in_the_shared_words() {
1820        let dir = tempfile::tempdir().unwrap();
1821        let (process, _) = process_with_lifeline("sleep", &["30"]);
1822        let mut child = LaunchedChild {
1823            workdir: dir.path().to_path_buf(),
1824            session_id: "ses_child".to_string(),
1825            capsule_url: String::new(),
1826            argv: Vec::new(),
1827            env: Vec::new(),
1828            delegation_id: Some("dlg_watched".to_string()),
1829            formation_id: None,
1830            door_token: None,
1831            process: Arc::new(Mutex::new(process)),
1832            stderr_tail: Arc::new(StderrTail {
1833                state: Mutex::new(StderrState::default()),
1834                drained: Condvar::new(),
1835            }),
1836            started: Instant::now(),
1837            watcher: None,
1838        };
1839        let watcher = watch_for_completion(
1840            &child,
1841            SpawnerHandle::for_delegation(&spawner(), "dlg_watched".to_string()),
1842            CompletionAddress {
1843                url: "http://127.0.0.1:1".to_string(),
1844            },
1845            "worker",
1846            "0.1.0",
1847            None,
1848        )
1849        .expect("the watcher starts");
1850
1851        child.shutdown().unwrap();
1852        watcher.join().unwrap();
1853
1854        let recorded = delegation::read_completion(dir.path()).expect("the ending is recorded");
1855        assert_eq!(recorded.status, DelegationStatus::Terminated);
1856        assert_eq!(recorded.reported_by, Reporter::Launcher);
1857        assert!(!recorded.delivered);
1858        assert_eq!(
1859            recorded.detail.as_deref(),
1860            Some(delegation::PARENT_ENDED_DETAIL)
1861        );
1862    }
1863
1864    /// A child that records its own completion while it winds down from `SIGTERM` does not
1865    /// decide the outcome of a delegation its parent ended: the launcher's `terminated` record is
1866    /// written over it, and the child's is never re-posted.
1867    #[cfg(unix)]
1868    #[test]
1869    fn a_completion_the_child_records_while_being_ended_is_overwritten_by_the_launcher() {
1870        let dir = tempfile::tempdir().unwrap();
1871        let own = DelegationOutcome {
1872            delegation_id: "dlg_watched".to_string(),
1873            capsule_name: "worker".to_string(),
1874            capsule_version: "0.1.0".to_string(),
1875            session_id: "ses_child".to_string(),
1876            status: DelegationStatus::Error,
1877            result_path: None,
1878            workdir: dir.path().display().to_string(),
1879            duration_ms: 1,
1880            detail: None,
1881            reported_by: Reporter::Child,
1882            delivered: false,
1883            delivery_error: None,
1884        };
1885        let staged = dir.path().join("own-record.json");
1886        std::fs::write(&staged, serde_json::to_vec(&own).unwrap()).unwrap();
1887        let script = format!(
1888            "trap 'cp {} {}; exit 0' TERM; sleep 30 & wait",
1889            staged.display(),
1890            dir.path().join(delegation::COMPLETION_FILE).display()
1891        );
1892        let (process, _) = process_with_lifeline("bash", &["-c", &script]);
1893        let mut child = LaunchedChild {
1894            workdir: dir.path().to_path_buf(),
1895            session_id: "ses_child".to_string(),
1896            capsule_url: String::new(),
1897            argv: Vec::new(),
1898            env: Vec::new(),
1899            delegation_id: Some("dlg_watched".to_string()),
1900            formation_id: None,
1901            door_token: None,
1902            process: Arc::new(Mutex::new(process)),
1903            stderr_tail: Arc::new(StderrTail {
1904                state: Mutex::new(StderrState::default()),
1905                drained: Condvar::new(),
1906            }),
1907            started: Instant::now(),
1908            watcher: None,
1909        };
1910        let watcher = watch_for_completion(
1911            &child,
1912            SpawnerHandle::for_delegation(&spawner(), "dlg_watched".to_string()),
1913            CompletionAddress {
1914                url: "http://127.0.0.1:1".to_string(),
1915            },
1916            "worker",
1917            "0.1.0",
1918            None,
1919        )
1920        .expect("the watcher starts");
1921        std::thread::sleep(Duration::from_millis(300));
1922
1923        child.shutdown().unwrap();
1924        watcher.join().unwrap();
1925        let recorded = delegation::read_completion(dir.path()).expect("the ending is recorded");
1926        assert_eq!(recorded.status, DelegationStatus::Terminated);
1927        assert_eq!(recorded.reported_by, Reporter::Launcher);
1928        assert!(!recorded.delivered);
1929        assert_eq!(recorded.delivery_error, None);
1930    }
1931}