Skip to main content

running_process/broker/
session_server.rs

1//! Phase 3 session server (soldr#2365, slice 3b): run a **contained** child as a
2//! broker-proxied session over a byte transport.
3//!
4//! This ties together three already-merged pieces:
5//!   - the byte-transparent proxy pump ([`crate::broker::session_pump`]),
6//!   - the SESSION-lane codec ([`crate::broker::session_codec`]),
7//!   - the sanitized contained-spawn layer
8//!     ([`ContainedProcessGroup`] → [`SpawnedChild`], a child confined to its own
9//!     Job Object on Windows / process group on Unix, killed when dropped).
10//!
11//! [`serve_session`] reads inbound `SessionFrame`s off a reader `R`, applies them
12//! to the child's stdin, streams the child's stdout/stderr/exit back out as
13//! `SessionFrame`s on a writer `W`, and reaps the child. It is generic over the
14//! two transport halves (`R: Read` inbound, `W: Write` outbound), matching
15//! [`crate::broker::backend_sdk::FrameClient::from_stream`]'s
16//! generic-over-stream grain, so the real broker `local_socket` — whose
17//! raw-duplex takeover (`into_backend_io`) is Windows-deferred (#720) — is wired
18//! in a later slice without changing this code.
19//!
20//! Nothing dials this yet; it is additive and dormant.
21//!
22//! **Client contract:** the client closes its inbound-writing half once it has
23//! sent stdin + `StdinEof`. `serve_session` returns only after the child exits,
24//! the inbound stream reaches EOF, and all outbound frames are flushed — so a
25//! client that holds the inbound half open forever keeps the session's stdin
26//! pump thread alive. This mirrors the pump's existing `stdin_rx`-closed
27//! contract and is exactly what a dumb-terminal client (slice 4) does.
28
29use std::io::{self, Read, Write};
30use std::process::Command;
31use std::sync::mpsc::{channel, Receiver, Sender};
32use std::thread;
33
34use crate::broker::protocol_v2::{SessionExit, SessionFrame};
35use crate::broker::session_codec::{encode_session_frame, try_decode_session_frame};
36use crate::broker::session_pump::{run_child_session, SessionChild};
37use crate::containment::ContainedProcessGroup;
38use crate::spawn::{SpawnStdio, SpawnedChild, StdioSource};
39
40impl SessionChild for SpawnedChild {
41    type Stdin = std::process::ChildStdin;
42    type Stdout = std::process::ChildStdout;
43    type Stderr = std::process::ChildStderr;
44
45    fn take_stdin(&mut self) -> Option<Self::Stdin> {
46        self.stdin.take()
47    }
48    fn take_stdout(&mut self) -> Option<Self::Stdout> {
49        self.stdout.take()
50    }
51    fn take_stderr(&mut self) -> Option<Self::Stderr> {
52        self.stderr.take()
53    }
54    fn wait_session(&mut self) -> io::Result<SessionExit> {
55        // The sanitized contained-spawn layer reports a single exit code (its
56        // own `unix_exit_code` mapping folds a signal death into that code), so
57        // `signal` is always 0 on this path. The daemon keys on the code.
58        Ok(SessionExit {
59            code: self.wait()?,
60            signal: 0,
61            metadata: Default::default(),
62        })
63    }
64}
65
66/// Spawn `command` as a contained child — its own Job Object (Windows) /
67/// process group (Unix), killed when the returned [`SpawnedChild`] drops — with
68/// all three stdio streams piped, ready to hand to [`serve_session`] or
69/// [`run_child_session`].
70///
71/// The pipes carry raw bytes (no line splitting), which is what preserves the
72/// byte-for-byte fidelity the pump guarantees.
73///
74/// # Errors
75///
76/// Propagates any spawn failure from the sanitized contained-spawn layer.
77pub fn spawn_contained_session(
78    group: &ContainedProcessGroup,
79    command: &mut Command,
80) -> io::Result<SpawnedChild> {
81    let stdio = SpawnStdio {
82        stdin: StdioSource::Pipe,
83        stdout: StdioSource::Pipe,
84        stderr: StdioSource::Pipe,
85        ..SpawnStdio::default()
86    };
87    group.spawn(command, stdio)
88}
89
90/// [`spawn_contained_session`] with an explicit environment base policy.
91pub fn spawn_contained_session_with_environment(
92    group: &ContainedProcessGroup,
93    command: &mut Command,
94    environment_policy: crate::EnvironmentPolicy,
95) -> io::Result<SpawnedChild> {
96    let stdio = SpawnStdio {
97        stdin: StdioSource::Pipe,
98        stdout: StdioSource::Pipe,
99        stderr: StdioSource::Pipe,
100        ..SpawnStdio::default()
101    };
102    group.spawn_with_environment_policy(command, stdio, environment_policy)
103}
104
105/// Drive `child` as a proxied session over a byte transport.
106///
107/// Inbound `SessionFrame`s are decoded off `inbound` and applied to the child's
108/// stdin; the child's stdout/stderr/exit are encoded onto `outbound`. Returns
109/// the child's [`SessionExit`] once it exits, `inbound` reaches EOF, and every
110/// outbound frame has been flushed. An `Err` reflects a transport or reap
111/// failure, never stdio content.
112///
113/// `child` must have all three stdio streams piped (use
114/// [`spawn_contained_session`]).
115pub fn serve_session<C, R, W>(child: C, inbound: R, outbound: W) -> io::Result<SessionExit>
116where
117    C: SessionChild,
118    R: Read + Send + 'static,
119    W: Write + Send + 'static,
120{
121    let (stdin_tx, stdin_rx) = channel::<SessionFrame>();
122    let (out_tx, out_rx) = channel::<SessionFrame>();
123
124    // Inbound: decode client→daemon frames and feed them to the pump's stdin
125    // channel. When `inbound` hits EOF the thread returns, dropping `stdin_tx`,
126    // which lets the pump's stdin thread finish.
127    let inbound_thread = thread::spawn(move || decode_inbound(inbound, stdin_tx));
128    // Outbound: encode each daemon→client frame and write it to the transport.
129    let outbound_thread = thread::spawn(move || encode_outbound(out_rx, outbound));
130
131    let exit = run_child_session(child, out_tx, stdin_rx)?;
132    // The pump has sent its terminal Exit frame and dropped `out_tx`; draining
133    // threads now finish on their own.
134    let _ = inbound_thread.join();
135    let outbound_result = outbound_thread.join();
136    // Surface a transport write error from the outbound half; a join panic is
137    // reported as a broken-session error rather than swallowed.
138    match outbound_result {
139        Ok(result) => result?,
140        Err(_) => return Err(io::Error::other("session outbound thread panicked")),
141    }
142    Ok(exit)
143}
144
145/// Read framed `SessionFrame`s from `inbound` and forward each to `stdin_tx`
146/// until EOF (or the pump hangs up). Returns on EOF so the caller's `stdin_tx`
147/// drop signals end-of-stdin to the pump.
148fn decode_inbound<R: Read>(mut inbound: R, stdin_tx: Sender<SessionFrame>) -> io::Result<()> {
149    let mut buf: Vec<u8> = Vec::new();
150    let mut chunk = [0u8; 8192];
151    loop {
152        // Drain every complete frame currently buffered.
153        loop {
154            match try_decode_session_frame(&buf) {
155                Ok(Some(decoded)) => {
156                    buf.drain(..decoded.consumed);
157                    if stdin_tx.send(decoded.frame).is_err() {
158                        return Ok(()); // pump gone; stop reading
159                    }
160                }
161                Ok(None) => break,
162                Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)),
163            }
164        }
165        match inbound.read(&mut chunk) {
166            Ok(0) => return Ok(()),
167            Ok(n) => buf.extend_from_slice(&chunk[..n]),
168            Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue,
169            Err(err) => return Err(err),
170        }
171    }
172}
173
174/// Encode each `SessionFrame` from `out_rx` and write it to `outbound`, flushing
175/// per frame so a streaming client sees output promptly. Returns when the pump
176/// drops its sender.
177fn encode_outbound<W: Write>(out_rx: Receiver<SessionFrame>, mut outbound: W) -> io::Result<()> {
178    for (seq, frame) in out_rx.into_iter().enumerate() {
179        let wire = encode_session_frame(&frame, seq as u64)
180            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
181        outbound.write_all(&wire)?;
182        outbound.flush()?;
183    }
184    Ok(())
185}
186
187#[cfg(test)]
188mod tests;