1use std::collections::BTreeMap;
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::sync::Mutex as StdMutex;
5use std::sync::OnceLock;
6use std::sync::atomic::AtomicBool;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9use std::time::Duration;
10
11use arc_swap::ArcSwap;
12use codex_exec_server_protocol::JSONRPCNotification;
13use futures::FutureExt;
14use futures::future::BoxFuture;
15use serde_json::Value;
16use tokio::sync::Mutex;
17use tokio::sync::OnceCell;
18use tokio::sync::Semaphore;
19use tokio::sync::mpsc;
20use tokio::sync::watch;
21use tokio_util::task::AbortOnDropHandle;
22
23use tokio::time::timeout;
24use tracing::Instrument;
25use tracing::debug;
26use tracing::instrument::WithSubscriber;
27
28use crate::ProcessId;
29use crate::client::http_client::response_body_stream::MAX_QUEUED_HTTP_BODY_BYTES;
30use crate::client::http_client::response_body_stream::QueuedHttpBodyDelta;
31use crate::client_api::ExecServerClientConnectOptions;
32use crate::client_api::ExecServerTransportParams;
33use crate::client_api::HttpClient;
34use crate::client_api::RemoteExecServerConnectArgs;
35use crate::client_api::StdioExecServerConnectArgs;
36use crate::client_transport::ExecServerReconnectStrategy;
37use crate::connection::JsonRpcConnection;
38use crate::environment::EnvironmentConnectionState;
39use crate::process::ExecProcessEvent;
40use crate::process::ExecProcessEventLog;
41use crate::process::ExecProcessEventReceiver;
42use crate::protocol::CAPABILITY_ROOTS_DISCOVER_METHOD;
43use crate::protocol::CapabilityRootsDiscoverParams;
44use crate::protocol::CapabilityRootsDiscoverResponse;
45use crate::protocol::ENVIRONMENT_INFO_METHOD;
46use crate::protocol::ENVIRONMENT_STATUS_METHOD;
47use crate::protocol::EXEC_CLOSED_METHOD;
48use crate::protocol::EXEC_EXITED_METHOD;
49use crate::protocol::EXEC_METHOD;
50use crate::protocol::EXEC_OUTPUT_DELTA_METHOD;
51use crate::protocol::EXEC_READ_METHOD;
52use crate::protocol::EXEC_SIGNAL_METHOD;
53use crate::protocol::EXEC_TERMINATE_METHOD;
54use crate::protocol::EXEC_WRITE_METHOD;
55use crate::protocol::EnvironmentInfo;
56use crate::protocol::EnvironmentStatus;
57use crate::protocol::ExecClosedNotification;
58use crate::protocol::ExecExitedNotification;
59use crate::protocol::ExecOutputDeltaNotification;
60use crate::protocol::ExecParams;
61use crate::protocol::ExecResponse;
62use crate::protocol::FS_CANONICALIZE_METHOD;
63use crate::protocol::FS_CLOSE_METHOD;
64use crate::protocol::FS_COPY_METHOD;
65use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
66use crate::protocol::FS_GET_METADATA_METHOD;
67use crate::protocol::FS_OPEN_METHOD;
68use crate::protocol::FS_READ_BLOCK_METHOD;
69use crate::protocol::FS_READ_DIRECTORY_METHOD;
70use crate::protocol::FS_READ_FILE_METHOD;
71use crate::protocol::FS_REMOVE_METHOD;
72use crate::protocol::FS_WALK_METHOD;
73use crate::protocol::FS_WRITE_FILE_METHOD;
74use crate::protocol::FsCanonicalizeParams;
75use crate::protocol::FsCanonicalizeResponse;
76use crate::protocol::FsCloseParams;
77use crate::protocol::FsCloseResponse;
78use crate::protocol::FsCopyParams;
79use crate::protocol::FsCopyResponse;
80use crate::protocol::FsCreateDirectoryParams;
81use crate::protocol::FsCreateDirectoryResponse;
82use crate::protocol::FsGetMetadataParams;
83use crate::protocol::FsGetMetadataResponse;
84use crate::protocol::FsOpenParams;
85use crate::protocol::FsOpenResponse;
86use crate::protocol::FsReadBlockParams;
87use crate::protocol::FsReadBlockResponse;
88use crate::protocol::FsReadDirectoryParams;
89use crate::protocol::FsReadDirectoryResponse;
90use crate::protocol::FsReadFileParams;
91use crate::protocol::FsReadFileResponse;
92use crate::protocol::FsRemoveParams;
93use crate::protocol::FsRemoveResponse;
94use crate::protocol::FsWalkParams;
95use crate::protocol::FsWalkResponse;
96use crate::protocol::FsWriteFileParams;
97use crate::protocol::FsWriteFileResponse;
98use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD;
99use crate::protocol::INITIALIZE_METHOD;
100use crate::protocol::INITIALIZED_METHOD;
101use crate::protocol::InitializeParams;
102use crate::protocol::InitializeResponse;
103use crate::protocol::ProcessOutputChunk;
104use crate::protocol::ProcessSignal;
105use crate::protocol::ReadParams;
106use crate::protocol::ReadResponse;
107use crate::protocol::SignalParams;
108use crate::protocol::SignalResponse;
109use crate::protocol::TerminateParams;
110use crate::protocol::TerminateResponse;
111use crate::protocol::WriteParams;
112use crate::protocol::WriteResponse;
113use crate::rpc::RpcCallError;
114use crate::rpc::RpcClient;
115
116pub(crate) mod http_client;
117#[path = "client_recovery.rs"]
118mod recovery;
119
120const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
121const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
122const ENVIRONMENT_INFO_TIMEOUT: Duration = Duration::from_secs(30);
123const ENVIRONMENT_STATUS_TIMEOUT: Duration = Duration::from_secs(10);
124const PROCESS_EVENT_CHANNEL_CAPACITY: usize = 256;
125const PROCESS_EVENT_RETAINED_BYTES: usize = 1024 * 1024;
126const MAX_PENDING_PROCESS_EVENTS: usize = 256;
127const MAX_PENDING_PROCESS_EVENT_BYTES: usize = 1024 * 1024;
128
129impl Default for ExecServerClientConnectOptions {
130 fn default() -> Self {
131 Self {
132 client_name: "codex-core".to_string(),
133 initialize_timeout: INITIALIZE_TIMEOUT,
134 resume_session_id: None,
135 }
136 }
137}
138
139impl From<RemoteExecServerConnectArgs> for ExecServerClientConnectOptions {
140 fn from(value: RemoteExecServerConnectArgs) -> Self {
141 Self {
142 client_name: value.client_name,
143 initialize_timeout: value.initialize_timeout,
144 resume_session_id: value.resume_session_id,
145 }
146 }
147}
148
149impl From<StdioExecServerConnectArgs> for ExecServerClientConnectOptions {
150 fn from(value: StdioExecServerConnectArgs) -> Self {
151 Self {
152 client_name: value.client_name,
153 initialize_timeout: value.initialize_timeout,
154 resume_session_id: value.resume_session_id,
155 }
156 }
157}
158
159impl RemoteExecServerConnectArgs {
160 pub fn new(websocket_url: String, client_name: String) -> Self {
161 Self {
162 websocket_url,
163 client_name,
164 connect_timeout: CONNECT_TIMEOUT,
165 initialize_timeout: INITIALIZE_TIMEOUT,
166 resume_session_id: None,
167 }
168 }
169}
170
171pub(crate) struct SessionState {
172 wake_tx: watch::Sender<u64>,
173 events: ExecProcessEventLog,
174 ordered_events: StdMutex<OrderedSessionEvents>,
175 recoverable: AtomicBool,
176 next_write_id: AtomicU64,
177}
178
179#[derive(Default)]
180struct OrderedSessionEvents {
181 last_published_seq: u64,
182 exit_published: bool,
183 closed_published: bool,
184 pending: BTreeMap<u64, ExecProcessEvent>,
188 pending_bytes: usize,
189 failure: Option<String>,
190}
191
192#[derive(Clone)]
193pub(crate) struct Session {
194 client: ExecServerClient,
195 process_id: ProcessId,
196 state: Arc<SessionState>,
197}
198
199struct Inner {
200 connection: StdMutex<ConnectionState>,
201 connection_changed: watch::Sender<()>,
202 sessions: ArcSwap<HashMap<ProcessId, Arc<SessionState>>>,
207 sessions_write_lock: StdMutex<()>,
211 http_body_streams: ArcSwap<HashMap<String, mpsc::Sender<QueuedHttpBodyDelta>>>,
216 http_body_stream_failures: ArcSwap<HashMap<String, String>>,
217 http_body_streams_write_lock: Mutex<()>,
218 http_body_stream_byte_budget: Arc<Semaphore>,
219 http_body_stream_next_id: AtomicU64,
220 session_id: OnceLock<String>,
221 reconnect_strategy: Option<ExecServerReconnectStrategy>,
222}
223
224struct ConnectionState {
225 status: ConnectionStatus,
226 active_process_starts: usize,
227 environment_connection_state_tx: watch::Sender<EnvironmentConnectionState>,
228}
229
230enum ConnectionStatus {
231 Connected(Arc<RpcClient>),
232 Recovering,
233 Failed(String),
234}
235
236impl ConnectionState {
237 fn set_status(&mut self, status: ConnectionStatus) {
238 self.status = status;
239 self.publish_environment_connection_state();
240 }
241
242 fn publish_environment_connection_state(&self) {
243 let state = match &self.status {
244 ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
245 EnvironmentConnectionState::Connected
246 }
247 ConnectionStatus::Connected(_)
248 | ConnectionStatus::Recovering
249 | ConnectionStatus::Failed(_) => EnvironmentConnectionState::Disconnected,
250 };
251 let _ = self
252 .environment_connection_state_tx
253 .send_if_modified(|current| {
254 if *current == state {
255 false
256 } else {
257 *current = state;
258 true
259 }
260 });
261 }
262}
263
264#[derive(Clone, Copy)]
265enum RecoveryPolicy {
266 Wait,
267 FailFast,
268}
269
270#[derive(Clone)]
271pub struct ExecServerClient {
272 inner: Arc<Inner>,
273 recovery_policy: RecoveryPolicy,
274}
275
276struct ActiveProcessStart {
277 inner: Arc<Inner>,
278}
279
280impl Drop for ActiveProcessStart {
281 fn drop(&mut self) {
282 self.inner.finish_process_start();
283 }
284}
285
286type ConnectionResult = Result<ExecServerClient, Arc<ExecServerError>>;
287type ConnectionAttempt = OnceCell<ConnectionResult>;
288
289#[derive(Clone)]
290pub(crate) struct LazyRemoteExecServerClient {
291 transport_params: ExecServerTransportParams,
292 recovery_policy: RecoveryPolicy,
293 startup: Arc<ConnectionAttempt>,
295 current_client: Arc<StdMutex<Option<ExecServerClient>>>,
297 reconnect: Arc<StdMutex<Option<Arc<ConnectionAttempt>>>>,
298 environment_connection_state_tx: watch::Sender<EnvironmentConnectionState>,
299}
300
301impl LazyRemoteExecServerClient {
302 pub(crate) fn new(transport_params: ExecServerTransportParams) -> Self {
303 Self {
304 transport_params,
305 recovery_policy: RecoveryPolicy::Wait,
306 startup: Arc::new(ConnectionAttempt::new()),
307 current_client: Arc::new(StdMutex::new(None)),
308 reconnect: Arc::new(StdMutex::new(None)),
309 environment_connection_state_tx: watch::channel(
310 EnvironmentConnectionState::Disconnected,
311 )
312 .0,
313 }
314 }
315
316 pub(crate) fn subscribe_connection_state(&self) -> watch::Receiver<EnvironmentConnectionState> {
317 self.environment_connection_state_tx.subscribe()
318 }
319
320 pub(crate) fn start_connecting(&self) -> Option<AbortOnDropHandle<()>> {
321 if matches!(
323 self.transport_params,
324 ExecServerTransportParams::StdioCommand { .. }
325 ) {
326 return None;
327 }
328 let client = self.clone();
329 Some(AbortOnDropHandle::new(tokio::spawn(async move {
330 if let Err(error) = client.wait_until_ready().await {
331 debug!(%error, "exec-server environment startup failed");
332 }
333 })))
334 }
335
336 pub(crate) fn startup_finished(&self) -> bool {
337 self.startup.get().is_some()
338 }
339
340 pub(crate) fn readiness_result(&self) -> Option<Result<(), ExecServerError>> {
341 if let Some(client) = self.cached_client() {
342 return client.readiness_result();
343 }
344 self.startup.get().and_then(|result| match result {
345 Ok(client) => client.readiness_result(),
346 Err(error) => Some(Err(ExecServerError::ConnectionAttempt(Arc::clone(error)))),
347 })
348 }
349
350 pub(crate) async fn status(&self) -> crate::EnvironmentObservedStatus {
351 let client = match self.fail_fast().get().await {
353 Ok(client) => client,
354 Err(error) => {
355 if self.cached_client().is_none() && self.startup.get().is_none() {
357 return crate::EnvironmentObservedStatus::Pending;
358 }
359 return crate::EnvironmentObservedStatus::Disconnected {
361 error: error.to_string(),
362 };
363 }
364 };
365 match client.environment_status().await {
367 Ok(_) => crate::EnvironmentObservedStatus::Ready,
368 Err(error) => crate::EnvironmentObservedStatus::Disconnected {
369 error: error.to_string(),
370 },
371 }
372 }
373
374 pub(crate) fn fail_fast(&self) -> Self {
375 Self {
376 recovery_policy: RecoveryPolicy::FailFast,
377 ..self.clone()
378 }
379 }
380
381 pub(crate) async fn wait_until_ready(&self) -> Result<(), ExecServerError> {
382 self.initial_client().await.map(drop)
383 }
384
385 pub(crate) async fn get(&self) -> Result<ExecServerClient, ExecServerError> {
386 if matches!(self.recovery_policy, RecoveryPolicy::FailFast) {
387 let client = match self.cached_client() {
388 Some(client) => client,
389 None => match self.startup.get() {
390 Some(Ok(client)) => client.clone(),
391 Some(Err(error)) => {
392 return Err(ExecServerError::ConnectionAttempt(Arc::clone(error)));
393 }
394 None => {
395 return Err(ExecServerError::Disconnected(
396 "exec-server environment is not ready".to_string(),
397 ));
398 }
399 },
400 };
401 return client.fail_fast();
402 }
403 if let Some(client) = self.connected_client() {
404 return Ok(client);
405 }
406
407 let Some(cached_client) = self.cached_client() else {
408 let client = self.initial_client().await?;
409 if !client.is_disconnected() || !self.can_reconnect() {
410 return Ok(client);
411 }
412 return self.reconnect().await;
413 };
414
415 if !self.can_reconnect() {
416 return Ok(cached_client);
417 }
418
419 self.reconnect().await
420 }
421
422 async fn initial_client(&self) -> Result<ExecServerClient, ExecServerError> {
423 let result = self.startup.get_or_init(|| self.connect_once()).await;
425 match result {
426 Ok(client) => {
427 let mut current_client = self
428 .current_client
429 .lock()
430 .unwrap_or_else(std::sync::PoisonError::into_inner);
431 if current_client.is_none() {
432 *current_client = Some(client.clone());
433 }
434 Ok(client.clone())
435 }
436 Err(error) => Err(ExecServerError::ConnectionAttempt(Arc::clone(error))),
437 }
438 }
439
440 async fn reconnect(&self) -> Result<ExecServerClient, ExecServerError> {
441 let attempt = {
443 let mut reconnect = self
444 .reconnect
445 .lock()
446 .unwrap_or_else(std::sync::PoisonError::into_inner);
447 if let Some(client) = self.connected_client() {
448 return Ok(client);
449 }
450 reconnect
451 .get_or_insert_with(|| Arc::new(ConnectionAttempt::new()))
452 .clone()
453 };
454 let result = attempt
455 .get_or_init(|| async {
456 let result = self.connect_once().await;
457 if let Ok(client) = &result {
458 *self
459 .current_client
460 .lock()
461 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());
462 }
463 result
464 })
465 .await;
466 let mut reconnect = self
467 .reconnect
468 .lock()
469 .unwrap_or_else(std::sync::PoisonError::into_inner);
470 if reconnect
472 .as_ref()
473 .is_some_and(|current| Arc::ptr_eq(current, &attempt))
474 {
475 *reconnect = None;
476 }
477 result.clone().map_err(ExecServerError::ConnectionAttempt)
478 }
479
480 fn connected_client(&self) -> Option<ExecServerClient> {
481 self.cached_client()
482 .filter(|client| !client.is_disconnected())
483 }
484
485 fn cached_client(&self) -> Option<ExecServerClient> {
486 self.current_client
487 .lock()
488 .unwrap_or_else(std::sync::PoisonError::into_inner)
489 .clone()
490 }
491
492 fn can_reconnect(&self) -> bool {
493 matches!(
494 self.transport_params,
495 ExecServerTransportParams::Deferred(_)
496 | ExecServerTransportParams::WebSocketUrl { .. }
497 | ExecServerTransportParams::NoiseRendezvous { .. }
498 )
499 }
500
501 async fn connect_once(&self) -> ConnectionResult {
502 let result = ExecServerClient::connect_for_transport(self.transport_params.clone())
503 .await
504 .map_err(Arc::new);
505 if let Ok(client) = &result {
506 client
507 .attach_environment_connection_state(self.environment_connection_state_tx.clone());
508 }
509 result
510 }
511}
512
513impl HttpClient for LazyRemoteExecServerClient {
514 fn http_request(
515 &self,
516 params: crate::HttpRequestParams,
517 ) -> BoxFuture<'_, Result<crate::HttpRequestResponse, ExecServerError>> {
518 async move { self.get().await?.http_request(params).await }.boxed()
519 }
520
521 fn http_request_stream(
522 &self,
523 params: crate::HttpRequestParams,
524 ) -> BoxFuture<
525 '_,
526 Result<(crate::HttpRequestResponse, crate::HttpResponseBodyStream), ExecServerError>,
527 > {
528 async move { self.get().await?.http_request_stream(params).await }.boxed()
529 }
530}
531
532impl LazyRemoteExecServerClient {
533 pub(crate) async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError> {
534 self.get().await?.environment_info().await
535 }
536}
537
538#[derive(Debug, thiserror::Error)]
539pub enum ExecServerError {
540 #[error("failed to spawn exec-server: {0}")]
541 Spawn(#[source] std::io::Error),
542 #[error("timed out connecting to exec-server websocket `{url}` after {timeout:?}")]
543 WebSocketConnectTimeout { url: String, timeout: Duration },
544 #[error("failed to connect to exec-server websocket `{url}`: {source}")]
545 WebSocketConnect {
546 url: String,
547 #[source]
548 source: tokio_tungstenite::tungstenite::Error,
549 },
550 #[error("timed out waiting for exec-server initialize handshake after {timeout:?}")]
551 InitializeTimedOut { timeout: Duration },
552 #[error("exec-server transport closed")]
553 Closed,
554 #[error("{0}")]
555 Disconnected(String),
556 #[error("failed to serialize or deserialize exec-server JSON: {0}")]
557 Json(#[from] serde_json::Error),
558 #[error("HTTP request failed: {0}")]
559 HttpRequest(String),
560 #[error("exec-server protocol error: {0}")]
561 Protocol(String),
562 #[error("exec-server rejected request ({code}): {message}")]
563 Server { code: i64, message: String },
564 #[error("environment registry request failed ({status}{code_suffix}): {message}", code_suffix = .code.as_ref().map(|code| format!(", {code}")).unwrap_or_default())]
565 EnvironmentRegistryHttp {
566 status: reqwest::StatusCode,
567 code: Option<String>,
568 message: String,
569 },
570 #[error("environment registry configuration error: {0}")]
571 EnvironmentRegistryConfig(String),
572 #[error("environment registry authentication error: {0}")]
573 EnvironmentRegistryAuth(String),
574 #[error("environment registry request failed: {0}")]
575 EnvironmentRegistryRequest(#[from] reqwest::Error),
576 #[error("exec-server connection attempt failed: {0}")]
577 ConnectionAttempt(#[source] Arc<ExecServerError>),
578}
579
580impl ExecServerClient {
581 fn attach_environment_connection_state(
582 &self,
583 state_tx: watch::Sender<EnvironmentConnectionState>,
584 ) {
585 let mut connection = self
586 .inner
587 .connection
588 .lock()
589 .unwrap_or_else(std::sync::PoisonError::into_inner);
590 connection.environment_connection_state_tx = state_tx;
591 connection.publish_environment_connection_state();
592 }
593
594 fn fail_fast(&self) -> Result<Self, ExecServerError> {
595 self.rpc_client_without_recovery()?;
596 Ok(Self {
597 inner: Arc::clone(&self.inner),
598 recovery_policy: RecoveryPolicy::FailFast,
599 })
600 }
601
602 fn rpc_client_without_recovery(&self) -> Result<Arc<RpcClient>, ExecServerError> {
603 let connection = self
604 .inner
605 .connection
606 .lock()
607 .unwrap_or_else(std::sync::PoisonError::into_inner);
608 match &connection.status {
609 ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
610 Ok(Arc::clone(rpc_client))
611 }
612 ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => Err(
613 ExecServerError::Disconnected("exec-server environment is recovering".to_string()),
614 ),
615 ConnectionStatus::Failed(message) => {
616 Err(ExecServerError::Disconnected(message.clone()))
617 }
618 }
619 }
620
621 async fn rpc_client(&self) -> Result<Arc<RpcClient>, ExecServerError> {
622 match self.recovery_policy {
623 RecoveryPolicy::Wait => self.inner.rpc_client().await,
624 RecoveryPolicy::FailFast => self.rpc_client_without_recovery(),
625 }
626 }
627
628 async fn initialize_rpc(
629 &self,
630 rpc_client: &RpcClient,
631 options: ExecServerClientConnectOptions,
632 ) -> Result<InitializeResponse, ExecServerError> {
633 let ExecServerClientConnectOptions {
634 client_name,
635 initialize_timeout,
636 resume_session_id,
637 } = options;
638
639 timeout(initialize_timeout, async {
640 let response: InitializeResponse = rpc_client
641 .call(
642 INITIALIZE_METHOD,
643 &InitializeParams {
644 client_name,
645 resume_session_id,
646 },
647 )
648 .await?;
649 let session_id = self
650 .inner
651 .session_id
652 .get_or_init(|| response.session_id.clone());
653 if session_id != &response.session_id {
654 return Err(ExecServerError::Protocol(format!(
655 "exec-server initialized an unexpected session {}",
656 response.session_id
657 )));
658 }
659 rpc_client
660 .notify(INITIALIZED_METHOD, &serde_json::json!({}))
661 .await?;
662 Ok(response)
663 })
664 .await
665 .map_err(|_| ExecServerError::InitializeTimedOut {
666 timeout: initialize_timeout,
667 })?
668 }
669
670 pub async fn exec(&self, params: ExecParams) -> Result<ExecResponse, ExecServerError> {
671 self.call(EXEC_METHOD, ¶ms).await
672 }
673
674 pub async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError> {
675 let rpc_client = self.rpc_client().await?;
676 map_rpc_call_result(
677 rpc_client
678 .call_with_timeout(ENVIRONMENT_INFO_METHOD, &(), ENVIRONMENT_INFO_TIMEOUT)
679 .await,
680 )
681 }
682
683 pub async fn environment_status(&self) -> Result<EnvironmentStatus, ExecServerError> {
684 let rpc_client = self.rpc_client_without_recovery()?;
686 map_rpc_call_result(
687 rpc_client
688 .call_with_timeout(ENVIRONMENT_STATUS_METHOD, &(), ENVIRONMENT_STATUS_TIMEOUT)
689 .await,
690 )
691 }
692
693 pub async fn discover_capability_roots(
694 &self,
695 params: CapabilityRootsDiscoverParams,
696 ) -> Result<CapabilityRootsDiscoverResponse, ExecServerError> {
697 self.call(CAPABILITY_ROOTS_DISCOVER_METHOD, ¶ms).await
698 }
699
700 pub async fn read(&self, params: ReadParams) -> Result<ReadResponse, ExecServerError> {
701 self.call(EXEC_READ_METHOD, ¶ms).await
702 }
703
704 pub async fn write(
705 &self,
706 process_id: &ProcessId,
707 chunk: Vec<u8>,
708 write_id: String,
709 ) -> Result<WriteResponse, ExecServerError> {
710 self.call(
711 EXEC_WRITE_METHOD,
712 &WriteParams {
713 process_id: process_id.clone(),
714 chunk: chunk.into(),
715 write_id,
716 },
717 )
718 .await
719 }
720
721 pub async fn signal(
722 &self,
723 process_id: &ProcessId,
724 signal: ProcessSignal,
725 ) -> Result<(), ExecServerError> {
726 let _response: SignalResponse = self
727 .call(
728 EXEC_SIGNAL_METHOD,
729 &SignalParams {
730 process_id: process_id.clone(),
731 signal,
732 },
733 )
734 .await?;
735 Ok(())
736 }
737
738 pub async fn terminate(
739 &self,
740 process_id: &ProcessId,
741 ) -> Result<TerminateResponse, ExecServerError> {
742 self.call_for_cleanup(
743 EXEC_TERMINATE_METHOD,
744 &TerminateParams {
745 process_id: process_id.clone(),
746 },
747 )
748 .await
749 }
750
751 pub async fn fs_read_file(
752 &self,
753 params: FsReadFileParams,
754 ) -> Result<FsReadFileResponse, ExecServerError> {
755 self.call(FS_READ_FILE_METHOD, ¶ms).await
756 }
757
758 pub async fn fs_open(&self, params: FsOpenParams) -> Result<FsOpenResponse, ExecServerError> {
759 self.call(FS_OPEN_METHOD, ¶ms).await
760 }
761
762 pub async fn fs_read_block(
763 &self,
764 params: FsReadBlockParams,
765 ) -> Result<FsReadBlockResponse, ExecServerError> {
766 self.call(FS_READ_BLOCK_METHOD, ¶ms).await
767 }
768
769 pub async fn fs_close(
770 &self,
771 params: FsCloseParams,
772 ) -> Result<FsCloseResponse, ExecServerError> {
773 self.call_for_cleanup(FS_CLOSE_METHOD, ¶ms).await
774 }
775
776 pub async fn fs_write_file(
777 &self,
778 params: FsWriteFileParams,
779 ) -> Result<FsWriteFileResponse, ExecServerError> {
780 self.call(FS_WRITE_FILE_METHOD, ¶ms).await
781 }
782
783 pub async fn fs_create_directory(
784 &self,
785 params: FsCreateDirectoryParams,
786 ) -> Result<FsCreateDirectoryResponse, ExecServerError> {
787 self.call(FS_CREATE_DIRECTORY_METHOD, ¶ms).await
788 }
789
790 pub async fn fs_get_metadata(
791 &self,
792 params: FsGetMetadataParams,
793 ) -> Result<FsGetMetadataResponse, ExecServerError> {
794 self.call(FS_GET_METADATA_METHOD, ¶ms).await
795 }
796
797 pub async fn fs_canonicalize(
798 &self,
799 params: FsCanonicalizeParams,
800 ) -> Result<FsCanonicalizeResponse, ExecServerError> {
801 self.call(FS_CANONICALIZE_METHOD, ¶ms).await
802 }
803
804 pub async fn fs_read_directory(
805 &self,
806 params: FsReadDirectoryParams,
807 ) -> Result<FsReadDirectoryResponse, ExecServerError> {
808 self.call(FS_READ_DIRECTORY_METHOD, ¶ms).await
809 }
810
811 pub async fn fs_walk(&self, params: FsWalkParams) -> Result<FsWalkResponse, ExecServerError> {
812 self.call(FS_WALK_METHOD, ¶ms).await
813 }
814
815 pub async fn fs_remove(
816 &self,
817 params: FsRemoveParams,
818 ) -> Result<FsRemoveResponse, ExecServerError> {
819 self.call(FS_REMOVE_METHOD, ¶ms).await
820 }
821
822 pub async fn fs_copy(&self, params: FsCopyParams) -> Result<FsCopyResponse, ExecServerError> {
823 self.call(FS_COPY_METHOD, ¶ms).await
824 }
825
826 pub(crate) async fn start_process(
827 &self,
828 params: ExecParams,
829 ) -> Result<Session, ExecServerError> {
830 loop {
831 let rpc_client = self.rpc_client().await?;
832 if !self.inner.begin_process_start(&rpc_client) {
833 continue;
834 }
835
836 let process_id = params.process_id.clone();
837 let state = Arc::new(SessionState::new(false));
838 if let Err(error) = self.inner.insert_session(&process_id, Arc::clone(&state)) {
839 self.inner.finish_process_start();
840 return Err(error);
841 }
842 let active_start = ActiveProcessStart {
843 inner: Arc::clone(&self.inner),
844 };
845 let client = self.clone();
846 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
847 let process_start_task = async move {
848 let _active_start = active_start;
849 match client
850 .call_rpc::<_, ExecResponse>(&rpc_client, EXEC_METHOD, ¶ms)
851 .await
852 {
853 Ok(_) => {
854 state.recoverable.store(true, Ordering::Release);
855 let session = Session {
856 client: client.clone(),
857 process_id: process_id.clone(),
858 state: Arc::clone(&state),
859 };
860 if result_tx.send(Ok(session)).is_err() {
861 state.recoverable.store(false, Ordering::Release);
862 tokio::spawn(async move {
863 cleanup_process_start(&client, &process_id, &state).await;
864 });
865 }
866 }
867 Err(error) => {
868 if is_transport_closed_error(&error) {
869 tokio::spawn(async move {
870 cleanup_process_start(&client, &process_id, &state).await;
871 });
872 } else {
873 client.inner.remove_session_if(&process_id, &state);
874 }
875 let _ = result_tx.send(Err(error));
876 }
877 }
878 };
879 tokio::spawn(
880 process_start_task
881 .in_current_span()
882 .with_current_subscriber(),
883 );
884 return result_rx.await.map_err(|_| {
885 ExecServerError::Protocol("process start task stopped unexpectedly".to_string())
886 })?;
887 }
888 }
889
890 #[cfg(test)]
891 pub(crate) async fn register_session(
892 &self,
893 process_id: &ProcessId,
894 ) -> Result<Session, ExecServerError> {
895 let state = Arc::new(SessionState::new(true));
896 self.inner.insert_session(process_id, Arc::clone(&state))?;
897 Ok(Session {
898 client: self.clone(),
899 process_id: process_id.clone(),
900 state,
901 })
902 }
903
904 pub fn session_id(&self) -> Option<String> {
905 self.inner.session_id.get().cloned()
906 }
907
908 fn is_disconnected(&self) -> bool {
909 self.inner.is_failed()
910 }
911
912 fn readiness_result(&self) -> Option<Result<(), ExecServerError>> {
913 let connection = self
914 .inner
915 .connection
916 .lock()
917 .unwrap_or_else(std::sync::PoisonError::into_inner);
918 match &connection.status {
919 ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
920 Some(Ok(()))
921 }
922 ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => None,
923 ConnectionStatus::Failed(message) => {
924 Some(Err(ExecServerError::Disconnected(message.clone())))
925 }
926 }
927 }
928
929 pub(crate) async fn connect(
930 connection: JsonRpcConnection,
931 options: ExecServerClientConnectOptions,
932 ) -> Result<Self, ExecServerError> {
933 Self::connect_with_recovery(connection, options, None).await
934 }
935
936 pub(crate) async fn connect_with_recovery(
937 connection: JsonRpcConnection,
938 options: ExecServerClientConnectOptions,
939 reconnect_strategy: Option<ExecServerReconnectStrategy>,
940 ) -> Result<Self, ExecServerError> {
941 let (rpc_client, events_rx) = RpcClient::new(connection);
942 let rpc_client = Arc::new(rpc_client);
943 let session_id = OnceLock::new();
944 let (connection_changed, _connection_changed_rx) = watch::channel(());
945 let inner = Arc::new(Inner {
946 connection: StdMutex::new(ConnectionState {
947 status: ConnectionStatus::Connected(Arc::clone(&rpc_client)),
948 active_process_starts: 0,
949 environment_connection_state_tx: watch::channel(
950 EnvironmentConnectionState::Connected,
951 )
952 .0,
953 }),
954 connection_changed,
955 sessions: ArcSwap::from_pointee(HashMap::new()),
956 sessions_write_lock: StdMutex::new(()),
957 http_body_streams: ArcSwap::from_pointee(HashMap::new()),
958 http_body_stream_failures: ArcSwap::from_pointee(HashMap::new()),
959 http_body_streams_write_lock: Mutex::new(()),
960 http_body_stream_byte_budget: Arc::new(Semaphore::new(MAX_QUEUED_HTTP_BODY_BYTES)),
961 http_body_stream_next_id: AtomicU64::new(1),
962 session_id,
963 reconnect_strategy,
964 });
965 let client = Self {
966 inner,
967 recovery_policy: RecoveryPolicy::Wait,
968 };
969 client.spawn_rpc_reader(&rpc_client, events_rx);
973 client.initialize_rpc(&rpc_client, options).await?;
974 Ok(client)
975 }
976
977 async fn call<P, T>(&self, method: &str, params: &P) -> Result<T, ExecServerError>
978 where
979 P: serde::Serialize,
980 T: serde::de::DeserializeOwned,
981 {
982 let rpc_client = self.rpc_client().await?;
983 self.call_rpc(&rpc_client, method, params).await
984 }
985
986 async fn call_rpc<P, T>(
987 &self,
988 rpc_client: &Arc<RpcClient>,
989 method: &str,
990 params: &P,
991 ) -> Result<T, ExecServerError>
992 where
993 P: serde::Serialize,
994 T: serde::de::DeserializeOwned,
995 {
996 map_rpc_call_result(rpc_client.call(method, params).await)
997 }
998
999 async fn call_for_cleanup<P, T>(&self, method: &str, params: &P) -> Result<T, ExecServerError>
1000 where
1001 P: serde::Serialize,
1002 T: serde::de::DeserializeOwned,
1003 {
1004 let rpc_client = self.inner.rpc_client().await?;
1005 map_rpc_call_result(rpc_client.call_for_cleanup(method, params).await)
1006 }
1007}
1008
1009fn map_rpc_call_result<T>(result: Result<T, RpcCallError>) -> Result<T, ExecServerError> {
1010 result.map_err(|error| {
1011 let error = ExecServerError::from(error);
1012 if is_transport_closed_error(&error) {
1013 ExecServerError::Disconnected(disconnected_message(None))
1014 } else {
1015 error
1016 }
1017 })
1018}
1019
1020async fn cleanup_process_start(
1021 client: &ExecServerClient,
1022 process_id: &ProcessId,
1023 state: &Arc<SessionState>,
1024) {
1025 loop {
1026 match client.terminate(process_id).await {
1027 Ok(_) => break,
1028 Err(error) if is_transport_closed_error(&error) && !client.inner.is_failed() => {
1029 continue;
1030 }
1031 Err(_) => break,
1032 }
1033 }
1034 client.inner.remove_session_if(process_id, state);
1035}
1036
1037impl From<RpcCallError> for ExecServerError {
1038 fn from(value: RpcCallError) -> Self {
1039 match value {
1040 RpcCallError::Closed => Self::Closed,
1041 RpcCallError::Json(err) => Self::Json(err),
1042 RpcCallError::Server(error) => Self::Server {
1043 code: error.code,
1044 message: error.message,
1045 },
1046 RpcCallError::TimedOut { method, timeout } => Self::Protocol(format!(
1047 "timed out waiting for exec-server `{method}` response after {timeout:?}"
1048 )),
1049 RpcCallError::PendingRequestLimitExceeded { limit } => Self::Protocol(format!(
1050 "exec-server has reached its limit of {limit} pending requests"
1051 )),
1052 }
1053 }
1054}
1055
1056impl SessionState {
1057 fn new(recoverable: bool) -> Self {
1058 let (wake_tx, _wake_rx) = watch::channel(0);
1059 Self {
1060 wake_tx,
1061 events: ExecProcessEventLog::new(
1062 PROCESS_EVENT_CHANNEL_CAPACITY,
1063 PROCESS_EVENT_RETAINED_BYTES,
1064 ),
1065 ordered_events: StdMutex::new(OrderedSessionEvents::default()),
1066 recoverable: AtomicBool::new(recoverable),
1067 next_write_id: AtomicU64::new(1),
1068 }
1069 }
1070
1071 pub(crate) fn subscribe(&self) -> watch::Receiver<u64> {
1072 self.wake_tx.subscribe()
1073 }
1074
1075 pub(crate) fn subscribe_events(&self) -> ExecProcessEventReceiver {
1076 self.events.subscribe()
1077 }
1078
1079 fn note_change(&self, seq: u64) {
1080 self.wake_tx
1081 .send_modify(|current| *current = (*current).max(seq));
1082 }
1083
1084 fn publish_ordered_event(&self, event: ExecProcessEvent) -> Result<bool, String> {
1092 let Some(seq) = event.seq() else {
1093 self.events.publish(event);
1094 return Ok(false);
1095 };
1096
1097 let mut ordered_events = self
1098 .ordered_events
1099 .lock()
1100 .unwrap_or_else(std::sync::PoisonError::into_inner);
1101 if ordered_events.failure.is_some()
1104 || ordered_events.closed_published
1105 || seq <= ordered_events.last_published_seq
1106 {
1107 return Ok(false);
1108 }
1109
1110 ordered_events.insert_pending(event)?;
1111 Ok(self.publish_ready(&mut ordered_events))
1112 }
1113
1114 fn publish_ready(&self, ordered_events: &mut OrderedSessionEvents) -> bool {
1115 let mut published_closed = false;
1116 loop {
1117 let next_seq = ordered_events.last_published_seq.saturating_add(1);
1118 let Some(event) = ordered_events.pending.remove(&next_seq) else {
1119 break;
1120 };
1121 ordered_events.pending_bytes = ordered_events
1122 .pending_bytes
1123 .saturating_sub(pending_process_event_bytes(&event));
1124 ordered_events.last_published_seq = next_seq;
1125 ordered_events.exit_published |= matches!(&event, ExecProcessEvent::Exited { .. });
1126 let is_closed = matches!(&event, ExecProcessEvent::Closed { .. });
1127 ordered_events.closed_published |= is_closed;
1128 published_closed |= is_closed;
1129 self.events.publish(event);
1130 }
1131 published_closed
1132 }
1133
1134 fn set_failure(&self, message: String) {
1135 let mut ordered_events = self
1136 .ordered_events
1137 .lock()
1138 .unwrap_or_else(std::sync::PoisonError::into_inner);
1139 if ordered_events.failure.is_some() || ordered_events.closed_published {
1140 return;
1141 }
1142 ordered_events.failure = Some(message.clone());
1143 ordered_events.pending.clear();
1144 ordered_events.pending_bytes = 0;
1145 self.events.publish(ExecProcessEvent::Failed(message));
1146 drop(ordered_events);
1147 self.wake_tx
1148 .send_modify(|current| *current = current.saturating_add(1));
1149 }
1150
1151 fn failed_response(&self) -> Option<ReadResponse> {
1152 self.ordered_events
1153 .lock()
1154 .unwrap_or_else(std::sync::PoisonError::into_inner)
1155 .failure
1156 .clone()
1157 .map(|message| self.synthesized_failure(message))
1158 }
1159
1160 fn synthesized_failure(&self, message: String) -> ReadResponse {
1161 let next_seq = (*self.wake_tx.borrow()).saturating_add(1);
1162 ReadResponse {
1163 chunks: Vec::new(),
1164 next_seq,
1165 exited: true,
1166 exit_code: None,
1167 closed: true,
1168 failure: Some(message),
1169 sandbox_denied: false,
1170 }
1171 }
1172
1173 fn next_write_id(&self) -> String {
1174 self.next_write_id
1175 .fetch_add(1, Ordering::Relaxed)
1176 .to_string()
1177 }
1178}
1179
1180impl OrderedSessionEvents {
1181 fn insert_pending(&mut self, event: ExecProcessEvent) -> Result<(), String> {
1182 let Some(seq) = event.seq() else {
1183 return Err("cannot reorder an unsequenced process event".to_string());
1184 };
1185 if self.pending.contains_key(&seq) {
1186 return Ok(());
1187 }
1188
1189 let next_seq = self.last_published_seq.saturating_add(1);
1190 let closes_gap = seq == next_seq;
1193 if !closes_gap && self.pending.len() >= MAX_PENDING_PROCESS_EVENTS {
1194 return Err(format!(
1195 "process event reorder buffer exceeds {MAX_PENDING_PROCESS_EVENTS} entries"
1196 ));
1197 }
1198
1199 let event_bytes = pending_process_event_bytes(&event);
1200 if event_bytes > MAX_PENDING_PROCESS_EVENT_BYTES {
1201 return Err(format!(
1202 "process event exceeds {MAX_PENDING_PROCESS_EVENT_BYTES} bytes"
1203 ));
1204 }
1205 let pending_bytes = self.pending_bytes.saturating_add(event_bytes);
1206 if !closes_gap && pending_bytes > MAX_PENDING_PROCESS_EVENT_BYTES {
1207 return Err(format!(
1208 "process event reorder buffer exceeds {MAX_PENDING_PROCESS_EVENT_BYTES} bytes"
1209 ));
1210 }
1211
1212 self.pending.insert(seq, event);
1213 self.pending_bytes = pending_bytes;
1214 Ok(())
1215 }
1216}
1217
1218fn pending_process_event_bytes(event: &ExecProcessEvent) -> usize {
1219 match event {
1220 ExecProcessEvent::Output(chunk) => chunk.chunk.0.len(),
1221 ExecProcessEvent::Failed(message) => message.len(),
1222 ExecProcessEvent::Exited { .. } | ExecProcessEvent::Closed { .. } => 0,
1223 }
1224}
1225
1226fn finish_process_event(
1227 inner: &Inner,
1228 process_id: &ProcessId,
1229 session: &Arc<SessionState>,
1230 result: Result<bool, String>,
1231) {
1232 match result {
1233 Ok(true) => inner.remove_session_if(process_id, session),
1234 Ok(false) => {}
1235 Err(message) => {
1236 session.set_failure(message);
1237 inner.remove_session_if(process_id, session);
1238 }
1239 }
1240}
1241
1242impl Session {
1243 pub(crate) fn process_id(&self) -> &ProcessId {
1244 &self.process_id
1245 }
1246
1247 pub(crate) fn subscribe_wake(&self) -> watch::Receiver<u64> {
1248 self.state.subscribe()
1249 }
1250
1251 pub(crate) fn subscribe_events(&self) -> ExecProcessEventReceiver {
1252 self.state.subscribe_events()
1253 }
1254
1255 pub(crate) async fn read(
1256 &self,
1257 after_seq: Option<u64>,
1258 max_bytes: Option<usize>,
1259 wait_ms: Option<u64>,
1260 ) -> Result<ReadResponse, ExecServerError> {
1261 loop {
1262 if let Some(response) = self.state.failed_response() {
1263 return Ok(response);
1264 }
1265
1266 match self
1267 .client
1268 .read(ReadParams {
1269 process_id: self.process_id.clone(),
1270 after_seq,
1271 max_bytes,
1272 wait_ms,
1273 })
1274 .await
1275 {
1276 Ok(response) => return Ok(response),
1277 Err(error)
1278 if is_transport_closed_error(&error) && !self.client.inner.is_failed() =>
1279 {
1280 continue;
1281 }
1282 Err(error) if is_transport_closed_error(&error) => {
1283 if let Some(response) = self.state.failed_response() {
1284 return Ok(response);
1285 }
1286 let message = error.to_string();
1287 self.state.set_failure(message.clone());
1288 return Ok(self.state.synthesized_failure(message));
1289 }
1290 Err(error) => return Err(error),
1291 }
1292 }
1293 }
1294
1295 pub(crate) async fn write(&self, chunk: Vec<u8>) -> Result<WriteResponse, ExecServerError> {
1296 let write_id = self.state.next_write_id();
1297 loop {
1298 match self
1299 .client
1300 .write(&self.process_id, chunk.clone(), write_id.clone())
1301 .await
1302 {
1303 Ok(response) => return Ok(response),
1304 Err(error)
1305 if is_transport_closed_error(&error) && !self.client.inner.is_failed() =>
1306 {
1307 continue;
1308 }
1309 Err(error) => return Err(error),
1310 }
1311 }
1312 }
1313
1314 pub(crate) async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError> {
1315 self.client.signal(&self.process_id, signal).await
1316 }
1317
1318 pub(crate) async fn terminate(&self) -> Result<(), ExecServerError> {
1319 self.client.terminate(&self.process_id).await?;
1320 Ok(())
1321 }
1322
1323 pub(crate) async fn unregister(&self) {
1324 self.client
1325 .inner
1326 .remove_session_if(&self.process_id, &self.state);
1327 }
1328}
1329
1330impl Inner {
1331 fn get_session(&self, process_id: &ProcessId) -> Option<Arc<SessionState>> {
1332 self.sessions.load().get(process_id).cloned()
1333 }
1334
1335 fn insert_session(
1336 &self,
1337 process_id: &ProcessId,
1338 session: Arc<SessionState>,
1339 ) -> Result<(), ExecServerError> {
1340 let _sessions_write_guard = self
1341 .sessions_write_lock
1342 .lock()
1343 .unwrap_or_else(std::sync::PoisonError::into_inner);
1344 if let Some(message) = self.failure_message() {
1348 return Err(ExecServerError::Disconnected(message));
1349 }
1350 let sessions = self.sessions.load();
1351 if sessions.contains_key(process_id) {
1352 return Err(ExecServerError::Protocol(format!(
1353 "session already registered for process {process_id}"
1354 )));
1355 }
1356 let mut next_sessions = sessions.as_ref().clone();
1357 next_sessions.insert(process_id.clone(), session);
1358 self.sessions.store(Arc::new(next_sessions));
1359 Ok(())
1360 }
1361
1362 fn remove_session_if(&self, process_id: &ProcessId, expected: &Arc<SessionState>) {
1363 let _sessions_write_guard = self
1364 .sessions_write_lock
1365 .lock()
1366 .unwrap_or_else(std::sync::PoisonError::into_inner);
1367 let sessions = self.sessions.load();
1368 if !sessions
1369 .get(process_id)
1370 .is_some_and(|session| Arc::ptr_eq(session, expected))
1371 {
1372 return;
1373 }
1374 let mut next_sessions = sessions.as_ref().clone();
1375 next_sessions.remove(process_id);
1376 self.sessions.store(Arc::new(next_sessions));
1377 }
1378
1379 fn take_all_sessions(&self) -> HashMap<ProcessId, Arc<SessionState>> {
1380 let _sessions_write_guard = self
1381 .sessions_write_lock
1382 .lock()
1383 .unwrap_or_else(std::sync::PoisonError::into_inner);
1384 let sessions = self.sessions.load();
1385 let drained_sessions = sessions.as_ref().clone();
1386 self.sessions.store(Arc::new(HashMap::new()));
1387 drained_sessions
1388 }
1389}
1390
1391fn disconnected_message(reason: Option<&str>) -> String {
1392 match reason {
1393 Some(reason) => format!("exec-server transport disconnected: {reason}"),
1394 None => "exec-server transport disconnected".to_string(),
1395 }
1396}
1397
1398fn is_transport_closed_error(error: &ExecServerError) -> bool {
1399 matches!(
1400 error,
1401 ExecServerError::Closed | ExecServerError::Disconnected(_)
1402 ) || matches!(
1403 error,
1404 ExecServerError::Server {
1405 code: -32000,
1406 message,
1407 } if message == "JSON-RPC transport closed"
1408 )
1409}
1410
1411fn fail_all_sessions(inner: &Arc<Inner>, message: String) {
1412 let sessions = inner.take_all_sessions();
1413
1414 for (_, session) in sessions {
1415 session.set_failure(message.clone());
1419 }
1420}
1421
1422async fn fail_all_in_flight_work(inner: &Arc<Inner>, message: String) {
1424 fail_all_sessions(inner, message.clone());
1425 inner.fail_all_http_body_streams(message).await;
1426}
1427
1428async fn handle_server_notification(
1429 inner: &Arc<Inner>,
1430 notification: JSONRPCNotification,
1431) -> Result<(), ExecServerError> {
1432 match notification.method.as_str() {
1433 EXEC_OUTPUT_DELTA_METHOD => {
1434 let params: ExecOutputDeltaNotification =
1435 serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1436 if let Some(session) = inner.get_session(¶ms.process_id) {
1437 let result =
1438 session.publish_ordered_event(ExecProcessEvent::Output(ProcessOutputChunk {
1439 seq: params.seq,
1440 stream: params.stream,
1441 chunk: params.chunk,
1442 }));
1443 if result.is_ok() {
1444 session.note_change(params.seq);
1445 }
1446 finish_process_event(inner, ¶ms.process_id, &session, result);
1447 }
1448 }
1449 EXEC_EXITED_METHOD => {
1450 let params: ExecExitedNotification =
1451 serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1452 if let Some(session) = inner.get_session(¶ms.process_id) {
1453 let result = session.publish_ordered_event(ExecProcessEvent::Exited {
1454 seq: params.seq,
1455 exit_code: params.exit_code,
1456 sandbox_denied: params.sandbox_denied,
1457 });
1458 if result.is_ok() {
1459 session.note_change(params.seq);
1460 }
1461 finish_process_event(inner, ¶ms.process_id, &session, result);
1462 }
1463 }
1464 EXEC_CLOSED_METHOD => {
1465 let params: ExecClosedNotification =
1466 serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1467 if let Some(session) = inner.get_session(¶ms.process_id) {
1468 let result =
1472 session.publish_ordered_event(ExecProcessEvent::Closed { seq: params.seq });
1473 if result.is_ok() {
1474 session.note_change(params.seq);
1475 }
1476 finish_process_event(inner, ¶ms.process_id, &session, result);
1477 }
1478 }
1479 HTTP_REQUEST_BODY_DELTA_METHOD => {
1480 inner
1481 .handle_http_body_delta_notification(notification.params)
1482 .await?;
1483 }
1484 other => {
1485 debug!("ignoring unknown exec-server notification: {other}");
1486 }
1487 }
1488 Ok(())
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493 use codex_exec_server_protocol::JSONRPCMessage;
1494 use codex_exec_server_protocol::JSONRPCNotification;
1495 use codex_exec_server_protocol::JSONRPCResponse;
1496 use codex_utils_path_uri::PathUri;
1497 use futures::SinkExt;
1498 use futures::StreamExt;
1499 use opentelemetry::trace::TracerProvider as _;
1500 use opentelemetry_sdk::trace::SdkTracerProvider;
1501 use pretty_assertions::assert_eq;
1502 use std::collections::HashMap;
1503 #[cfg(unix)]
1504 use std::path::Path;
1505 #[cfg(unix)]
1506 use std::process::Command;
1507 use std::sync::Arc;
1508 use tokio::io::AsyncBufReadExt;
1509 use tokio::io::AsyncWrite;
1510 use tokio::io::AsyncWriteExt;
1511 use tokio::io::BufReader;
1512 use tokio::io::duplex;
1513 use tokio::net::TcpListener;
1514 use tokio::net::TcpStream;
1515 use tokio::sync::mpsc;
1516 use tokio::sync::oneshot;
1517 use tokio::sync::watch;
1518 use tokio::time::Duration;
1519 #[cfg(unix)]
1520 use tokio::time::sleep;
1521 use tokio::time::timeout;
1522 use tokio_tungstenite::WebSocketStream;
1523 use tokio_tungstenite::accept_async;
1524 use tokio_tungstenite::tungstenite::Message;
1525 use tracing::Instrument;
1526 use tracing_subscriber::filter::filter_fn;
1527 use tracing_subscriber::prelude::*;
1528
1529 use super::ExecServerClient;
1530 use super::ExecServerClientConnectOptions;
1531 use super::LazyRemoteExecServerClient;
1532 use crate::EnvironmentObservedStatus;
1533 use crate::ProcessId;
1534 #[cfg(not(windows))]
1535 use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT;
1536 use crate::client_api::ExecServerTransportParams;
1537 use crate::client_api::RemoteExecServerConnectArgs;
1538 use crate::client_api::StdioExecServerCommand;
1539 use crate::client_api::StdioExecServerConnectArgs;
1540 use crate::connection::JsonRpcConnection;
1541 use crate::process::ExecProcessEvent;
1542 use crate::protocol::EXEC_CLOSED_METHOD;
1543 use crate::protocol::EXEC_EXITED_METHOD;
1544 use crate::protocol::EXEC_METHOD;
1545 use crate::protocol::EXEC_OUTPUT_DELTA_METHOD;
1546 use crate::protocol::EXEC_READ_METHOD;
1547 use crate::protocol::EXEC_WRITE_METHOD;
1548 use crate::protocol::ExecClosedNotification;
1549 use crate::protocol::ExecExitedNotification;
1550 use crate::protocol::ExecOutputDeltaNotification;
1551 use crate::protocol::ExecOutputStream;
1552 use crate::protocol::ExecParams;
1553 use crate::protocol::ExecResponse;
1554 use crate::protocol::INITIALIZE_METHOD;
1555 use crate::protocol::INITIALIZED_METHOD;
1556 use crate::protocol::InitializeResponse;
1557 use crate::protocol::ProcessOutputChunk;
1558 use crate::protocol::ReadResponse;
1559 use crate::protocol::WriteParams;
1560 use crate::protocol::WriteResponse;
1561 use crate::protocol::WriteStatus;
1562
1563 async fn read_jsonrpc_line<R>(lines: &mut tokio::io::Lines<BufReader<R>>) -> JSONRPCMessage
1564 where
1565 R: tokio::io::AsyncRead + Unpin,
1566 {
1567 let line = timeout(Duration::from_secs(1), lines.next_line())
1568 .await
1569 .expect("json-rpc read should not time out")
1570 .expect("json-rpc read should succeed")
1571 .expect("json-rpc connection should stay open");
1572 serde_json::from_str(&line).expect("json-rpc line should parse")
1573 }
1574
1575 async fn write_jsonrpc_line<W>(writer: &mut W, message: JSONRPCMessage)
1576 where
1577 W: AsyncWrite + Unpin,
1578 {
1579 let encoded = serde_json::to_string(&message).expect("json-rpc message should serialize");
1580 writer
1581 .write_all(format!("{encoded}\n").as_bytes())
1582 .await
1583 .expect("json-rpc line should write");
1584 }
1585
1586 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1587 async fn process_start_propagates_caller_trace_context_across_background_task() {
1588 let (client_stdin, server_reader) = duplex(1 << 20);
1589 let (mut server_writer, client_stdout) = duplex(1 << 20);
1590 let server = tokio::spawn(async move {
1591 let mut lines = BufReader::new(server_reader).lines();
1592 let initialize = read_jsonrpc_line(&mut lines).await;
1593 let initialize = match initialize {
1594 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1595 other => panic!("expected initialize request, got {other:?}"),
1596 };
1597 write_jsonrpc_line(
1598 &mut server_writer,
1599 JSONRPCMessage::Response(JSONRPCResponse {
1600 id: initialize.id,
1601 result: serde_json::to_value(InitializeResponse {
1602 session_id: "trace-test".to_string(),
1603 })
1604 .expect("initialize response should serialize"),
1605 }),
1606 )
1607 .await;
1608
1609 match read_jsonrpc_line(&mut lines).await {
1610 JSONRPCMessage::Notification(notification)
1611 if notification.method == INITIALIZED_METHOD => {}
1612 other => panic!("expected initialized notification, got {other:?}"),
1613 }
1614
1615 let request = match read_jsonrpc_line(&mut lines).await {
1616 JSONRPCMessage::Request(request) if request.method == EXEC_METHOD => request,
1617 other => panic!("expected process start request, got {other:?}"),
1618 };
1619 let trace = request.trace.clone();
1620 let params: ExecParams =
1621 serde_json::from_value(request.params.expect("process start params should exist"))
1622 .expect("process start params should deserialize");
1623 write_jsonrpc_line(
1624 &mut server_writer,
1625 JSONRPCMessage::Response(JSONRPCResponse {
1626 id: request.id,
1627 result: serde_json::to_value(ExecResponse {
1628 process_id: params.process_id,
1629 })
1630 .expect("process start response should serialize"),
1631 }),
1632 )
1633 .await;
1634 trace
1635 });
1636
1637 let client = ExecServerClient::connect(
1638 JsonRpcConnection::from_stdio(
1639 client_stdout,
1640 client_stdin,
1641 "trace-test-client".to_string(),
1642 ),
1643 ExecServerClientConnectOptions::default(),
1644 )
1645 .await
1646 .expect("client should connect");
1647
1648 let tracer_provider = SdkTracerProvider::builder().build();
1649 let tracer = tracer_provider.tracer("exec-server-test");
1650 let subscriber = tracing_subscriber::registry().with(
1651 tracing_opentelemetry::layer()
1652 .with_tracer(tracer)
1653 .with_filter(filter_fn(codex_otel::OtelProvider::trace_export_filter)),
1654 );
1655 let _subscriber_guard = tracing::subscriber::set_default(subscriber);
1656 tracing::callsite::rebuild_interest_cache();
1657 let parent_span = tracing::info_span!("process-start-parent");
1658 let expected_trace = codex_otel::span_w3c_trace_context(&parent_span)
1659 .expect("parent span should have trace context");
1660 let process_id = ProcessId::from("trace-process");
1661
1662 let session = client
1663 .start_process(ExecParams {
1664 process_id: process_id.clone(),
1665 argv: vec!["true".to_string()],
1666 cwd: PathUri::from_host_native_path(std::env::current_dir().expect("cwd"))
1667 .expect("cwd URI"),
1668 env_policy: None,
1669 env: HashMap::new(),
1670 tty: false,
1671 pipe_stdin: false,
1672 arg0: None,
1673 sandbox: None,
1674 enforce_managed_network: false,
1675 managed_network: None,
1676 network_proxy: None,
1677 })
1678 .instrument(parent_span)
1679 .await
1680 .expect("process start should succeed");
1681
1682 assert_eq!(session.process_id(), &process_id);
1683 let trace = server.await.expect("server task").expect("trace context");
1684 let expected_traceparent = expected_trace
1685 .traceparent
1686 .as_deref()
1687 .expect("parent traceparent");
1688 let traceparent = trace.traceparent.as_deref().expect("request traceparent");
1689 let expected_parts = expected_traceparent.split('-').collect::<Vec<_>>();
1690 let parts = traceparent.split('-').collect::<Vec<_>>();
1691 assert_eq!(parts[1], expected_parts[1]);
1692 assert_ne!(parts[2], expected_parts[2]);
1693 assert_eq!(trace.tracestate, expected_trace.tracestate);
1694 }
1695
1696 async fn accept_websocket(listener: &TcpListener) -> WebSocketStream<TcpStream> {
1697 let (stream, _) = listener.accept().await.expect("listener should accept");
1698 accept_async(stream)
1699 .await
1700 .expect("websocket handshake should succeed")
1701 }
1702
1703 async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage {
1704 loop {
1705 match timeout(Duration::from_secs(1), websocket.next())
1706 .await
1707 .expect("json-rpc websocket read should not time out")
1708 .expect("websocket should stay open")
1709 .expect("websocket frame should read")
1710 {
1711 Message::Text(text) => {
1712 return serde_json::from_str(text.as_ref())
1713 .expect("json-rpc text frame should parse");
1714 }
1715 Message::Binary(bytes) => {
1716 return serde_json::from_slice(bytes.as_ref())
1717 .expect("json-rpc binary frame should parse");
1718 }
1719 Message::Ping(_) | Message::Pong(_) => {}
1720 other => panic!("expected json-rpc websocket frame, got {other:?}"),
1721 }
1722 }
1723 }
1724
1725 async fn write_jsonrpc_websocket(
1726 websocket: &mut WebSocketStream<TcpStream>,
1727 message: JSONRPCMessage,
1728 ) {
1729 let encoded = serde_json::to_string(&message).expect("json-rpc should serialize");
1730 websocket
1731 .send(Message::Text(encoded.into()))
1732 .await
1733 .expect("json-rpc websocket frame should write");
1734 }
1735
1736 async fn complete_websocket_initialize(
1737 websocket: &mut WebSocketStream<TcpStream>,
1738 session_id: &str,
1739 expected_resume_session_id: Option<&str>,
1740 ) {
1741 let initialize = read_jsonrpc_websocket(websocket).await;
1742 let request = match initialize {
1743 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1744 other => panic!("expected initialize request, got {other:?}"),
1745 };
1746 let params: crate::protocol::InitializeParams =
1747 serde_json::from_value(request.params.expect("initialize params should exist"))
1748 .expect("initialize params should deserialize");
1749 assert_eq!(
1750 params.resume_session_id.as_deref(),
1751 expected_resume_session_id
1752 );
1753 write_jsonrpc_websocket(
1754 websocket,
1755 JSONRPCMessage::Response(JSONRPCResponse {
1756 id: request.id,
1757 result: serde_json::to_value(InitializeResponse {
1758 session_id: session_id.to_string(),
1759 })
1760 .expect("initialize response should serialize"),
1761 }),
1762 )
1763 .await;
1764
1765 let initialized = read_jsonrpc_websocket(websocket).await;
1766 match initialized {
1767 JSONRPCMessage::Notification(notification)
1768 if notification.method == INITIALIZED_METHOD => {}
1769 other => panic!("expected initialized notification, got {other:?}"),
1770 }
1771 }
1772
1773 #[cfg(not(windows))]
1774 #[tokio::test]
1775 async fn connect_stdio_command_initializes_json_rpc_client() {
1776 let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1777 command: StdioExecServerCommand {
1778 program: "sh".to_string(),
1779 args: vec![
1780 "-c".to_string(),
1781 "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'; read _line; sleep 60".to_string(),
1782 ],
1783 env: HashMap::new(),
1784 cwd: None,
1785 },
1786 client_name: "stdio-test-client".to_string(),
1787 initialize_timeout: Duration::from_secs(1),
1788 resume_session_id: None,
1789 })
1790 .await
1791 .expect("stdio client should connect");
1792
1793 assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1794 }
1795
1796 #[cfg(not(windows))]
1797 #[tokio::test]
1798 async fn connect_for_transport_initializes_stdio_command() {
1799 let client = ExecServerClient::connect_for_transport(
1800 ExecServerTransportParams::StdioCommand {
1801 command: StdioExecServerCommand {
1802 program: "sh".to_string(),
1803 args: vec![
1804 "-c".to_string(),
1805 "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'; read _line; sleep 60".to_string(),
1806 ],
1807 env: HashMap::new(),
1808 cwd: None,
1809 },
1810 initialize_timeout: DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT,
1811 },
1812 )
1813 .await
1814 .expect("stdio transport should connect");
1815
1816 assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1817 }
1818
1819 #[cfg(windows)]
1820 #[tokio::test]
1821 async fn connect_stdio_command_initializes_json_rpc_client_on_windows() {
1822 let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1823 command: StdioExecServerCommand {
1824 program: "powershell".to_string(),
1825 args: vec![
1826 "-NoProfile".to_string(),
1827 "-Command".to_string(),
1828 "$null = [Console]::In.ReadLine(); [Console]::Out.WriteLine('{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'); $null = [Console]::In.ReadLine(); Start-Sleep -Seconds 60".to_string(),
1829 ],
1830 env: HashMap::new(),
1831 cwd: None,
1832 },
1833 client_name: "stdio-test-client".to_string(),
1834 initialize_timeout: Duration::from_secs(1),
1835 resume_session_id: None,
1836 })
1837 .await
1838 .expect("stdio client should connect");
1839
1840 assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1841 }
1842
1843 #[cfg(unix)]
1844 #[tokio::test]
1845 async fn dropping_stdio_client_terminates_spawned_process() {
1846 let tempdir = tempfile::tempdir().expect("tempdir should be created");
1847 let pid_file = tempdir.path().join("server.pid");
1848 let child_pid_file = tempdir.path().join("server-child.pid");
1849 let stdio_script = format!(
1850 "read _line; \
1851 echo \"$$\" > {}; \
1852 sleep 60 >/dev/null 2>&1 & echo \"$!\" > {}; \
1853 printf '%s\\n' '{{\"id\":1,\"result\":{{\"sessionId\":\"stdio-test\"}}}}'; \
1854 read _line; \
1855 wait",
1856 shell_quote(pid_file.as_path()),
1857 shell_quote(child_pid_file.as_path()),
1858 );
1859
1860 let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1861 command: StdioExecServerCommand {
1862 program: "sh".to_string(),
1863 args: vec!["-c".to_string(), stdio_script],
1864 env: HashMap::new(),
1865 cwd: None,
1866 },
1867 client_name: "stdio-test-client".to_string(),
1868 initialize_timeout: Duration::from_secs(1),
1869 resume_session_id: None,
1870 })
1871 .await
1872 .expect("stdio client should connect");
1873 let server_pid = read_pid_file(pid_file.as_path()).await;
1874 let child_pid = read_pid_file(child_pid_file.as_path()).await;
1875 assert!(
1876 process_exists(server_pid),
1877 "spawned stdio process should be running before client drop"
1878 );
1879 assert!(
1880 process_exists(child_pid),
1881 "spawned stdio child process should be running before client drop"
1882 );
1883
1884 drop(client);
1885
1886 wait_for_process_exit(server_pid).await;
1887 wait_for_process_exit(child_pid).await;
1888 }
1889
1890 #[cfg(unix)]
1891 #[tokio::test]
1892 async fn malformed_stdio_message_terminates_spawned_process() {
1893 let tempdir = tempfile::tempdir().expect("tempdir should be created");
1894 let pid_file = tempdir.path().join("server.pid");
1895 let stdio_script = format!(
1896 "read _line; \
1897 echo \"$$\" > {}; \
1898 printf '%s\\n' 'not-json'; \
1899 sleep 60",
1900 shell_quote(pid_file.as_path()),
1901 );
1902
1903 let result = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1904 command: StdioExecServerCommand {
1905 program: "sh".to_string(),
1906 args: vec!["-c".to_string(), stdio_script],
1907 env: HashMap::new(),
1908 cwd: None,
1909 },
1910 client_name: "stdio-test-client".to_string(),
1911 initialize_timeout: Duration::from_secs(1),
1912 resume_session_id: None,
1913 })
1914 .await;
1915 assert!(result.is_err(), "malformed stdio server should not connect");
1916
1917 let server_pid = read_pid_file(pid_file.as_path()).await;
1918 wait_for_process_exit(server_pid).await;
1919 }
1920
1921 #[cfg(unix)]
1922 async fn read_pid_file(path: &Path) -> u32 {
1923 for _ in 0..20 {
1924 if let Ok(contents) = std::fs::read_to_string(path) {
1925 return contents
1926 .trim()
1927 .parse()
1928 .expect("pid file should contain a pid");
1929 }
1930 sleep(Duration::from_millis(50)).await;
1931 }
1932 panic!("pid file {} should be written", path.display());
1933 }
1934
1935 #[cfg(unix)]
1936 async fn wait_for_process_exit(pid: u32) {
1937 for _ in 0..20 {
1938 if !process_exists(pid) {
1939 return;
1940 }
1941 sleep(Duration::from_millis(100)).await;
1942 }
1943 panic!("process {pid} should exit");
1944 }
1945
1946 #[cfg(unix)]
1947 fn process_exists(pid: u32) -> bool {
1948 Command::new("kill")
1949 .arg("-0")
1950 .arg(pid.to_string())
1951 .status()
1952 .is_ok_and(|status| status.success())
1953 }
1954
1955 #[cfg(unix)]
1956 fn shell_quote(path: &Path) -> String {
1957 let value = path.to_string_lossy();
1958 format!("'{}'", value.replace('\'', "'\\''"))
1959 }
1960
1961 #[tokio::test]
1962 async fn process_events_are_delivered_in_seq_order_when_notifications_are_reordered() {
1963 let (client_stdin, server_reader) = duplex(1 << 20);
1964 let (mut server_writer, client_stdout) = duplex(1 << 20);
1965 let (notifications_tx, mut notifications_rx) = mpsc::channel(16);
1966 let server = tokio::spawn(async move {
1967 let mut lines = BufReader::new(server_reader).lines();
1968 let initialize = read_jsonrpc_line(&mut lines).await;
1969 let request = match initialize {
1970 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1971 other => panic!("expected initialize request, got {other:?}"),
1972 };
1973 write_jsonrpc_line(
1974 &mut server_writer,
1975 JSONRPCMessage::Response(JSONRPCResponse {
1976 id: request.id,
1977 result: serde_json::to_value(InitializeResponse {
1978 session_id: "session-1".to_string(),
1979 })
1980 .expect("initialize response should serialize"),
1981 }),
1982 )
1983 .await;
1984
1985 let initialized = read_jsonrpc_line(&mut lines).await;
1986 match initialized {
1987 JSONRPCMessage::Notification(notification)
1988 if notification.method == INITIALIZED_METHOD => {}
1989 other => panic!("expected initialized notification, got {other:?}"),
1990 }
1991
1992 while let Some(message) = notifications_rx.recv().await {
1993 write_jsonrpc_line(&mut server_writer, message).await;
1994 }
1995 });
1996
1997 let client = ExecServerClient::connect(
1998 JsonRpcConnection::from_stdio(
1999 client_stdout,
2000 client_stdin,
2001 "test-exec-server-client".to_string(),
2002 ),
2003 ExecServerClientConnectOptions::default(),
2004 )
2005 .await
2006 .expect("client should connect");
2007
2008 let process_id = ProcessId::from("reordered");
2009 let session = client
2010 .register_session(&process_id)
2011 .await
2012 .expect("session should register");
2013 let mut events = session.subscribe_events();
2014
2015 for message in [
2016 JSONRPCMessage::Notification(JSONRPCNotification {
2017 method: EXEC_CLOSED_METHOD.to_string(),
2018 params: Some(
2019 serde_json::to_value(ExecClosedNotification {
2020 process_id: process_id.clone(),
2021 seq: 4,
2022 })
2023 .expect("closed notification should serialize"),
2024 ),
2025 }),
2026 JSONRPCMessage::Notification(JSONRPCNotification {
2027 method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2028 params: Some(
2029 serde_json::to_value(ExecOutputDeltaNotification {
2030 process_id: process_id.clone(),
2031 seq: 1,
2032 stream: ExecOutputStream::Stdout,
2033 chunk: b"one".to_vec().into(),
2034 })
2035 .expect("output notification should serialize"),
2036 ),
2037 }),
2038 JSONRPCMessage::Notification(JSONRPCNotification {
2039 method: EXEC_EXITED_METHOD.to_string(),
2040 params: Some(
2041 serde_json::to_value(ExecExitedNotification {
2042 process_id: process_id.clone(),
2043 seq: 3,
2044 exit_code: 0,
2045 sandbox_denied: Some(true),
2046 })
2047 .expect("exit notification should serialize"),
2048 ),
2049 }),
2050 JSONRPCMessage::Notification(JSONRPCNotification {
2051 method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2052 params: Some(
2053 serde_json::to_value(ExecOutputDeltaNotification {
2054 process_id: process_id.clone(),
2055 seq: 2,
2056 stream: ExecOutputStream::Stderr,
2057 chunk: b"two".to_vec().into(),
2058 })
2059 .expect("output notification should serialize"),
2060 ),
2061 }),
2062 ] {
2063 notifications_tx
2064 .send(message)
2065 .await
2066 .expect("notification should queue");
2067 }
2068
2069 let mut delivered = Vec::new();
2070 for _ in 0..4 {
2071 delivered.push(
2072 timeout(Duration::from_secs(1), events.recv())
2073 .await
2074 .expect("process event should not time out")
2075 .expect("process event stream should stay open"),
2076 );
2077 }
2078
2079 assert_eq!(
2080 delivered,
2081 vec![
2082 ExecProcessEvent::Output(ProcessOutputChunk {
2083 seq: 1,
2084 stream: ExecOutputStream::Stdout,
2085 chunk: b"one".to_vec().into(),
2086 }),
2087 ExecProcessEvent::Output(ProcessOutputChunk {
2088 seq: 2,
2089 stream: ExecOutputStream::Stderr,
2090 chunk: b"two".to_vec().into(),
2091 }),
2092 ExecProcessEvent::Exited {
2093 seq: 3,
2094 exit_code: 0,
2095 sandbox_denied: Some(true),
2096 },
2097 ExecProcessEvent::Closed { seq: 4 },
2098 ]
2099 );
2100
2101 drop(notifications_tx);
2102 drop(client);
2103 server.await.expect("server task should finish");
2104 }
2105
2106 #[tokio::test]
2107 async fn transport_disconnect_fails_sessions_and_rejects_new_sessions() {
2108 let (client_stdin, server_reader) = duplex(1 << 20);
2109 let (mut server_writer, client_stdout) = duplex(1 << 20);
2110 let (disconnect_tx, disconnect_rx) = oneshot::channel();
2111 let server = tokio::spawn(async move {
2112 let mut lines = BufReader::new(server_reader).lines();
2113 let initialize = read_jsonrpc_line(&mut lines).await;
2114 let request = match initialize {
2115 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2116 other => panic!("expected initialize request, got {other:?}"),
2117 };
2118 write_jsonrpc_line(
2119 &mut server_writer,
2120 JSONRPCMessage::Response(JSONRPCResponse {
2121 id: request.id,
2122 result: serde_json::to_value(InitializeResponse {
2123 session_id: "session-1".to_string(),
2124 })
2125 .expect("initialize response should serialize"),
2126 }),
2127 )
2128 .await;
2129
2130 let initialized = read_jsonrpc_line(&mut lines).await;
2131 match initialized {
2132 JSONRPCMessage::Notification(notification)
2133 if notification.method == INITIALIZED_METHOD => {}
2134 other => panic!("expected initialized notification, got {other:?}"),
2135 }
2136
2137 let _ = disconnect_rx.await;
2138 drop(server_writer);
2139 });
2140
2141 let client = ExecServerClient::connect(
2142 JsonRpcConnection::from_stdio(
2143 client_stdout,
2144 client_stdin,
2145 "test-exec-server-client".to_string(),
2146 ),
2147 ExecServerClientConnectOptions::default(),
2148 )
2149 .await
2150 .expect("client should connect");
2151
2152 let process_id = ProcessId::from("disconnect");
2153 let session = client
2154 .register_session(&process_id)
2155 .await
2156 .expect("session should register");
2157 let mut events = session.subscribe_events();
2158
2159 disconnect_tx.send(()).expect("disconnect should signal");
2160
2161 let event = timeout(Duration::from_secs(1), events.recv())
2162 .await
2163 .expect("session failure should not time out")
2164 .expect("session event stream should stay open");
2165 let ExecProcessEvent::Failed(message) = event else {
2166 panic!("expected session failure after disconnect, got {event:?}");
2167 };
2168 assert_eq!(message, "exec-server transport disconnected");
2169
2170 let response = session
2171 .read(
2172 None, None, None,
2173 )
2174 .await
2175 .expect("disconnected session read should synthesize a response");
2176 assert_eq!(
2177 response.failure.as_deref(),
2178 Some("exec-server transport disconnected")
2179 );
2180 assert!(response.closed);
2181
2182 let new_session = client.register_session(&ProcessId::from("new")).await;
2183 assert!(matches!(
2184 new_session,
2185 Err(super::ExecServerError::Disconnected(_))
2186 ));
2187
2188 drop(client);
2189 server.await.expect("server task should finish");
2190 }
2191
2192 #[tokio::test]
2193 async fn remote_websocket_client_resumes_session() {
2194 let listener = TcpListener::bind("127.0.0.1:0")
2195 .await
2196 .expect("listener should bind");
2197 let websocket_url = format!(
2198 "ws://{}",
2199 listener.local_addr().expect("listener should have address")
2200 );
2201 let (resumed_tx, resumed_rx) = oneshot::channel();
2202 let (finish_tx, finish_rx) = oneshot::channel();
2203 let server = tokio::spawn(async move {
2204 let mut first = accept_websocket(&listener).await;
2205 complete_websocket_initialize(
2206 &mut first,
2207 "session-1",
2208 None,
2209 )
2210 .await;
2211 first.close(None).await.expect("websocket should close");
2212
2213 let mut resumed = accept_websocket(&listener).await;
2214 complete_websocket_initialize(
2215 &mut resumed,
2216 "session-1",
2217 Some("session-1"),
2218 )
2219 .await;
2220 resumed_tx.send(()).expect("resume should signal");
2221 finish_rx.await.expect("test should finish");
2222 });
2223
2224 let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2225 websocket_url,
2226 connect_timeout: Duration::from_secs(1),
2227 initialize_timeout: Duration::from_secs(1),
2228 });
2229 let stable_client = client.get().await.expect("client should connect");
2230 timeout(Duration::from_secs(1), resumed_rx)
2231 .await
2232 .expect("session resume should not time out")
2233 .expect("session resume should signal");
2234 let reused_client = client.get().await.expect("client should stay connected");
2235 assert_eq!(stable_client.session_id().as_deref(), Some("session-1"));
2236 assert!(Arc::ptr_eq(&stable_client.inner, &reused_client.inner));
2237 finish_tx.send(()).expect("test should finish");
2238 server.await.expect("server task should finish");
2239 }
2240
2241 #[tokio::test]
2242 async fn session_write_retries_same_write_id_after_recovery() {
2243 let listener = TcpListener::bind("127.0.0.1:0")
2244 .await
2245 .expect("listener should bind");
2246 let websocket_url = format!(
2247 "ws://{}",
2248 listener.local_addr().expect("listener should have address")
2249 );
2250 let (finish_tx, finish_rx) = oneshot::channel();
2251 let server = tokio::spawn(async move {
2252 let mut first = accept_websocket(&listener).await;
2253 complete_websocket_initialize(
2254 &mut first,
2255 "session-1",
2256 None,
2257 )
2258 .await;
2259
2260 let first_write = read_jsonrpc_websocket(&mut first).await;
2261 let first_write = match first_write {
2262 JSONRPCMessage::Request(request) if request.method == EXEC_WRITE_METHOD => request,
2263 other => panic!("expected first process/write request, got {other:?}"),
2264 };
2265 let first_write_params: WriteParams =
2266 serde_json::from_value(first_write.params.expect("write params should exist"))
2267 .expect("write params should deserialize");
2268 assert_eq!(first_write_params.process_id.as_str(), "proc-write");
2269 assert_eq!(first_write_params.chunk.into_inner(), b"hello\n".to_vec());
2270 let write_id = first_write_params.write_id;
2271 assert!(!write_id.is_empty());
2272 drop(first);
2273
2274 let mut resumed = accept_websocket(&listener).await;
2275 complete_websocket_initialize(
2276 &mut resumed,
2277 "session-1",
2278 Some("session-1"),
2279 )
2280 .await;
2281
2282 let recovery_read = read_jsonrpc_websocket(&mut resumed).await;
2283 let recovery_read = match recovery_read {
2284 JSONRPCMessage::Request(request) if request.method == EXEC_READ_METHOD => request,
2285 other => panic!("expected recovery process/read request, got {other:?}"),
2286 };
2287 write_jsonrpc_websocket(
2288 &mut resumed,
2289 JSONRPCMessage::Response(JSONRPCResponse {
2290 id: recovery_read.id,
2291 result: serde_json::to_value(ReadResponse {
2292 chunks: Vec::new(),
2293 next_seq: 1,
2294 exited: false,
2295 exit_code: None,
2296 closed: false,
2297 failure: None,
2298 sandbox_denied: false,
2299 })
2300 .expect("read response should serialize"),
2301 }),
2302 )
2303 .await;
2304
2305 let retried_write = read_jsonrpc_websocket(&mut resumed).await;
2306 let retried_write = match retried_write {
2307 JSONRPCMessage::Request(request) if request.method == EXEC_WRITE_METHOD => request,
2308 other => panic!("expected retried process/write request, got {other:?}"),
2309 };
2310 let retried_write_params: WriteParams =
2311 serde_json::from_value(retried_write.params.expect("write params should exist"))
2312 .expect("write params should deserialize");
2313 assert_eq!(retried_write_params.process_id.as_str(), "proc-write");
2314 assert_eq!(retried_write_params.chunk.into_inner(), b"hello\n".to_vec());
2315 assert_eq!(retried_write_params.write_id, write_id);
2316 write_jsonrpc_websocket(
2317 &mut resumed,
2318 JSONRPCMessage::Response(JSONRPCResponse {
2319 id: retried_write.id,
2320 result: serde_json::to_value(WriteResponse {
2321 status: WriteStatus::Accepted,
2322 })
2323 .expect("write response should serialize"),
2324 }),
2325 )
2326 .await;
2327
2328 finish_rx.await.expect("test should finish");
2329 });
2330
2331 let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2332 websocket_url,
2333 connect_timeout: Duration::from_secs(1),
2334 initialize_timeout: Duration::from_secs(1),
2335 });
2336 let stable_client = client.get().await.expect("client should connect");
2337 let session = stable_client
2338 .register_session(&ProcessId::from("proc-write"))
2339 .await
2340 .expect("session should register");
2341
2342 let response = timeout(Duration::from_secs(2), session.write(b"hello\n".to_vec()))
2343 .await
2344 .expect("write should not time out")
2345 .expect("write should recover");
2346 assert_eq!(
2347 response,
2348 WriteResponse {
2349 status: WriteStatus::Accepted
2350 }
2351 );
2352
2353 finish_tx.send(()).expect("test should finish");
2354 server.await.expect("server task should finish");
2355 }
2356
2357 #[tokio::test]
2358 async fn explicit_resume_drains_notifications_before_initialize_response() {
2359 let listener = TcpListener::bind("127.0.0.1:0")
2360 .await
2361 .expect("listener should bind");
2362 let websocket_url = format!(
2363 "ws://{}",
2364 listener.local_addr().expect("listener should have address")
2365 );
2366 let (initialized_tx, initialized_rx) = oneshot::channel();
2367 let (finish_tx, finish_rx) = oneshot::channel();
2368 let server = tokio::spawn(async move {
2369 let mut websocket = accept_websocket(&listener).await;
2370 let initialize = read_jsonrpc_websocket(&mut websocket).await;
2371 let request = match initialize {
2372 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2373 other => panic!("expected initialize request, got {other:?}"),
2374 };
2375 let params: crate::protocol::InitializeParams =
2376 serde_json::from_value(request.params.expect("initialize params should exist"))
2377 .expect("initialize params should deserialize");
2378 assert_eq!(params.resume_session_id.as_deref(), Some("session-1"));
2379
2380 for seq in 1..=256 {
2381 write_jsonrpc_websocket(
2382 &mut websocket,
2383 JSONRPCMessage::Notification(JSONRPCNotification {
2384 method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2385 params: Some(
2386 serde_json::to_value(ExecOutputDeltaNotification {
2387 process_id: ProcessId::from("busy-process"),
2388 seq,
2389 stream: ExecOutputStream::Stdout,
2390 chunk: b"output".to_vec().into(),
2391 })
2392 .expect("output notification should serialize"),
2393 ),
2394 }),
2395 )
2396 .await;
2397 }
2398 write_jsonrpc_websocket(
2399 &mut websocket,
2400 JSONRPCMessage::Response(JSONRPCResponse {
2401 id: request.id,
2402 result: serde_json::to_value(InitializeResponse {
2403 session_id: "session-1".to_string(),
2404 })
2405 .expect("initialize response should serialize"),
2406 }),
2407 )
2408 .await;
2409
2410 let initialized = read_jsonrpc_websocket(&mut websocket).await;
2411 match initialized {
2412 JSONRPCMessage::Notification(notification)
2413 if notification.method == INITIALIZED_METHOD => {}
2414 other => panic!("expected initialized notification, got {other:?}"),
2415 }
2416 initialized_tx
2417 .send(())
2418 .expect("initialized notification should signal");
2419 finish_rx.await.expect("test should finish");
2420 });
2421
2422 let client = timeout(
2423 Duration::from_secs(1),
2424 ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
2425 websocket_url,
2426 client_name: "test-client".to_string(),
2427 connect_timeout: Duration::from_secs(1),
2428 initialize_timeout: Duration::from_secs(1),
2429 resume_session_id: Some("session-1".to_string()),
2430 }),
2431 )
2432 .await
2433 .expect("explicit resume should not time out")
2434 .expect("explicit resume should connect");
2435 assert_eq!(client.session_id().as_deref(), Some("session-1"));
2436
2437 timeout(Duration::from_secs(1), initialized_rx)
2438 .await
2439 .expect("initialized notification should not time out")
2440 .expect("initialized notification should signal");
2441 finish_tx.send(()).expect("test should finish");
2442 server.await.expect("server task should finish");
2443 }
2444
2445 #[tokio::test]
2446 async fn initial_connection_is_shared_by_all_waiters() {
2447 let listener = TcpListener::bind("127.0.0.1:0")
2448 .await
2449 .expect("listener should bind");
2450 let websocket_url = format!(
2451 "ws://{}",
2452 listener.local_addr().expect("listener should have address")
2453 );
2454 let server = tokio::spawn(async move {
2455 let mut connection = accept_websocket(&listener).await;
2456 complete_websocket_initialize(
2457 &mut connection,
2458 "startup-session",
2459 None,
2460 )
2461 .await;
2462 timeout(Duration::from_secs(1), connection.next())
2463 .await
2464 .expect("client should close after the test");
2465 });
2466 let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2467 websocket_url,
2468 connect_timeout: Duration::from_secs(1),
2469 initialize_timeout: Duration::from_secs(1),
2470 });
2471
2472 assert!(!client.startup_finished());
2473 let _startup_task = client.start_connecting();
2474 let (ready, first, second) =
2475 tokio::join!(client.wait_until_ready(), client.get(), client.get());
2476 ready.expect("background startup should finish");
2477 let first = first.expect("first waiter should receive the client");
2478 let second = second.expect("second waiter should receive the same client");
2479
2480 assert!(client.startup_finished());
2481 assert_eq!(first.session_id().as_deref(), Some("startup-session"));
2482 assert!(Arc::ptr_eq(&first.inner, &second.inner));
2483
2484 drop(first);
2485 drop(second);
2486 drop(client);
2487 server.await.expect("server task should finish");
2488 }
2489
2490 #[tokio::test]
2491 async fn terminal_stdio_startup_failure_is_remembered() {
2492 let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::StdioCommand {
2493 command: StdioExecServerCommand {
2494 program: "codex-missing-exec-server-for-test".to_string(),
2495 args: Vec::new(),
2496 env: HashMap::new(),
2497 cwd: None,
2498 },
2499 initialize_timeout: Duration::from_secs(1),
2500 });
2501
2502 assert!(client.start_connecting().is_none());
2503 assert!(!client.startup_finished());
2504 let first = match client.get().await {
2505 Ok(_) => panic!("missing executable should fail"),
2506 Err(error) => error,
2507 };
2508 assert!(client.startup_finished());
2509 let second = match client.get().await {
2510 Ok(_) => panic!("burned environment should stay failed"),
2511 Err(error) => error,
2512 };
2513 assert!(matches!(
2514 client.status().await,
2515 EnvironmentObservedStatus::Disconnected { .. }
2516 ));
2517
2518 let (
2519 super::ExecServerError::ConnectionAttempt(first),
2520 super::ExecServerError::ConnectionAttempt(second),
2521 ) = (first, second)
2522 else {
2523 panic!("expected saved connection failures");
2524 };
2525 assert!(Arc::ptr_eq(&first, &second));
2526 }
2527
2528 #[tokio::test]
2529 async fn failed_reconnect_does_not_burn_environment() {
2530 let listener = TcpListener::bind("127.0.0.1:0")
2531 .await
2532 .expect("listener should bind");
2533 let websocket_url = format!(
2534 "ws://{}",
2535 listener.local_addr().expect("listener should have address")
2536 );
2537 let (replacement_initialized_tx, replacement_initialized_rx) = oneshot::channel();
2538 let (allow_replacement_tx, allow_replacement_rx) = watch::channel(false);
2539 let server = tokio::spawn(async move {
2540 let mut first = accept_websocket(&listener).await;
2541 complete_websocket_initialize(
2542 &mut first,
2543 "startup-session",
2544 None,
2545 )
2546 .await;
2547 first
2548 .close(None)
2549 .await
2550 .expect("startup websocket should close");
2551
2552 let successful_reconnect = loop {
2553 let (stream, _) = listener.accept().await.expect("reconnect should arrive");
2554 if *allow_replacement_rx.borrow() {
2555 break stream;
2556 }
2557 let mut failed_reconnect = stream;
2558 failed_reconnect
2559 .write_all(b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n")
2560 .await
2561 .expect("failed handshake response should write");
2562 };
2563 let mut successful_reconnect = accept_async(successful_reconnect)
2564 .await
2565 .expect("replacement websocket handshake should succeed");
2566 complete_websocket_initialize(
2567 &mut successful_reconnect,
2568 "replacement-session",
2569 None,
2570 )
2571 .await;
2572 replacement_initialized_tx
2573 .send(())
2574 .expect("replacement initialization should be observed");
2575 timeout(Duration::from_secs(1), successful_reconnect.next())
2576 .await
2577 .expect("client should close after the test");
2578 });
2579 let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2580 websocket_url,
2581 connect_timeout: Duration::from_secs(1),
2582 initialize_timeout: Duration::from_secs(1),
2583 });
2584
2585 let initial = client.get().await.expect("startup should connect");
2586 timeout(Duration::from_secs(1), async {
2587 while !initial.is_disconnected() {
2588 tokio::task::yield_now().await;
2589 }
2590 })
2591 .await
2592 .expect("client should observe disconnect");
2593 let failed_reconnect = match client.get().await {
2594 Ok(_) => panic!("first lazy reconnect should fail"),
2595 Err(error) => error,
2596 };
2597 assert!(matches!(
2598 failed_reconnect,
2599 super::ExecServerError::ConnectionAttempt(_)
2600 ));
2601 allow_replacement_tx
2602 .send(true)
2603 .expect("server should allow a fresh client");
2604 let replacement = client.get().await.expect("later reconnect should succeed");
2605
2606 assert_eq!(
2607 replacement.session_id().as_deref(),
2608 Some("replacement-session")
2609 );
2610 replacement_initialized_rx
2611 .await
2612 .expect("server should observe replacement initialization");
2613
2614 drop(initial);
2615 drop(replacement);
2616 drop(client);
2617 server.await.expect("server task should finish");
2618 }
2619
2620 #[tokio::test]
2621 async fn wake_notifications_do_not_block_other_sessions() {
2622 let (client_stdin, server_reader) = duplex(1 << 20);
2623 let (mut server_writer, client_stdout) = duplex(1 << 20);
2624 let (notifications_tx, mut notifications_rx) = mpsc::channel(16);
2625 let server = tokio::spawn(async move {
2626 let mut lines = BufReader::new(server_reader).lines();
2627 let initialize = read_jsonrpc_line(&mut lines).await;
2628 let request = match initialize {
2629 JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2630 other => panic!("expected initialize request, got {other:?}"),
2631 };
2632 write_jsonrpc_line(
2633 &mut server_writer,
2634 JSONRPCMessage::Response(JSONRPCResponse {
2635 id: request.id,
2636 result: serde_json::to_value(InitializeResponse {
2637 session_id: "session-1".to_string(),
2638 })
2639 .expect("initialize response should serialize"),
2640 }),
2641 )
2642 .await;
2643
2644 let initialized = read_jsonrpc_line(&mut lines).await;
2645 match initialized {
2646 JSONRPCMessage::Notification(notification)
2647 if notification.method == INITIALIZED_METHOD => {}
2648 other => panic!("expected initialized notification, got {other:?}"),
2649 }
2650
2651 while let Some(message) = notifications_rx.recv().await {
2652 write_jsonrpc_line(&mut server_writer, message).await;
2653 }
2654 });
2655
2656 let client = ExecServerClient::connect(
2657 JsonRpcConnection::from_stdio(
2658 client_stdout,
2659 client_stdin,
2660 "test-exec-server-client".to_string(),
2661 ),
2662 ExecServerClientConnectOptions::default(),
2663 )
2664 .await
2665 .expect("client should connect");
2666
2667 let noisy_process_id = ProcessId::from("noisy");
2668 let quiet_process_id = ProcessId::from("quiet");
2669 let _noisy_session = client
2670 .register_session(&noisy_process_id)
2671 .await
2672 .expect("noisy session should register");
2673 let quiet_session = client
2674 .register_session(&quiet_process_id)
2675 .await
2676 .expect("quiet session should register");
2677 let mut quiet_wake_rx = quiet_session.subscribe_wake();
2678
2679 for seq in 0..=4096 {
2680 notifications_tx
2681 .send(JSONRPCMessage::Notification(JSONRPCNotification {
2682 method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2683 params: Some(
2684 serde_json::to_value(ExecOutputDeltaNotification {
2685 process_id: noisy_process_id.clone(),
2686 seq,
2687 stream: ExecOutputStream::Stdout,
2688 chunk: b"x".to_vec().into(),
2689 })
2690 .expect("output notification should serialize"),
2691 ),
2692 }))
2693 .await
2694 .expect("output notification should queue");
2695 }
2696
2697 notifications_tx
2698 .send(JSONRPCMessage::Notification(JSONRPCNotification {
2699 method: EXEC_EXITED_METHOD.to_string(),
2700 params: Some(
2701 serde_json::to_value(ExecExitedNotification {
2702 process_id: quiet_process_id,
2703 seq: 1,
2704 exit_code: 17,
2705 sandbox_denied: Some(false),
2706 })
2707 .expect("exit notification should serialize"),
2708 ),
2709 }))
2710 .await
2711 .expect("exit notification should queue");
2712
2713 timeout(Duration::from_secs(1), quiet_wake_rx.changed())
2714 .await
2715 .expect("quiet session should receive wake before timeout")
2716 .expect("quiet wake channel should stay open");
2717 assert_eq!(*quiet_wake_rx.borrow(), 1);
2718
2719 drop(notifications_tx);
2720 drop(client);
2721 server.await.expect("server task should finish");
2722 }
2723}