Skip to main content

codex_code_mode/
remote_session.rs

1use std::ffi::OsString;
2use std::io;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::sync::Mutex as StdMutex;
6use std::sync::atomic::AtomicBool;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9
10use codex_code_mode_protocol::CellId;
11use codex_code_mode_protocol::CodeModeSession;
12use codex_code_mode_protocol::CodeModeSessionDelegate;
13use codex_code_mode_protocol::CodeModeSessionProvider;
14use codex_code_mode_protocol::CodeModeSessionProviderFuture;
15use codex_code_mode_protocol::CodeModeSessionResultFuture;
16use codex_code_mode_protocol::ExecuteRequest;
17use codex_code_mode_protocol::StartedCell;
18use codex_code_mode_protocol::WaitOutcome;
19use codex_code_mode_protocol::WaitRequest;
20use codex_code_mode_protocol::host::SessionId;
21use tokio::sync::Semaphore;
22use tokio::sync::watch;
23
24use self::connection::Connection;
25use self::connection::ConnectionError;
26use self::connection::RemoteSession;
27use self::connection::SessionCleanup;
28use crate::NoopCodeModeSessionDelegate;
29
30mod connection;
31
32const CODE_MODE_HOST_PATH_ENV: &str = "CODEX_CODE_MODE_HOST_PATH";
33
34type ShutdownResultReceiver = watch::Receiver<Option<Result<(), String>>>;
35
36/// Creates code-mode sessions backed by one lazily spawned process host.
37pub struct ProcessOwnedCodeModeSessionProvider {
38    state: StdMutex<ProviderState>,
39}
40
41enum ProviderState {
42    OwnedProcess(Arc<OwnedProcessHost>),
43    InProcess,
44}
45
46impl ProcessOwnedCodeModeSessionProvider {
47    pub fn with_host_program(host_program: PathBuf) -> Self {
48        Self {
49            state: StdMutex::new(ProviderState::OwnedProcess(Arc::new(
50                OwnedProcessHost::new(host_program),
51            ))),
52        }
53    }
54
55    fn process_host(&self) -> Option<Arc<OwnedProcessHost>> {
56        match &*self
57            .state
58            .lock()
59            .unwrap_or_else(std::sync::PoisonError::into_inner)
60        {
61            ProviderState::OwnedProcess(process_host) => Some(Arc::clone(process_host)),
62            ProviderState::InProcess => None,
63        }
64    }
65}
66
67impl Default for ProcessOwnedCodeModeSessionProvider {
68    fn default() -> Self {
69        Self::with_host_program(default_host_program())
70    }
71}
72
73impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
74    fn create_session<'a>(
75        &'a self,
76        delegate: Arc<dyn CodeModeSessionDelegate>,
77    ) -> CodeModeSessionProviderFuture<'a> {
78        Box::pin(async move {
79            let Some(process_host) = self.process_host() else {
80                let session: Arc<dyn CodeModeSession> =
81                    Arc::new(crate::InProcessCodeModeSession::with_delegate(delegate));
82                return Ok(session);
83            };
84
85            match process_host.connection().await {
86                Ok(_) => {}
87                Err(error) if error.host_program_not_found() => {
88                    *self
89                        .state
90                        .lock()
91                        .unwrap_or_else(std::sync::PoisonError::into_inner) =
92                        ProviderState::InProcess;
93                    let session: Arc<dyn CodeModeSession> =
94                        Arc::new(crate::InProcessCodeModeSession::with_delegate(delegate));
95                    return Ok(session);
96                }
97                Err(error) => return Err(error.to_string()),
98            }
99            let session = ProcessOwnedCodeModeSession::with_process_host(delegate, process_host);
100            session.connection().await?;
101            let session: Arc<dyn CodeModeSession> = Arc::new(session);
102            Ok(session)
103        })
104    }
105}
106
107struct OwnedProcessHost {
108    host_program: PathBuf,
109    connection: StdMutex<Option<Arc<Connection>>>,
110    spawn_permit: Semaphore,
111    next_session_id: AtomicU64,
112}
113
114impl OwnedProcessHost {
115    fn new(host_program: PathBuf) -> Self {
116        Self {
117            host_program,
118            connection: StdMutex::new(None),
119            spawn_permit: Semaphore::new(/*permits*/ 1),
120            next_session_id: AtomicU64::new(1),
121        }
122    }
123
124    async fn connection(&self) -> Result<Arc<Connection>, ConnectionError> {
125        if let Some(connection) = self.live_connection() {
126            return Ok(connection);
127        }
128
129        let _spawn_permit = self.spawn_permit.acquire().await.map_err(|_| {
130            ConnectionError::Other("code-mode host spawn coordinator closed".into())
131        })?;
132        if let Some(connection) = self.live_connection() {
133            return Ok(connection);
134        }
135        let new_connection = Arc::new(Connection::spawn(&self.host_program).await?);
136        *self
137            .connection
138            .lock()
139            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(&new_connection));
140        Ok(new_connection)
141    }
142
143    fn live_connection(&self) -> Option<Arc<Connection>> {
144        self.connection
145            .lock()
146            .unwrap_or_else(std::sync::PoisonError::into_inner)
147            .as_ref()
148            .filter(|connection| connection.is_alive())
149            .cloned()
150    }
151
152    fn allocate_session_id(&self) -> SessionId {
153        let value = self.next_session_id.fetch_add(1, Ordering::Relaxed);
154        match SessionId::new(format!("session-{value}")) {
155            Ok(session_id) => session_id,
156            Err(_) => unreachable!("a generated code-mode session ID is nonempty"),
157        }
158    }
159}
160
161enum SessionState {
162    New,
163    Opening {
164        remote: RemoteSession,
165        result_rx: watch::Receiver<Option<Result<SessionBinding, String>>>,
166    },
167    Open(SessionBinding),
168    Closing,
169    Closed,
170}
171
172#[derive(Clone)]
173struct SessionBinding {
174    connection: Arc<Connection>,
175    remote: RemoteSession,
176    cleanup: SessionCleanup,
177}
178
179struct SessionInner {
180    process_host: Arc<OwnedProcessHost>,
181    delegate: Arc<dyn CodeModeSessionDelegate>,
182    state: StdMutex<SessionState>,
183    next_generation: AtomicU64,
184    shutdown_requested: AtomicBool,
185    shutdown_result: StdMutex<Option<ShutdownResultReceiver>>,
186    retired_cleanups: StdMutex<Vec<SessionCleanup>>,
187}
188
189/// A logical code-mode session assigned to a process-owned host.
190pub struct ProcessOwnedCodeModeSession {
191    inner: Arc<SessionInner>,
192}
193
194impl ProcessOwnedCodeModeSession {
195    pub fn new() -> Self {
196        Self::with_process_host(
197            Arc::new(NoopCodeModeSessionDelegate),
198            Arc::new(OwnedProcessHost::new(default_host_program())),
199        )
200    }
201
202    fn with_process_host(
203        delegate: Arc<dyn CodeModeSessionDelegate>,
204        process_host: Arc<OwnedProcessHost>,
205    ) -> Self {
206        Self {
207            inner: Arc::new(SessionInner {
208                process_host,
209                delegate,
210                state: StdMutex::new(SessionState::New),
211                next_generation: AtomicU64::new(1),
212                shutdown_requested: AtomicBool::new(false),
213                shutdown_result: StdMutex::new(None),
214                retired_cleanups: StdMutex::new(Vec::new()),
215            }),
216        }
217    }
218
219    async fn connection(&self) -> Result<SessionBinding, String> {
220        self.inner.connection().await
221    }
222
223    pub async fn execute(&self, request: ExecuteRequest) -> Result<StartedCell, String> {
224        let binding = self.connection().await?;
225        binding.connection.execute(binding.remote, request).await
226    }
227
228    pub async fn wait(&self, request: WaitRequest) -> Result<WaitOutcome, String> {
229        let binding = self.connection().await?;
230        binding.connection.wait(binding.remote, request).await
231    }
232
233    pub async fn terminate(&self, cell_id: CellId) -> Result<WaitOutcome, String> {
234        let binding = self.connection().await?;
235        binding.connection.terminate(binding.remote, cell_id).await
236    }
237
238    pub async fn shutdown(&self) -> Result<(), String> {
239        wait_for_watch(self.inner.request_shutdown()).await
240    }
241}
242
243impl SessionInner {
244    async fn connection(self: &Arc<Self>) -> Result<SessionBinding, String> {
245        loop {
246            if self.shutdown_requested.load(Ordering::Acquire) {
247                return Err("code mode session is shutting down".to_string());
248            }
249            let (result_rx, start) = {
250                let mut state = self
251                    .state
252                    .lock()
253                    .unwrap_or_else(std::sync::PoisonError::into_inner);
254                match &*state {
255                    SessionState::New => {
256                        let generation = self.next_generation.fetch_add(1, Ordering::Relaxed);
257                        let remote = RemoteSession {
258                            id: self.process_host.allocate_session_id(),
259                            generation,
260                        };
261                        let (result_tx, result_rx) = watch::channel(None);
262                        *state = SessionState::Opening {
263                            remote: remote.clone(),
264                            result_rx: result_rx.clone(),
265                        };
266                        (result_rx, Some((remote, result_tx)))
267                    }
268                    SessionState::Opening { result_rx, .. } => (result_rx.clone(), None),
269                    SessionState::Open(binding) if binding.connection.is_alive() => {
270                        return Ok(binding.clone());
271                    }
272                    SessionState::Open(binding) => {
273                        self.retain_cleanup(binding.cleanup.clone());
274                        *state = SessionState::New;
275                        continue;
276                    }
277                    SessionState::Closing | SessionState::Closed => {
278                        return Err("code mode session is shutting down".to_string());
279                    }
280                }
281            };
282            if let Some((remote, result_tx)) = start {
283                let inner = Arc::clone(self);
284                tokio::spawn(async move {
285                    inner.open(remote, result_tx).await;
286                });
287            }
288            return wait_for_watch(result_rx).await;
289        }
290    }
291
292    async fn open(
293        self: Arc<Self>,
294        remote: RemoteSession,
295        result_tx: watch::Sender<Option<Result<SessionBinding, String>>>,
296    ) {
297        let result = match self.process_host.connection().await {
298            Ok(connection) => {
299                let cleanup = connection
300                    .open_session(remote.clone(), Arc::clone(&self.delegate))
301                    .await;
302                cleanup.map(|cleanup| SessionBinding {
303                    connection,
304                    remote: remote.clone(),
305                    cleanup,
306                })
307            }
308            Err(err) => Err(err.to_string()),
309        };
310        {
311            let mut state = self
312                .state
313                .lock()
314                .unwrap_or_else(std::sync::PoisonError::into_inner);
315            if matches!(
316                &*state,
317                SessionState::Opening {
318                    remote: opening_remote,
319                    ..
320                } if opening_remote == &remote
321            ) {
322                *state = match &result {
323                    Ok(binding) => SessionState::Open(binding.clone()),
324                    Err(_) => SessionState::New,
325                };
326            }
327        }
328        result_tx.send_replace(Some(result));
329    }
330
331    fn request_shutdown(self: &Arc<Self>) -> ShutdownResultReceiver {
332        self.shutdown_requested.store(true, Ordering::Release);
333        let mut shutdown_result = self
334            .shutdown_result
335            .lock()
336            .unwrap_or_else(std::sync::PoisonError::into_inner);
337        if let Some(result_rx) = shutdown_result.as_ref() {
338            return result_rx.clone();
339        }
340        let (result_tx, result_rx) = watch::channel(None);
341        *shutdown_result = Some(result_rx.clone());
342        let inner = Arc::clone(self);
343        tokio::spawn(async move {
344            let result = inner.drive_shutdown().await;
345            result_tx.send_replace(Some(result));
346        });
347        result_rx
348    }
349
350    async fn drive_shutdown(self: &Arc<Self>) -> Result<(), String> {
351        loop {
352            let action = {
353                let mut state = self
354                    .state
355                    .lock()
356                    .unwrap_or_else(std::sync::PoisonError::into_inner);
357                match &*state {
358                    SessionState::New => {
359                        *state = SessionState::Closed;
360                        ShutdownAction::Finish
361                    }
362                    SessionState::Opening { result_rx, .. } => {
363                        ShutdownAction::WaitForOpen(result_rx.clone())
364                    }
365                    SessionState::Open(binding) if !binding.connection.is_alive() => {
366                        let cleanup = binding.cleanup.clone();
367                        *state = SessionState::Closing;
368                        ShutdownAction::WaitForSessionCleanup(cleanup)
369                    }
370                    SessionState::Open(binding) => {
371                        let binding = binding.clone();
372                        *state = SessionState::Closing;
373                        ShutdownAction::Close(binding)
374                    }
375                    SessionState::Closing => {
376                        return Err("code-mode session shutdown driver entered twice".to_string());
377                    }
378                    SessionState::Closed => return Ok(()),
379                }
380            };
381            match action {
382                ShutdownAction::WaitForOpen(result_rx) => {
383                    let _ = wait_for_watch(result_rx).await;
384                }
385                ShutdownAction::Finish => {
386                    self.wait_for_retired_cleanups().await;
387                    return Ok(());
388                }
389                ShutdownAction::WaitForSessionCleanup(cleanup) => {
390                    cleanup.wait().await;
391                    self.wait_for_retired_cleanups().await;
392                    *self
393                        .state
394                        .lock()
395                        .unwrap_or_else(std::sync::PoisonError::into_inner) = SessionState::Closed;
396                    return Ok(());
397                }
398                ShutdownAction::Close(binding) => {
399                    let result = binding.connection.shutdown_session(binding.remote).await;
400                    if result.is_err() && !binding.connection.is_alive() {
401                        binding.cleanup.wait().await;
402                    }
403                    self.wait_for_retired_cleanups().await;
404                    *self
405                        .state
406                        .lock()
407                        .unwrap_or_else(std::sync::PoisonError::into_inner) = SessionState::Closed;
408                    return result;
409                }
410            }
411        }
412    }
413
414    fn retain_cleanup(&self, cleanup: SessionCleanup) {
415        let mut retired = self
416            .retired_cleanups
417            .lock()
418            .unwrap_or_else(std::sync::PoisonError::into_inner);
419        retired.retain(|cleanup| !cleanup.is_complete());
420        if !cleanup.is_complete() {
421            retired.push(cleanup);
422        }
423    }
424
425    async fn wait_for_retired_cleanups(&self) {
426        let retired = std::mem::take(
427            &mut *self
428                .retired_cleanups
429                .lock()
430                .unwrap_or_else(std::sync::PoisonError::into_inner),
431        );
432        for cleanup in retired {
433            cleanup.wait().await;
434        }
435    }
436}
437
438enum ShutdownAction {
439    WaitForOpen(watch::Receiver<Option<Result<SessionBinding, String>>>),
440    Finish,
441    WaitForSessionCleanup(SessionCleanup),
442    Close(SessionBinding),
443}
444
445async fn wait_for_watch<T>(
446    mut result_rx: watch::Receiver<Option<Result<T, String>>>,
447) -> Result<T, String>
448where
449    T: Clone,
450{
451    loop {
452        if let Some(result) = result_rx.borrow().clone() {
453            return result;
454        }
455        result_rx
456            .changed()
457            .await
458            .map_err(|_| "code-mode session transition stopped".to_string())?;
459    }
460}
461
462impl Drop for ProcessOwnedCodeModeSession {
463    fn drop(&mut self) {
464        if tokio::runtime::Handle::try_current().is_ok() {
465            self.inner.request_shutdown();
466        }
467    }
468}
469
470impl Default for ProcessOwnedCodeModeSession {
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476impl CodeModeSession for ProcessOwnedCodeModeSession {
477    fn execute<'a>(
478        &'a self,
479        request: ExecuteRequest,
480    ) -> CodeModeSessionResultFuture<'a, StartedCell> {
481        Box::pin(ProcessOwnedCodeModeSession::execute(self, request))
482    }
483
484    fn wait<'a>(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
485        Box::pin(ProcessOwnedCodeModeSession::wait(self, request))
486    }
487
488    fn terminate<'a>(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
489        Box::pin(ProcessOwnedCodeModeSession::terminate(self, cell_id))
490    }
491
492    fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()> {
493        Box::pin(ProcessOwnedCodeModeSession::shutdown(self))
494    }
495}
496
497fn default_host_program() -> PathBuf {
498    resolve_host_program(
499        std::env::var_os(CODE_MODE_HOST_PATH_ENV),
500        std::env::current_exe(),
501    )
502}
503
504fn resolve_host_program(
505    override_path: Option<OsString>,
506    current_exe: io::Result<PathBuf>,
507) -> PathBuf {
508    if let Some(path) = override_path {
509        return PathBuf::from(path);
510    }
511    let executable_name = if cfg!(windows) {
512        "codex-code-mode-host.exe"
513    } else {
514        "codex-code-mode-host"
515    };
516    if let Ok(current_exe) = current_exe
517        && let Some(parent) = current_exe.parent()
518    {
519        return parent.join(executable_name);
520    }
521    PathBuf::from(executable_name)
522}
523
524#[cfg(test)]
525#[path = "remote_session_tests.rs"]
526mod tests;