mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
## Why Remote-control connections and queued operations must not carry over to a different signed-in user or account. Token refreshes for the same identity should preserve the live relay connection. ## What changed - Scope relay state and authentication recovery to a login lifetime. Retire the session on logout or identity changes and leave remote control disabled until enabled again. - Reject stale incoming messages and queued RPCs, and cancel pending remote-control operations when their authentication owner changes. - Start replacement sessions with fresh client, replay, and enrollment state, and prevent retired sessions from publishing status into their replacements. - Serialize enrollment and preference persistence across sessions, retaining write permits through caller cancellation and draining admitted writes on shutdown. ## Testing Add regression coverage for same-owner refreshes, user and account changes, stale pairing and client-revocation work, unauthorized enrollment recovery after logging in again, and persistence during cancellation. GitOrigin-RevId: 5e6ecd23588aab1e2e6168a21f575fb24bbb341c
250 lines
7.5 KiB
Rust
250 lines
7.5 KiB
Rust
//! Controls RPC admission and draining for each connection.
|
|
//! Auth ownership changes reject queued work while started handlers finish cleanup.
|
|
|
|
use std::future::Future;
|
|
|
|
use codex_app_server_transport::ConnectionAuth;
|
|
|
|
use tokio::sync::Mutex;
|
|
use tokio_util::task::TaskTracker;
|
|
|
|
/// Per-connection gate for incoming messages and initialized RPC handler execution.
|
|
///
|
|
/// Closing the gate prevents queued handlers from starting while allowing
|
|
/// handlers that already acquired a token to finish.
|
|
#[derive(Debug)]
|
|
pub(crate) struct ConnectionRpcGate {
|
|
pub(crate) auth: Option<ConnectionAuth>,
|
|
accepting: Mutex<bool>,
|
|
tasks: TaskTracker,
|
|
}
|
|
|
|
impl ConnectionRpcGate {
|
|
pub(crate) fn new() -> Self {
|
|
let accepting = true;
|
|
Self {
|
|
auth: None,
|
|
accepting: Mutex::new(accepting),
|
|
tasks: TaskTracker::new(),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn run<F>(&self, future: F)
|
|
where
|
|
F: Future<Output = ()>,
|
|
{
|
|
let token = {
|
|
let accepting = self.accepting.lock().await;
|
|
if !*accepting || self.is_closed() {
|
|
return;
|
|
}
|
|
self.tasks.token()
|
|
};
|
|
|
|
future.await;
|
|
drop(token);
|
|
}
|
|
|
|
pub(crate) fn is_closed(&self) -> bool {
|
|
self.tasks.is_closed() || self.auth.as_ref().is_some_and(|auth| !auth.is_current())
|
|
}
|
|
|
|
pub(crate) async fn close(&self) {
|
|
let mut accepting = self.accepting.lock().await;
|
|
*accepting = false;
|
|
self.tasks.close();
|
|
}
|
|
|
|
pub(crate) async fn shutdown(&self) {
|
|
self.close().await;
|
|
self.tasks.wait().await;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
async fn is_accepting(&self) -> bool {
|
|
*self.accepting.lock().await
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn inflight_count(&self) -> usize {
|
|
self.tasks.len()
|
|
}
|
|
}
|
|
|
|
impl Default for ConnectionRpcGate {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
use std::sync::atomic::Ordering;
|
|
use tokio::sync::oneshot;
|
|
use tokio::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
#[tokio::test]
|
|
async fn run_executes_while_open() {
|
|
let gate = ConnectionRpcGate::new();
|
|
let ran = Arc::new(AtomicBool::new(/*v*/ false));
|
|
let ran_clone = Arc::clone(&ran);
|
|
|
|
gate.run(async move {
|
|
ran_clone.store(/*val*/ true, Ordering::Release);
|
|
})
|
|
.await;
|
|
|
|
assert!(ran.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn run_drops_future_without_polling_after_close() {
|
|
let gate = ConnectionRpcGate::new();
|
|
gate.close().await;
|
|
let polled = Arc::new(AtomicBool::new(/*v*/ false));
|
|
let polled_clone = Arc::clone(&polled);
|
|
|
|
gate.run(async move {
|
|
polled_clone.store(/*val*/ true, Ordering::Release);
|
|
})
|
|
.await;
|
|
|
|
assert!(!polled.load(Ordering::Acquire));
|
|
assert!(!gate.is_accepting().await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn close_returns_while_started_run_remains_active() {
|
|
let gate = Arc::new(ConnectionRpcGate::new());
|
|
let (started_tx, started_rx) = oneshot::channel();
|
|
let (finish_tx, finish_rx) = oneshot::channel();
|
|
let gate_for_run = Arc::clone(&gate);
|
|
let run_task = tokio::spawn(async move {
|
|
gate_for_run
|
|
.run(async move {
|
|
started_tx.send(()).expect("receiver should be open");
|
|
let _ = finish_rx.await;
|
|
})
|
|
.await;
|
|
});
|
|
|
|
started_rx.await.expect("run should start");
|
|
gate.close().await;
|
|
assert!(!gate.is_accepting().await);
|
|
assert_eq!(gate.inflight_count(), 1);
|
|
|
|
finish_tx
|
|
.send(())
|
|
.expect("running future should be waiting");
|
|
run_task.await.expect("run task should complete");
|
|
gate.shutdown().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_waits_for_started_run_to_finish() {
|
|
let gate = Arc::new(ConnectionRpcGate::new());
|
|
let (started_tx, started_rx) = oneshot::channel();
|
|
let (finish_tx, finish_rx) = oneshot::channel();
|
|
let gate_for_run = Arc::clone(&gate);
|
|
let run_task = tokio::spawn(async move {
|
|
gate_for_run
|
|
.run(async move {
|
|
started_tx.send(()).expect("receiver should be open");
|
|
let _ = finish_rx.await;
|
|
})
|
|
.await;
|
|
});
|
|
|
|
started_rx.await.expect("run should start");
|
|
assert_eq!(gate.inflight_count(), 1);
|
|
|
|
let gate_for_shutdown = Arc::clone(&gate);
|
|
let shutdown_task = tokio::spawn(async move {
|
|
gate_for_shutdown.shutdown().await;
|
|
});
|
|
|
|
timeout(Duration::from_millis(/*millis*/ 50), shutdown_task)
|
|
.await
|
|
.expect_err("shutdown should wait for the running future");
|
|
|
|
finish_tx
|
|
.send(())
|
|
.expect("running future should be waiting");
|
|
run_task.await.expect("run task should complete");
|
|
gate.shutdown().await;
|
|
assert_eq!(gate.inflight_count(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_drops_late_runs_while_waiting_for_inflight_work() {
|
|
let gate = Arc::new(ConnectionRpcGate::new());
|
|
let (started_tx, started_rx) = oneshot::channel();
|
|
let (finish_tx, finish_rx) = oneshot::channel();
|
|
let gate_for_run = Arc::clone(&gate);
|
|
let run_task = tokio::spawn(async move {
|
|
gate_for_run
|
|
.run(async move {
|
|
started_tx.send(()).expect("receiver should be open");
|
|
let _ = finish_rx.await;
|
|
})
|
|
.await;
|
|
});
|
|
|
|
started_rx.await.expect("run should start");
|
|
let gate_for_shutdown = Arc::clone(&gate);
|
|
let shutdown_task = tokio::spawn(async move {
|
|
gate_for_shutdown.shutdown().await;
|
|
});
|
|
|
|
timeout(Duration::from_millis(/*millis*/ 50), shutdown_task)
|
|
.await
|
|
.expect_err("shutdown should wait for the running future");
|
|
|
|
let late_polled = Arc::new(AtomicBool::new(/*v*/ false));
|
|
let late_polled_clone = Arc::clone(&late_polled);
|
|
gate.run(async move {
|
|
late_polled_clone.store(/*val*/ true, Ordering::Release);
|
|
})
|
|
.await;
|
|
|
|
assert!(!late_polled.load(Ordering::Acquire));
|
|
|
|
finish_tx
|
|
.send(())
|
|
.expect("running future should still be waiting");
|
|
run_task.await.expect("run task should complete");
|
|
gate.shutdown().await;
|
|
assert_eq!(gate.inflight_count(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn run_is_counted_before_handler_body_continues() {
|
|
let gate = Arc::new(ConnectionRpcGate::new());
|
|
let (entered_tx, entered_rx) = oneshot::channel();
|
|
let (continue_tx, continue_rx) = oneshot::channel();
|
|
let gate_for_run = Arc::clone(&gate);
|
|
let run_task = tokio::spawn(async move {
|
|
gate_for_run
|
|
.run(async move {
|
|
entered_tx.send(()).expect("receiver should be open");
|
|
let _ = continue_rx.await;
|
|
})
|
|
.await;
|
|
});
|
|
|
|
entered_rx.await.expect("handler body should be entered");
|
|
assert_eq!(gate.inflight_count(), 1);
|
|
|
|
continue_tx
|
|
.send(())
|
|
.expect("handler body should still be waiting");
|
|
run_task.await.expect("run task should complete");
|
|
assert_eq!(gate.inflight_count(), 0);
|
|
}
|
|
}
|