Skip to main content

codex_exec_server/
process.rs

1use std::collections::VecDeque;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::sync::Mutex as StdMutex;
6
7use tokio::sync::broadcast;
8use tokio::sync::watch;
9
10use crate::ExecServerError;
11use crate::ProcessId;
12use crate::protocol::ExecParams;
13use crate::protocol::ProcessOutputChunk;
14use crate::protocol::ProcessSignal;
15use crate::protocol::ReadResponse;
16use crate::protocol::WriteResponse;
17
18pub struct StartedExecProcess {
19    pub process: Arc<dyn ExecProcess>,
20}
21
22/// Pushed process events for consumers that want to follow process output as it
23/// arrives instead of polling retained output with [`ExecProcess::read`].
24///
25/// The stream is scoped to one [`ExecProcess`] handle. `Output` events carry
26/// stdout, stderr, or pty bytes. `Exited` reports the process exit status, while
27/// `Closed` means all output streams have ended and no more output events will
28/// arrive. `Failed` is used when the process session cannot continue, for
29/// example because the remote environment connection disconnected.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ExecProcessEvent {
32    Output(ProcessOutputChunk),
33    Exited {
34        seq: u64,
35        exit_code: i32,
36        sandbox_denied: Option<bool>,
37    },
38    Closed {
39        seq: u64,
40    },
41    Failed(String),
42}
43
44/// Replay buffer plus live fan-out for pushed process events.
45///
46/// New subscribers first drain a bounded replay history, then continue on the
47/// live broadcast channel. The history is bounded by event count and retained
48/// output bytes: count protects against many tiny events, while bytes protects
49/// against a few very large output chunks.
50#[derive(Clone)]
51pub(crate) struct ExecProcessEventLog {
52    inner: Arc<ExecProcessEventLogInner>,
53}
54
55struct ExecProcessEventLogInner {
56    history: StdMutex<ExecProcessEventHistory>,
57    live_tx: broadcast::Sender<ExecProcessEvent>,
58    event_capacity: usize,
59    byte_capacity: usize,
60}
61
62#[derive(Default)]
63struct ExecProcessEventHistory {
64    events: VecDeque<ExecProcessEvent>,
65    retained_bytes: usize,
66}
67
68impl ExecProcessEvent {
69    /// Sequence number used to order process-owned events.
70    ///
71    /// `Failed` is intentionally unsequenced because it is synthesized by the
72    /// client when the session or transport fails, not emitted by the process.
73    pub(crate) fn seq(&self) -> Option<u64> {
74        match self {
75            ExecProcessEvent::Output(chunk) => Some(chunk.seq),
76            ExecProcessEvent::Exited { seq, .. } | ExecProcessEvent::Closed { seq } => Some(*seq),
77            ExecProcessEvent::Failed(_) => None,
78        }
79    }
80
81    fn retained_len(&self) -> usize {
82        match self {
83            ExecProcessEvent::Output(chunk) => chunk.chunk.0.len(),
84            ExecProcessEvent::Failed(message) => message.len(),
85            ExecProcessEvent::Exited { .. } | ExecProcessEvent::Closed { .. } => 0,
86        }
87    }
88}
89
90impl ExecProcessEventLog {
91    pub(crate) fn new(event_capacity: usize, byte_capacity: usize) -> Self {
92        let (live_tx, _live_rx) = broadcast::channel(event_capacity);
93        Self {
94            inner: Arc::new(ExecProcessEventLogInner {
95                history: StdMutex::new(ExecProcessEventHistory::default()),
96                live_tx,
97                event_capacity,
98                byte_capacity,
99            }),
100        }
101    }
102
103    pub(crate) fn publish(&self, event: ExecProcessEvent) {
104        let mut history = self
105            .inner
106            .history
107            .lock()
108            .unwrap_or_else(std::sync::PoisonError::into_inner);
109        history.retained_bytes += event.retained_len();
110        history.events.push_back(event.clone());
111        while history.events.len() > self.inner.event_capacity
112            || history.retained_bytes > self.inner.byte_capacity
113        {
114            let Some(evicted) = history.events.pop_front() else {
115                break;
116            };
117            history.retained_bytes = history
118                .retained_bytes
119                .saturating_sub(evicted.retained_len());
120        }
121
122        let _ = self.inner.live_tx.send(event);
123    }
124
125    pub(crate) fn subscribe(&self) -> ExecProcessEventReceiver {
126        let history = self
127            .inner
128            .history
129            .lock()
130            .unwrap_or_else(std::sync::PoisonError::into_inner);
131        let live_rx = self.inner.live_tx.subscribe();
132        let replay = history.events.iter().cloned().collect();
133
134        ExecProcessEventReceiver {
135            replay,
136            live_rx,
137            _keepalive: None,
138        }
139    }
140}
141
142pub struct ExecProcessEventReceiver {
143    replay: VecDeque<ExecProcessEvent>,
144    live_rx: broadcast::Receiver<ExecProcessEvent>,
145    _keepalive: Option<broadcast::Sender<ExecProcessEvent>>,
146}
147
148impl ExecProcessEventReceiver {
149    /// Returns a receiver that remains open without yielding events.
150    pub fn empty() -> Self {
151        let (live_tx, live_rx) = broadcast::channel(1);
152        Self {
153            replay: VecDeque::new(),
154            live_rx,
155            _keepalive: Some(live_tx),
156        }
157    }
158
159    /// Returns the next replayed or live event.
160    ///
161    /// `Lagged` means this receiver fell behind the bounded live channel. The
162    /// caller should recover through [`ExecProcess::read`] using the last
163    /// delivered sequence number, then continue receiving pushed events.
164    pub async fn recv(&mut self) -> Result<ExecProcessEvent, broadcast::error::RecvError> {
165        if let Some(event) = self.replay.pop_front() {
166            return Ok(event);
167        }
168
169        self.live_rx.recv().await
170    }
171}
172
173/// Handle for an executor-managed process.
174///
175/// Implementations must support both retained-output reads and pushed events:
176/// `read` is the request/response API for callers that want to page through
177/// buffered output, while `subscribe_events` is the streaming API for callers
178/// that want output and lifecycle changes delivered as they happen.
179pub trait ExecProcess: Send + Sync {
180    fn process_id(&self) -> &ProcessId;
181
182    fn subscribe_wake(&self) -> watch::Receiver<u64>;
183
184    fn subscribe_events(&self) -> ExecProcessEventReceiver;
185
186    fn read(
187        &self,
188        after_seq: Option<u64>,
189        max_bytes: Option<usize>,
190        wait_ms: Option<u64>,
191    ) -> ExecProcessFuture<'_, ReadResponse>;
192
193    fn write(&self, chunk: Vec<u8>) -> ExecProcessFuture<'_, WriteResponse>;
194
195    fn signal(&self, signal: ProcessSignal) -> ExecProcessFuture<'_, ()>;
196
197    fn terminate(&self) -> ExecProcessFuture<'_, ()>;
198}
199
200pub type ExecProcessFuture<'a, T> =
201    Pin<Box<dyn Future<Output = Result<T, ExecServerError>> + Send + 'a>>;
202
203pub trait ExecBackend: Send + Sync {
204    fn start(&self, params: ExecParams) -> ExecBackendFuture<'_>;
205}
206
207pub type ExecBackendFuture<'a> =
208    Pin<Box<dyn Future<Output = Result<StartedExecProcess, ExecServerError>> + Send + 'a>>;
209
210#[cfg(test)]
211mod tests {
212    use pretty_assertions::assert_eq;
213    use tokio::time::Duration;
214    use tokio::time::timeout;
215
216    use super::ExecProcessEvent;
217    use super::ExecProcessEventLog;
218    use super::ExecProcessEventReceiver;
219    use crate::protocol::ExecOutputStream;
220    use crate::protocol::ProcessOutputChunk;
221
222    #[tokio::test]
223    async fn empty_event_receiver_stays_open() {
224        let mut events = ExecProcessEventReceiver::empty();
225
226        assert!(
227            timeout(Duration::from_millis(10), events.recv())
228                .await
229                .is_err()
230        );
231    }
232
233    #[tokio::test]
234    async fn event_history_replay_is_bounded_by_retained_bytes() {
235        let log = ExecProcessEventLog::new(/*event_capacity*/ 8, /*byte_capacity*/ 3);
236
237        log.publish(ExecProcessEvent::Output(ProcessOutputChunk {
238            seq: 1,
239            stream: ExecOutputStream::Stdout,
240            chunk: b"large".to_vec().into(),
241        }));
242        log.publish(ExecProcessEvent::Exited {
243            seq: 2,
244            exit_code: 0,
245            sandbox_denied: Some(false),
246        });
247        log.publish(ExecProcessEvent::Closed { seq: 3 });
248
249        let mut events = log.subscribe();
250        let replay = vec![
251            timeout(Duration::from_secs(1), events.recv())
252                .await
253                .expect("exit event replay should not time out")
254                .expect("exit event replay should be available"),
255            timeout(Duration::from_secs(1), events.recv())
256                .await
257                .expect("closed event replay should not time out")
258                .expect("closed event replay should be available"),
259        ];
260
261        assert_eq!(
262            replay,
263            vec![
264                ExecProcessEvent::Exited {
265                    seq: 2,
266                    exit_code: 0,
267                    sandbox_denied: Some(false),
268                },
269                ExecProcessEvent::Closed { seq: 3 },
270            ]
271        );
272    }
273}