diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index e025be4fd8..0aafe1c233 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -456,13 +456,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result } set_default_client_residency_requirement(config.enforce_residency.value()); - refresh_managed_chatgpt_token_for_storage_if_near_expiry( - config.codex_home.to_path_buf(), - /*enable_codex_api_key_env*/ false, - config.cli_auth_credentials_store_mode, - config.chatgpt_base_url.clone(), - ) - .await; if let Err(err) = enforce_login_restrictions(&AuthConfig { codex_home: config.codex_home.to_path_buf(), @@ -477,6 +470,14 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result std::process::exit(1); } + refresh_managed_chatgpt_token_for_storage_if_near_expiry( + config.codex_home.to_path_buf(), + /*enable_codex_api_key_env*/ true, + config.cli_auth_credentials_store_mode, + config.chatgpt_base_url.clone(), + ) + .await; + let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { codex_core::otel_init::build_provider( &config, diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 7e0dac9762..f4c9285a6f 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -1751,13 +1751,13 @@ impl AuthManager { return Ok(()); } + let _refresh_lock = self.acquire_chatgpt_startup_refresh_lock().await?; let _refresh_guard = self.refresh_lock.acquire().await.map_err(|_| { RefreshTokenError::Permanent(RefreshTokenFailedError::new( RefreshTokenFailedReason::Other, REFRESH_TOKEN_UNKNOWN_MESSAGE.to_string(), )) })?; - let _refresh_lock = self.acquire_chatgpt_startup_refresh_lock().await?; self.refresh_token_with_refresh_lock_held().await } diff --git a/codex-rs/login/tests/suite/auth_refresh.rs b/codex-rs/login/tests/suite/auth_refresh.rs index 759204dff6..64af10eb86 100644 --- a/codex-rs/login/tests/suite/auth_refresh.rs +++ b/codex-rs/login/tests/suite/auth_refresh.rs @@ -320,6 +320,81 @@ async fn refresh_managed_chatgpt_token_waits_while_startup_refresh_lock_is_held( Ok(()) } +#[serial_test::serial(auth_refresh)] +#[tokio::test] +async fn refresh_token_does_not_wait_while_startup_refresh_lock_is_held() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/oauth/token")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "access_token": "new-access-token", + "refresh_token": "new-refresh-token" + }))) + .expect(1) + .mount(&server) + .await; + + let ctx = RefreshTokenTestContext::new(&server).await?; + let initial_last_refresh = Utc::now(); + let expired_access_token = access_token_with_expiration(Utc::now() - Duration::minutes(1)); + let initial_tokens = build_tokens(&expired_access_token, INITIAL_REFRESH_TOKEN); + ctx.write_auth(&AuthDotJson { + auth_mode: Some(AuthMode::Chatgpt), + openai_api_key: None, + tokens: Some(initial_tokens), + last_refresh: Some(initial_last_refresh), + agent_identity: None, + }) + .await?; + + let lock_path = ctx + .codex_home + .path() + .join("chatgpt-access-token-startup-refresh.lock"); + let lock_file = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(lock_path)?; + lock_file.try_lock()?; + + let auth_manager = Arc::clone(&ctx.auth_manager); + let startup_refresh_task = tokio::spawn(async move { + auth_manager + .refresh_managed_chatgpt_token_if_near_expiry() + .await + }); + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + assert!( + !startup_refresh_task.is_finished(), + "startup refresh should wait while another process holds the file lock" + ); + + tokio::time::timeout( + std::time::Duration::from_secs(1), + ctx.auth_manager.refresh_token_from_authority(), + ) + .await + .context("normal refresh should not wait for the startup file lock")? + .context("normal refresh should succeed")?; + + startup_refresh_task.abort(); + let _ = startup_refresh_task.await; + drop(lock_file); + + let stored = ctx.load_auth()?; + let tokens = stored.tokens.as_ref().context("tokens should exist")?; + assert_eq!(tokens.access_token, "new-access-token"); + assert_eq!(tokens.refresh_token, "new-refresh-token"); + server.verify().await; + + Ok(()) +} + #[serial_test::serial(auth_refresh)] #[tokio::test] async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b25b887447..1d83dd0580 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1141,13 +1141,6 @@ pub async fn run_main( } set_default_client_residency_requirement(config.enforce_residency.value()); - refresh_managed_chatgpt_token_for_storage_if_near_expiry( - config.codex_home.to_path_buf(), - /*enable_codex_api_key_env*/ false, - config.cli_auth_credentials_store_mode, - config.chatgpt_base_url.clone(), - ) - .await; if let Some(warning) = add_dir_warning_message( &cli.add_dir, @@ -1175,6 +1168,14 @@ pub async fn run_main( eprintln!("{err}"); std::process::exit(1); } + + refresh_managed_chatgpt_token_for_storage_if_near_expiry( + config.codex_home.to_path_buf(), + /*enable_codex_api_key_env*/ false, + config.cli_auth_credentials_store_mode, + config.chatgpt_base_url.clone(), + ) + .await; } let log_dir = config.log_dir.clone();