Files
codex/codex-rs/tui/src/updates.rs
T
Celia Chen 8bbdf6c8f9 Use the shared HTTP client for TUI network checks (#35821)
## Why

TUI update checks and local OSS provider detection constructed their own HTTP
clients instead of using Codex's shared client behavior.

## What changed

- Route update requests through the configured route-aware client pool while
  retaining the existing default headers and custom CA fallback.
- Probe the hardcoded LM Studio and Ollama loopback endpoints with a shared
  direct client and a per-request timeout.
- Limit the legacy invalid-custom-CA fallback to the default routing policy so
  system-proxy routing still reports certificate configuration errors.

## Testing

Add coverage for local provider probes with invalid `CODEX_CA_CERTIFICATE` and
`SSL_CERT_FILE` values, and for custom CA fallback under both routing policies.

GitOrigin-RevId: b5c230b61e8964b3f1af3395052361ff716d6ce1
2026-07-28 19:58:25 +00:00

165 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![cfg(not(debug_assertions))]
use crate::legacy_core::config::Config;
use crate::npm_registry;
use crate::npm_registry::NpmPackageInfo;
use crate::update_action;
use crate::update_action::UpdateAction;
use crate::update_versions::extract_version_from_latest_tag;
use crate::update_versions::is_newer;
use crate::update_versions::is_source_build_version;
use crate::updates_cache::VersionInfo;
use crate::updates_cache::read_version_info;
use crate::updates_cache::version_filepath;
use chrono::Duration;
use chrono::Utc;
use codex_http_client::ClientRouteClass;
use codex_http_client::HttpClientFactory;
use codex_http_client::RouteAwareClientPool;
use codex_login::default_client::default_headers;
use serde::Deserialize;
use std::path::Path;
use crate::version::CODEX_CLI_VERSION;
pub(crate) use crate::updates_cache::dismiss_version;
pub fn get_upgrade_version(config: &Config) -> Option<String> {
if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) {
return None;
}
let action = update_action::get_update_action();
let version_file = version_filepath(config);
let info = read_version_info(&version_file).ok();
if match &info {
None => true,
Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20),
} {
let http_client_factory = config.http_client_factory();
// Refresh the cached latest version in the background so TUI startup
// isn’t blocked by a network call. The UI reads the previously cached
// value (if any) for this run; the next run shows the banner if needed.
tokio::spawn(async move {
check_for_update(&version_file, action, http_client_factory)
.await
.inspect_err(|e| tracing::error!("Failed to update version: {e}"))
});
}
info.and_then(|info| {
if is_newer(&info.latest_version, CODEX_CLI_VERSION).unwrap_or(false) {
Some(info.latest_version)
} else {
None
}
})
}
// We use the latest version from the cask if installation is via homebrew - homebrew does not immediately pick up the latest release and can lag behind.
const HOMEBREW_CASK_API_URL: &str = "https://formulae.brew.sh/api/cask/codex.json";
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
#[derive(Deserialize, Debug, Clone)]
struct ReleaseInfo {
tag_name: String,
}
#[derive(Deserialize, Debug, Clone)]
struct HomebrewCaskInfo {
version: String,
}
async fn check_for_update(
version_file: &Path,
action: Option<UpdateAction>,
http_client_factory: HttpClientFactory,
) -> anyhow::Result<()> {
let client_pool = RouteAwareClientPool::with_chatgpt_cloudflare_cookies(
http_client_factory,
ClientRouteClass::Other,
)
.with_legacy_custom_ca_fallback();
let latest_version = match action {
Some(UpdateAction::BrewUpgrade) => {
let HomebrewCaskInfo { version } = client_pool
.get(HOMEBREW_CASK_API_URL)
.headers(default_headers())
.send()
.await?
.error_for_status()?
.json::<HomebrewCaskInfo>()
.await?;
version
}
Some(UpdateAction::NpmGlobalLatest)
| Some(UpdateAction::BunGlobalLatest)
| Some(UpdateAction::PnpmGlobalLatest) => {
let latest_version = fetch_latest_github_release_version(&client_pool).await?;
let package_info = client_pool
.get(npm_registry::PACKAGE_URL)
.headers(default_headers())
.send()
.await?
.error_for_status()?
.json::<NpmPackageInfo>()
.await?;
npm_registry::ensure_version_ready(&package_info, &latest_version)?;
latest_version
}
Some(UpdateAction::StandaloneUnix) | Some(UpdateAction::StandaloneWindows) | None => {
fetch_latest_github_release_version(&client_pool).await?
}
};
// Preserve any previously dismissed version if present.
let prev_info = read_version_info(version_file).ok();
let info = VersionInfo {
latest_version,
last_checked_at: Utc::now(),
dismissed_version: prev_info.and_then(|p| p.dismissed_version),
};
let json_line = format!("{}\n", serde_json::to_string(&info)?);
if let Some(parent) = version_file.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(version_file, json_line).await?;
Ok(())
}
async fn fetch_latest_github_release_version(
client_pool: &RouteAwareClientPool,
) -> anyhow::Result<String> {
let ReleaseInfo {
tag_name: latest_tag_name,
} = client_pool
.get(LATEST_RELEASE_URL)
.headers(default_headers())
.send()
.await?
.error_for_status()?
.json::<ReleaseInfo>()
.await?;
extract_version_from_latest_tag(&latest_tag_name)
}
/// Returns the latest version to show in a popup, if it should be shown.
/// This respects the user's dismissal choice for the current latest version.
pub fn get_upgrade_version_for_popup(config: &Config) -> Option<String> {
if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) {
return None;
}
let version_file = version_filepath(config);
let latest = get_upgrade_version(config)?;
// If the user dismissed this exact version previously, do not show the popup.
if let Ok(info) = read_version_info(&version_file)
&& info.dismissed_version.as_deref() == Some(latest.as_str())
{
return None;
}
Some(latest)
}