merge: upstream sync via merge/upstream-2025-08-19 (prefer ours)

This commit is contained in:
James Peter
2025-08-19 11:53:25 +10:00
9 changed files with 189 additions and 210 deletions
+8 -1
View File
@@ -162,13 +162,20 @@ jobs:
base="$(basename "$f")" base="$(basename "$f")"
# Skip files that are already archives (shouldn't happen, but be # Skip files that are already archives (shouldn't happen, but be
# safe). # safe).
if [[ "$base" == *.tar.gz ]]; then if [[ "$base" == *.tar.gz || "$base" == *.zip ]]; then
continue continue
fi fi
# Create per-binary tar.gz # Create per-binary tar.gz
tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base"
# Create zip archive for Windows binaries
# Must run from inside the dest dir so 7z won't
# embed the directory path inside the zip.
if [[ "${{ matrix.runner }}" == windows* ]]; then
(cd "$dest" && 7z a "${base}.zip" "$base")
fi
# Also create .zst (existing behaviour) *and* remove the original # Also create .zst (existing behaviour) *and* remove the original
# uncompressed binary to keep the directory small. # uncompressed binary to keep the directory small.
zstd -T0 -19 --rm "$dest/$base" zstd -T0 -19 --rm "$dest/$base"
+12 -12
View File
@@ -176,9 +176,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@@ -630,9 +630,9 @@ checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.43" version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -640,9 +640,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.43" version = "4.5.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -653,18 +653,18 @@ dependencies = [
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "4.5.56" version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e4efcbb5da11a92e8a609233aa1e8a7d91e38de0be865f016d14700d45a7fd" checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.41" version = "4.5.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2814,9 +2814,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.174" version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
+2 -2
View File
@@ -14,14 +14,14 @@ use std::path::PathBuf;
pub async fn login_with_chatgpt(codex_home: PathBuf) -> std::io::Result<()> { pub async fn login_with_chatgpt(codex_home: PathBuf) -> std::io::Result<()> {
let opts = ServerOptions::new(codex_home, CLIENT_ID.to_string()); let opts = ServerOptions::new(codex_home, CLIENT_ID.to_string());
let server = run_login_server(opts, None)?; let server = run_login_server(opts)?;
eprintln!( eprintln!(
"Starting local login server on http://localhost:{}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{}", "Starting local login server on http://localhost:{}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{}",
server.actual_port, server.auth_url, server.actual_port, server.auth_url,
); );
server.block_until_done() server.block_until_done().await
} }
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! { pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
+1 -1
View File
@@ -20,7 +20,7 @@ clap = { version = "4", features = ["derive"] }
codex-common = { path = "../common", features = ["cli"] } codex-common = { path = "../common", features = ["cli"] }
codex-core = { path = "../core" } codex-core = { path = "../core" }
landlock = "0.4.1" landlock = "0.4.1"
libc = "0.2.172" libc = "0.2.175"
seccompiler = "0.5.0" seccompiler = "0.5.0"
[target.'cfg(target_os = "linux")'.dev-dependencies] [target.'cfg(target_os = "linux")'.dev-dependencies]
+119 -155
View File
@@ -3,11 +3,7 @@ use std::io::{self};
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::Duration;
use crate::AuthDotJson; use crate::AuthDotJson;
use crate::get_auth_file; use crate::get_auth_file;
@@ -32,7 +28,6 @@ pub struct ServerOptions {
pub port: u16, pub port: u16,
pub open_browser: bool, pub open_browser: bool,
pub force_state: Option<String>, pub force_state: Option<String>,
pub login_timeout: Option<Duration>,
} }
impl ServerOptions { impl ServerOptions {
@@ -44,7 +39,6 @@ impl ServerOptions {
port: DEFAULT_PORT, port: DEFAULT_PORT,
open_browser: true, open_browser: true,
force_state: None, force_state: None,
login_timeout: None,
} }
} }
} }
@@ -52,52 +46,38 @@ impl ServerOptions {
pub struct LoginServer { pub struct LoginServer {
pub auth_url: String, pub auth_url: String,
pub actual_port: u16, pub actual_port: u16,
pub server_handle: thread::JoinHandle<io::Result<()>>, server_handle: tokio::task::JoinHandle<io::Result<()>>,
pub shutdown_flag: Arc<AtomicBool>, shutdown_handle: ShutdownHandle,
pub server: Arc<Server>,
} }
impl LoginServer { impl LoginServer {
pub fn block_until_done(self) -> io::Result<()> { pub async fn block_until_done(self) -> io::Result<()> {
#[expect(clippy::expect_used)]
self.server_handle self.server_handle
.join() .await
.expect("can't join on the server thread") .map_err(|err| io::Error::other(format!("login server thread panicked: {err:?}")))?
} }
pub fn cancel(&self) { pub fn cancel(&self) {
shutdown(&self.shutdown_flag, &self.server); self.shutdown_handle.shutdown();
} }
pub fn cancel_handle(&self) -> ShutdownHandle { pub fn cancel_handle(&self) -> ShutdownHandle {
ShutdownHandle { self.shutdown_handle.clone()
shutdown_flag: self.shutdown_flag.clone(),
server: self.server.clone(),
}
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct ShutdownHandle { pub struct ShutdownHandle {
shutdown_flag: Arc<AtomicBool>, shutdown_notify: Arc<tokio::sync::Notify>,
server: Arc<Server>,
} }
impl ShutdownHandle { impl ShutdownHandle {
pub fn cancel(&self) { pub fn shutdown(&self) {
shutdown(&self.shutdown_flag, &self.server); self.shutdown_notify.notify_waiters();
} }
} }
pub fn shutdown(shutdown_flag: &AtomicBool, server: &Server) { pub fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer> {
shutdown_flag.store(true, Ordering::SeqCst);
server.unblock();
}
pub fn run_login_server(
opts: ServerOptions,
shutdown_flag: Option<Arc<AtomicBool>>,
) -> io::Result<LoginServer> {
let pkce = generate_pkce(); let pkce = generate_pkce();
let state = opts.force_state.clone().unwrap_or_else(generate_state); let state = opts.force_state.clone().unwrap_or_else(generate_state);
@@ -119,75 +99,71 @@ pub fn run_login_server(
if opts.open_browser { if opts.open_browser {
let _ = webbrowser::open(&auth_url); let _ = webbrowser::open(&auth_url);
} }
let shutdown_flag = shutdown_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
let shutdown_flag_clone = shutdown_flag.clone();
let timeout_flag = Arc::new(AtomicBool::new(false));
// Channel used to signal completion to timeout watcher. // Map blocking reads from server.recv() to an async channel.
let (done_tx, done_rx) = mpsc::channel::<()>(); let (tx, mut rx) = tokio::sync::mpsc::channel::<Request>(16);
let _server_handle = {
let server = server.clone();
thread::spawn(move || -> io::Result<()> {
while let Ok(request) = server.recv() {
tx.blocking_send(request).map_err(|e| {
eprintln!("Failed to send request to channel: {e}");
io::Error::other("Failed to send request to channel")
})?;
}
Ok(())
})
};
if let Some(timeout) = opts.login_timeout { let shutdown_notify = Arc::new(tokio::sync::Notify::new());
spawn_timeout_watcher( let server_handle = {
done_rx, let shutdown_notify = shutdown_notify.clone();
timeout, let server = server.clone();
shutdown_flag.clone(), tokio::spawn(async move {
timeout_flag.clone(), let result = loop {
server.clone(), tokio::select! {
); _ = shutdown_notify.notified() => {
} break Err(io::Error::other("Login was not completed"));
}
maybe_req = rx.recv() => {
let Some(req) = maybe_req else {
break Err(io::Error::other("Login was not completed"));
};
let server_for_thread = server.clone(); let url_raw = req.url().to_string();
let server_handle = thread::spawn(move || { let response =
while !shutdown_flag.load(Ordering::SeqCst) { process_request(&url_raw, &opts, &redirect_uri, &pkce, actual_port, &state).await;
let req = match server_for_thread.recv() {
Ok(r) => r, let is_login_complete = matches!(response, HandledRequest::ResponseAndExit(_));
Err(e) => { match response {
// If we've been asked to shut down, break gracefully so that HandledRequest::Response(r) | HandledRequest::ResponseAndExit(r) => {
// we can report timeout or cancellation status uniformly. let _ = tokio::task::spawn_blocking(move || req.respond(r)).await;
if shutdown_flag.load(Ordering::SeqCst) { }
break; HandledRequest::RedirectWithHeader(header) => {
} else { let redirect = Response::empty(302).with_header(header);
return Err(io::Error::other(e)); let _ = tokio::task::spawn_blocking(move || req.respond(redirect)).await;
}
}
if is_login_complete {
break Ok(());
}
} }
} }
}; };
let response = process_request(&req, &opts, &redirect_uri, &pkce, actual_port, &state); // Ensure that the server is unblocked so the thread dedicated to
let is_login_complete = matches!(response, HandledRequest::ResponseAndExit(_)); // running `server.recv()` in a loop exits cleanly.
match response { server.unblock();
HandledRequest::Response(r) | HandledRequest::ResponseAndExit(r) => { result
let _ = req.respond(r); })
} };
HandledRequest::RedirectWithHeader(header) => {
let redirect = Response::empty(302).with_header(header);
let _ = req.respond(redirect);
}
}
if is_login_complete {
shutdown_flag.store(true, Ordering::SeqCst);
// Login has succeeded, so disarm the timeout watcher.
let _ = done_tx.send(());
return Ok(());
}
}
// Login has failed or timed out, so disarm the timeout watcher.
let _ = done_tx.send(());
if timeout_flag.load(Ordering::SeqCst) {
Err(io::Error::other("Login timed out"))
} else {
Err(io::Error::other("Login was not completed"))
}
});
Ok(LoginServer { Ok(LoginServer {
auth_url: auth_url.clone(), auth_url,
actual_port, actual_port,
server_handle, server_handle,
shutdown_flag: shutdown_flag_clone, shutdown_handle: ShutdownHandle { shutdown_notify },
server,
}) })
} }
@@ -197,15 +173,14 @@ enum HandledRequest {
ResponseAndExit(Response<Cursor<Vec<u8>>>), ResponseAndExit(Response<Cursor<Vec<u8>>>),
} }
fn process_request( async fn process_request(
req: &Request, url_raw: &str,
opts: &ServerOptions, opts: &ServerOptions,
redirect_uri: &str, redirect_uri: &str,
pkce: &PkceCodes, pkce: &PkceCodes,
actual_port: u16, actual_port: u16,
state: &str, state: &str,
) -> HandledRequest { ) -> HandledRequest {
let url_raw = req.url().to_string();
let parsed_url = match url::Url::parse(&format!("http://localhost{url_raw}")) { let parsed_url = match url::Url::parse(&format!("http://localhost{url_raw}")) {
Ok(u) => u, Ok(u) => u,
Err(e) => { Err(e) => {
@@ -236,18 +211,22 @@ fn process_request(
}; };
match exchange_code_for_tokens(&opts.issuer, &opts.client_id, redirect_uri, pkce, &code) match exchange_code_for_tokens(&opts.issuer, &opts.client_id, redirect_uri, pkce, &code)
.await
{ {
Ok(tokens) => { Ok(tokens) => {
// Obtain API key via token-exchange and persist // Obtain API key via token-exchange and persist
let api_key = let api_key = obtain_api_key(&opts.issuer, &opts.client_id, &tokens.id_token)
obtain_api_key(&opts.issuer, &opts.client_id, &tokens.id_token).ok(); .await
if let Err(err) = persist_tokens( .ok();
if let Err(err) = persist_tokens_async(
&opts.codex_home, &opts.codex_home,
api_key.clone(), api_key.clone(),
tokens.id_token.clone(), tokens.id_token.clone(),
Some(tokens.access_token.clone()), Some(tokens.access_token.clone()),
Some(tokens.refresh_token.clone()), Some(tokens.refresh_token.clone()),
) { )
.await
{
eprintln!("Persist error: {err}"); eprintln!("Persist error: {err}");
return HandledRequest::Response( return HandledRequest::Response(
Response::from_string(format!("Unable to persist auth file: {err}")) Response::from_string(format!("Unable to persist auth file: {err}"))
@@ -292,29 +271,6 @@ fn process_request(
} }
} }
/// Spawns a detached thread that waits for either a completion signal on `done_rx`
/// or the specified `timeout` to elapse. If the timeout elapses first it marks
/// the `shutdown_flag`, records `timeout_flag`, and unblocks the HTTP server so
/// that the main server loop can exit promptly.
fn spawn_timeout_watcher(
done_rx: mpsc::Receiver<()>,
timeout: Duration,
shutdown_flag: Arc<AtomicBool>,
timeout_flag: Arc<AtomicBool>,
server: Arc<Server>,
) {
thread::spawn(move || {
if done_rx.recv_timeout(timeout).is_err()
&& shutdown_flag
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
timeout_flag.store(true, Ordering::SeqCst);
server.unblock();
}
});
}
fn build_authorize_url( fn build_authorize_url(
issuer: &str, issuer: &str,
client_id: &str, client_id: &str,
@@ -353,7 +309,7 @@ struct ExchangedTokens {
refresh_token: String, refresh_token: String,
} }
fn exchange_code_for_tokens( async fn exchange_code_for_tokens(
issuer: &str, issuer: &str,
client_id: &str, client_id: &str,
redirect_uri: &str, redirect_uri: &str,
@@ -367,7 +323,7 @@ fn exchange_code_for_tokens(
refresh_token: String, refresh_token: String,
} }
let client = reqwest::blocking::Client::new(); let client = reqwest::Client::new();
let resp = client let resp = client
.post(format!("{issuer}/oauth/token")) .post(format!("{issuer}/oauth/token"))
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
@@ -379,6 +335,7 @@ fn exchange_code_for_tokens(
urlencoding::encode(&pkce.code_verifier) urlencoding::encode(&pkce.code_verifier)
)) ))
.send() .send()
.await
.map_err(io::Error::other)?; .map_err(io::Error::other)?;
if !resp.status().is_success() { if !resp.status().is_success() {
@@ -388,7 +345,7 @@ fn exchange_code_for_tokens(
))); )));
} }
let tokens: TokenResponse = resp.json().map_err(io::Error::other)?; let tokens: TokenResponse = resp.json().await.map_err(io::Error::other)?;
Ok(ExchangedTokens { Ok(ExchangedTokens {
id_token: tokens.id_token, id_token: tokens.id_token,
access_token: tokens.access_token, access_token: tokens.access_token,
@@ -396,43 +353,49 @@ fn exchange_code_for_tokens(
}) })
} }
fn persist_tokens( async fn persist_tokens_async(
codex_home: &Path, codex_home: &Path,
api_key: Option<String>, api_key: Option<String>,
id_token: String, id_token: String,
access_token: Option<String>, access_token: Option<String>,
refresh_token: Option<String>, refresh_token: Option<String>,
) -> io::Result<()> { ) -> io::Result<()> {
let auth_file = get_auth_file(codex_home); // Reuse existing synchronous logic but run it off the async runtime.
if let Some(parent) = auth_file.parent() { let codex_home = codex_home.to_path_buf();
if !parent.exists() { tokio::task::spawn_blocking(move || {
std::fs::create_dir_all(parent).map_err(io::Error::other)?; let auth_file = get_auth_file(&codex_home);
if let Some(parent) = auth_file.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(io::Error::other)?;
}
} }
}
let mut auth = read_or_default(&auth_file); let mut auth = read_or_default(&auth_file);
if let Some(key) = api_key { if let Some(key) = api_key {
auth.openai_api_key = Some(key); auth.openai_api_key = Some(key);
} }
let tokens = auth let tokens = auth
.tokens .tokens
.get_or_insert_with(crate::token_data::TokenData::default); .get_or_insert_with(crate::token_data::TokenData::default);
tokens.id_token = crate::token_data::parse_id_token(&id_token).map_err(io::Error::other)?; tokens.id_token = crate::token_data::parse_id_token(&id_token).map_err(io::Error::other)?;
// Persist chatgpt_account_id if present in claims // Persist chatgpt_account_id if present in claims
if let Some(acc) = jwt_auth_claims(&id_token) if let Some(acc) = jwt_auth_claims(&id_token)
.get("chatgpt_account_id") .get("chatgpt_account_id")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
{ {
tokens.account_id = Some(acc.to_string()); tokens.account_id = Some(acc.to_string());
} }
if let Some(at) = access_token { if let Some(at) = access_token {
tokens.access_token = at; tokens.access_token = at;
} }
if let Some(rt) = refresh_token { if let Some(rt) = refresh_token {
tokens.refresh_token = rt; tokens.refresh_token = rt;
} }
auth.last_refresh = Some(Utc::now()); auth.last_refresh = Some(Utc::now());
super::write_auth_json(&auth_file, &auth) super::write_auth_json(&auth_file, &auth)
})
.await
.map_err(|e| io::Error::other(format!("persist task failed: {e}")))?
} }
fn read_or_default(path: &Path) -> AuthDotJson { fn read_or_default(path: &Path) -> AuthDotJson {
@@ -525,13 +488,13 @@ fn jwt_auth_claims(jwt: &str) -> serde_json::Map<String, serde_json::Value> {
serde_json::Map::new() serde_json::Map::new()
} }
fn obtain_api_key(issuer: &str, client_id: &str, id_token: &str) -> io::Result<String> { async fn obtain_api_key(issuer: &str, client_id: &str, id_token: &str) -> io::Result<String> {
// Token exchange for an API key access token // Token exchange for an API key access token
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct ExchangeResp { struct ExchangeResp {
access_token: String, access_token: String,
} }
let client = reqwest::blocking::Client::new(); let client = reqwest::Client::new();
let resp = client let resp = client
.post(format!("{issuer}/oauth/token")) .post(format!("{issuer}/oauth/token"))
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
@@ -544,6 +507,7 @@ fn obtain_api_key(issuer: &str, client_id: &str, id_token: &str) -> io::Result<S
urlencoding::encode("urn:ietf:params:oauth:token-type:id_token") urlencoding::encode("urn:ietf:params:oauth:token-type:id_token")
)) ))
.send() .send()
.await
.map_err(io::Error::other)?; .map_err(io::Error::other)?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
@@ -551,6 +515,6 @@ fn obtain_api_key(issuer: &str, client_id: &str, id_token: &str) -> io::Result<S
resp.status() resp.status()
))); )));
} }
let body: ExchangeResp = resp.json().map_err(io::Error::other)?; let body: ExchangeResp = resp.json().await.map_err(io::Error::other)?;
Ok(body.access_token) Ok(body.access_token)
} }
+12 -14
View File
@@ -73,8 +73,8 @@ fn start_mock_issuer() -> (SocketAddr, thread::JoinHandle<()>) {
(addr, handle) (addr, handle)
} }
#[test] #[tokio::test]
fn end_to_end_login_flow_persists_auth_json() { async fn end_to_end_login_flow_persists_auth_json() {
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
println!( println!(
"Skipping test because it cannot execute when network is disabled in a Codex sandbox." "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
@@ -100,22 +100,21 @@ fn end_to_end_login_flow_persists_auth_json() {
port: 0, port: 0,
open_browser: false, open_browser: false,
force_state: Some(state), force_state: Some(state),
login_timeout: None,
}; };
let server = run_login_server(opts, None).unwrap(); let server = run_login_server(opts).unwrap();
let login_port = server.actual_port; let login_port = server.actual_port;
// Simulate browser callback, and follow redirect to /success // Simulate browser callback, and follow redirect to /success
let client = reqwest::blocking::Client::builder() let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::limited(5)) .redirect(reqwest::redirect::Policy::limited(5))
.build() .build()
.unwrap(); .unwrap();
let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123"); let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123");
let resp = client.get(&url).send().unwrap(); let resp = client.get(&url).send().await.unwrap();
assert!(resp.status().is_success()); assert!(resp.status().is_success());
// Wait for server shutdown // Wait for server shutdown
server.block_until_done().unwrap(); server.block_until_done().await.unwrap();
// Validate auth.json // Validate auth.json
let auth_path = codex_home.join("auth.json"); let auth_path = codex_home.join("auth.json");
@@ -133,8 +132,8 @@ fn end_to_end_login_flow_persists_auth_json() {
drop(issuer_handle); drop(issuer_handle);
} }
#[test] #[tokio::test]
fn creates_missing_codex_home_dir() { async fn creates_missing_codex_home_dir() {
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
println!( println!(
"Skipping test because it cannot execute when network is disabled in a Codex sandbox." "Skipping test because it cannot execute when network is disabled in a Codex sandbox."
@@ -159,17 +158,16 @@ fn creates_missing_codex_home_dir() {
port: 0, port: 0,
open_browser: false, open_browser: false,
force_state: Some(state), force_state: Some(state),
login_timeout: None,
}; };
let server = run_login_server(opts, None).unwrap(); let server = run_login_server(opts).unwrap();
let login_port = server.actual_port; let login_port = server.actual_port;
let client = reqwest::blocking::Client::new(); let client = reqwest::Client::new();
let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=state2"); let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=state2");
let resp = client.get(&url).send().unwrap(); let resp = client.get(&url).send().await.unwrap();
assert!(resp.status().is_success()); assert!(resp.status().is_success());
server.block_until_done().unwrap(); server.block_until_done().await.unwrap();
let auth_path = codex_home.join("auth.json"); let auth_path = codex_home.join("auth.json");
assert!( assert!(
@@ -68,7 +68,7 @@ struct ActiveLogin {
impl ActiveLogin { impl ActiveLogin {
fn drop(&self) { fn drop(&self) {
self.shutdown_handle.cancel(); self.shutdown_handle.shutdown();
} }
} }
+5
View File
@@ -221,6 +221,11 @@ impl TextArea {
code: KeyCode::Backspace, code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
.. ..
}
| KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
..
} => self.delete_backward(1), } => self.delete_backward(1),
KeyEvent { KeyEvent {
code: KeyCode::Delete, code: KeyCode::Delete,
+29 -24
View File
@@ -1,5 +1,6 @@
use codex_login::CLIENT_ID; use codex_login::CLIENT_ID;
use codex_login::ServerOptions; use codex_login::ServerOptions;
use codex_login::ShutdownHandle;
use codex_login::run_login_server; use codex_login::run_login_server;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
@@ -28,10 +29,6 @@ use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider; use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::shimmer::shimmer_spans; use crate::shimmer::shimmer_spans;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::thread::JoinHandle;
use super::onboarding_screen::StepState; use super::onboarding_screen::StepState;
// no additional imports // no additional imports
@@ -50,13 +47,14 @@ pub(crate) enum SignInState {
/// Used to manage the lifecycle of SpawnedLogin and ensure it gets cleaned up. /// Used to manage the lifecycle of SpawnedLogin and ensure it gets cleaned up.
pub(crate) struct ContinueInBrowserState { pub(crate) struct ContinueInBrowserState {
auth_url: String, auth_url: String,
shutdown_flag: Option<Arc<AtomicBool>>, shutdown_handle: Option<ShutdownHandle>,
_login_wait_handle: Option<JoinHandle<()>>, _login_wait_handle: Option<tokio::task::JoinHandle<()>>,
} }
impl Drop for ContinueInBrowserState { impl Drop for ContinueInBrowserState {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(flag) = &self.shutdown_flag { if let Some(flag) = &self.shutdown_handle {
flag.store(true, Ordering::SeqCst); flag.shutdown();
} }
} }
} }
@@ -305,16 +303,21 @@ impl AuthModeWidget {
fn start_chatgpt_login(&mut self) { fn start_chatgpt_login(&mut self) {
self.error = None; self.error = None;
let opts = ServerOptions::new(self.codex_home.clone(), CLIENT_ID.to_string()); let opts = ServerOptions::new(self.codex_home.clone(), CLIENT_ID.to_string());
let server = run_login_server(opts, None); let server = run_login_server(opts);
match server { match server {
Ok(child) => { Ok(child) => {
let auth_url = child.auth_url.clone(); let auth_url = child.auth_url.clone();
let shutdown_flag = child.shutdown_flag.clone(); let shutdown_handle = child.cancel_handle();
let event_tx = self.event_tx.clone();
let join_handle = tokio::spawn(async move {
spawn_completion_poller(child, event_tx).await;
});
self.sign_in_state = self.sign_in_state =
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState { SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
auth_url, auth_url,
shutdown_flag: Some(shutdown_flag), shutdown_handle: Some(shutdown_handle),
_login_wait_handle: Some(self.spawn_completion_poller(child)), _login_wait_handle: Some(join_handle),
}); });
self.event_tx.send(AppEvent::RequestRedraw); self.event_tx.send(AppEvent::RequestRedraw);
} }
@@ -335,19 +338,21 @@ impl AuthModeWidget {
} }
self.event_tx.send(AppEvent::RequestRedraw); self.event_tx.send(AppEvent::RequestRedraw);
} }
}
fn spawn_completion_poller(&self, child: codex_login::LoginServer) -> JoinHandle<()> { async fn spawn_completion_poller(
let event_tx = self.event_tx.clone(); child: codex_login::LoginServer,
std::thread::spawn(move || { event_tx: AppEventSender,
if let Ok(()) = child.block_until_done() { ) -> tokio::task::JoinHandle<()> {
event_tx.send(AppEvent::OnboardingAuthComplete(Ok(()))); tokio::spawn(async move {
} else { if let Ok(()) = child.block_until_done().await {
event_tx.send(AppEvent::OnboardingAuthComplete(Err( event_tx.send(AppEvent::OnboardingAuthComplete(Ok(())));
"login failed".to_string() } else {
))); event_tx.send(AppEvent::OnboardingAuthComplete(Err(
} "login failed".to_string()
}) )));
} }
})
} }
impl StepStateProvider for AuthModeWidget { impl StepStateProvider for AuthModeWidget {