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
36pub 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(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
189pub 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;