Skip to content

Commit 5af4513

Browse files
committed
fallback in all errors
1 parent fa898ba commit 5af4513

3 files changed

Lines changed: 119 additions & 85 deletions

File tree

codex-rs/core/src/plugins/manager.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ use std::sync::RwLock;
6060
use std::sync::atomic::AtomicBool;
6161
use std::sync::atomic::Ordering;
6262
use std::time::Instant;
63+
use tokio::sync::Mutex;
6364
use toml_edit::value;
6465
use tracing::info;
6566
use tracing::warn;
@@ -463,6 +464,7 @@ pub struct PluginsManager {
463464
store: PluginStore,
464465
featured_plugin_ids_cache: RwLock<Option<CachedFeaturedPluginIds>>,
465466
cached_enabled_outcome: RwLock<Option<PluginLoadOutcome>>,
467+
remote_sync_lock: Mutex<()>,
466468
restriction_product: Option<Product>,
467469
analytics_events_client: RwLock<Option<AnalyticsEventsClient>>,
468470
}
@@ -488,6 +490,7 @@ impl PluginsManager {
488490
store: PluginStore::new(codex_home),
489491
featured_plugin_ids_cache: RwLock::new(None),
490492
cached_enabled_outcome: RwLock::new(None),
493+
remote_sync_lock: Mutex::new(()),
491494
restriction_product,
492495
analytics_events_client: RwLock::new(None),
493496
}
@@ -777,6 +780,8 @@ impl PluginsManager {
777780
auth: Option<&CodexAuth>,
778781
additive_only: bool,
779782
) -> Result<RemotePluginSyncResult, PluginRemoteSyncError> {
783+
let _remote_sync_guard = self.remote_sync_lock.lock().await;
784+
780785
if !config.features.enabled(Feature::Plugins) {
781786
return Ok(RemotePluginSyncResult::default());
782787
}

codex-rs/core/src/plugins/startup_sync.rs

Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ struct GitHubGitRefObject {
4545
sha: String,
4646
}
4747

48-
enum GitCuratedRepoSyncError {
49-
GitUnavailable(String),
50-
SyncFailed(String),
51-
}
52-
5348
pub(crate) fn curated_plugins_repo_path(codex_home: &Path) -> PathBuf {
5449
codex_home.join(CURATED_PLUGINS_RELATIVE_DIR)
5550
}
@@ -69,22 +64,18 @@ fn sync_openai_plugins_repo_with_transport_overrides(
6964
) -> Result<String, String> {
7065
match sync_openai_plugins_repo_via_git(codex_home, git_binary) {
7166
Ok(remote_sha) => Ok(remote_sha),
72-
Err(GitCuratedRepoSyncError::GitUnavailable(err)) => {
67+
Err(err) => {
7368
warn!(
7469
error = %err,
7570
git_binary,
76-
"git unavailable for curated plugin sync; falling back to GitHub HTTP"
71+
"git sync failed for curated plugin sync; falling back to GitHub HTTP"
7772
);
7873
sync_openai_plugins_repo_via_http(codex_home, api_base_url)
7974
}
80-
Err(GitCuratedRepoSyncError::SyncFailed(err)) => Err(err),
8175
}
8276
}
8377

84-
fn sync_openai_plugins_repo_via_git(
85-
codex_home: &Path,
86-
git_binary: &str,
87-
) -> Result<String, GitCuratedRepoSyncError> {
78+
fn sync_openai_plugins_repo_via_git(codex_home: &Path, git_binary: &str) -> Result<String, String> {
8879
let repo_path = curated_plugins_repo_path(codex_home);
8980
let sha_path = codex_home.join(CURATED_PLUGINS_SHA_FILE);
9081
let remote_sha = git_ls_remote_head_sha(git_binary)?;
@@ -94,8 +85,7 @@ fn sync_openai_plugins_repo_via_git(
9485
return Ok(remote_sha);
9586
}
9687

97-
let cloned_repo_path = prepare_curated_repo_parent_and_temp_dir(&repo_path)
98-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
88+
let cloned_repo_path = prepare_curated_repo_parent_and_temp_dir(&repo_path)?;
9989
let clone_output = run_git_command_with_timeout(
10090
Command::new(git_binary)
10191
.env("GIT_OPTIONAL_LOCKS", "0")
@@ -107,22 +97,18 @@ fn sync_openai_plugins_repo_via_git(
10797
"git clone curated plugins repo",
10898
CURATED_PLUGINS_GIT_TIMEOUT,
10999
)?;
110-
ensure_git_success(&clone_output, "git clone curated plugins repo")
111-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
100+
ensure_git_success(&clone_output, "git clone curated plugins repo")?;
112101

113102
let cloned_sha = git_head_sha(&cloned_repo_path, git_binary)?;
114103
if cloned_sha != remote_sha {
115-
return Err(GitCuratedRepoSyncError::SyncFailed(format!(
104+
return Err(format!(
116105
"curated plugins clone HEAD mismatch: expected {remote_sha}, got {cloned_sha}"
117-
)));
106+
));
118107
}
119108

120-
ensure_marketplace_manifest_exists(&cloned_repo_path)
121-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
122-
activate_curated_repo(&repo_path, &cloned_repo_path)
123-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
124-
write_curated_plugins_sha(&sha_path, &remote_sha)
125-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
109+
ensure_marketplace_manifest_exists(&cloned_repo_path)?;
110+
activate_curated_repo(&repo_path, &cloned_repo_path)?;
111+
write_curated_plugins_sha(&sha_path, &remote_sha)?;
126112
Ok(remote_sha)
127113
}
128114

@@ -363,7 +349,7 @@ fn read_local_git_or_sha_file(
363349
read_sha_file(sha_path)
364350
}
365351

366-
fn git_ls_remote_head_sha(git_binary: &str) -> Result<String, GitCuratedRepoSyncError> {
352+
fn git_ls_remote_head_sha(git_binary: &str) -> Result<String, String> {
367353
let output = run_git_command_with_timeout(
368354
Command::new(git_binary)
369355
.env("GIT_OPTIONAL_LOCKS", "0")
@@ -373,55 +359,45 @@ fn git_ls_remote_head_sha(git_binary: &str) -> Result<String, GitCuratedRepoSync
373359
"git ls-remote curated plugins repo",
374360
CURATED_PLUGINS_GIT_TIMEOUT,
375361
)?;
376-
ensure_git_success(&output, "git ls-remote curated plugins repo")
377-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
362+
ensure_git_success(&output, "git ls-remote curated plugins repo")?;
378363

379364
let stdout = String::from_utf8_lossy(&output.stdout);
380365
let Some(first_line) = stdout.lines().next() else {
381-
return Err(GitCuratedRepoSyncError::SyncFailed(
382-
"git ls-remote returned empty output for curated plugins repo".to_string(),
383-
));
366+
return Err("git ls-remote returned empty output for curated plugins repo".to_string());
384367
};
385368
let Some((sha, _)) = first_line.split_once('\t') else {
386-
return Err(GitCuratedRepoSyncError::SyncFailed(format!(
369+
return Err(format!(
387370
"unexpected git ls-remote output for curated plugins repo: {first_line}"
388-
)));
371+
));
389372
};
390373
if sha.is_empty() {
391-
return Err(GitCuratedRepoSyncError::SyncFailed(
392-
"git ls-remote returned empty sha for curated plugins repo".to_string(),
393-
));
374+
return Err("git ls-remote returned empty sha for curated plugins repo".to_string());
394375
}
395376
Ok(sha.to_string())
396377
}
397378

398-
fn git_head_sha(repo_path: &Path, git_binary: &str) -> Result<String, GitCuratedRepoSyncError> {
379+
fn git_head_sha(repo_path: &Path, git_binary: &str) -> Result<String, String> {
399380
let output = Command::new(git_binary)
400381
.env("GIT_OPTIONAL_LOCKS", "0")
401382
.arg("-C")
402383
.arg(repo_path)
403384
.arg("rev-parse")
404385
.arg("HEAD")
405386
.output()
406-
.map_err(|err| match err.kind() {
407-
std::io::ErrorKind::NotFound => GitCuratedRepoSyncError::GitUnavailable(format!(
408-
"failed to run git rev-parse HEAD in {}: {err}",
409-
repo_path.display()
410-
)),
411-
_ => GitCuratedRepoSyncError::SyncFailed(format!(
387+
.map_err(|err| {
388+
format!(
412389
"failed to run git rev-parse HEAD in {}: {err}",
413390
repo_path.display()
414-
)),
391+
)
415392
})?;
416-
ensure_git_success(&output, "git rev-parse HEAD")
417-
.map_err(GitCuratedRepoSyncError::SyncFailed)?;
393+
ensure_git_success(&output, "git rev-parse HEAD")?;
418394

419395
let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
420396
if sha.is_empty() {
421-
return Err(GitCuratedRepoSyncError::SyncFailed(format!(
397+
return Err(format!(
422398
"git rev-parse HEAD returned empty output in {}",
423399
repo_path.display()
424-
)));
400+
));
425401
}
426402
Ok(sha)
427403
}
@@ -430,71 +406,49 @@ fn run_git_command_with_timeout(
430406
command: &mut Command,
431407
context: &str,
432408
timeout: Duration,
433-
) -> Result<Output, GitCuratedRepoSyncError> {
409+
) -> Result<Output, String> {
434410
let mut child = command
435411
.stdin(Stdio::null())
436412
.stdout(Stdio::piped())
437413
.stderr(Stdio::piped())
438414
.spawn()
439-
.map_err(|err| match err.kind() {
440-
std::io::ErrorKind::NotFound => {
441-
GitCuratedRepoSyncError::GitUnavailable(format!("failed to run {context}: {err}"))
442-
}
443-
_ => GitCuratedRepoSyncError::SyncFailed(format!("failed to run {context}: {err}")),
444-
})?;
415+
.map_err(|err| format!("failed to run {context}: {err}"))?;
445416

446417
let start = std::time::Instant::now();
447418
loop {
448419
match child.try_wait() {
449420
Ok(Some(_)) => {
450-
return child.wait_with_output().map_err(|err| {
451-
GitCuratedRepoSyncError::SyncFailed(format!(
452-
"failed to wait for {context}: {err}"
453-
))
454-
});
421+
return child
422+
.wait_with_output()
423+
.map_err(|err| format!("failed to wait for {context}: {err}"));
455424
}
456425
Ok(None) => {}
457-
Err(err) => {
458-
return Err(GitCuratedRepoSyncError::SyncFailed(format!(
459-
"failed to poll {context}: {err}"
460-
)));
461-
}
426+
Err(err) => return Err(format!("failed to poll {context}: {err}")),
462427
}
463428

464429
if start.elapsed() >= timeout {
465430
match child.try_wait() {
466431
Ok(Some(_)) => {
467-
return child.wait_with_output().map_err(|err| {
468-
GitCuratedRepoSyncError::SyncFailed(format!(
469-
"failed to wait for {context}: {err}"
470-
))
471-
});
432+
return child
433+
.wait_with_output()
434+
.map_err(|err| format!("failed to wait for {context}: {err}"));
472435
}
473436
Ok(None) => {}
474-
Err(err) => {
475-
return Err(GitCuratedRepoSyncError::SyncFailed(format!(
476-
"failed to poll {context}: {err}"
477-
)));
478-
}
437+
Err(err) => return Err(format!("failed to poll {context}: {err}")),
479438
}
480439

481440
let _ = child.kill();
482-
let output = child.wait_with_output().map_err(|err| {
483-
GitCuratedRepoSyncError::SyncFailed(format!(
484-
"failed to wait for {context} after timeout: {err}"
485-
))
486-
})?;
441+
let output = child
442+
.wait_with_output()
443+
.map_err(|err| format!("failed to wait for {context} after timeout: {err}"))?;
487444
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
488445
return if stderr.is_empty() {
489-
Err(GitCuratedRepoSyncError::SyncFailed(format!(
490-
"{context} timed out after {}s",
491-
timeout.as_secs()
492-
)))
446+
Err(format!("{context} timed out after {}s", timeout.as_secs()))
493447
} else {
494-
Err(GitCuratedRepoSyncError::SyncFailed(format!(
448+
Err(format!(
495449
"{context} timed out after {}s: {stderr}",
496450
timeout.as_secs()
497-
)))
451+
))
498452
};
499453
}
500454

codex-rs/core/src/plugins/startup_sync_tests.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,81 @@ async fn sync_openai_plugins_repo_falls_back_to_http_when_git_is_unavailable() {
154154
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
155155
}
156156

157+
#[cfg(unix)]
158+
#[tokio::test]
159+
async fn sync_openai_plugins_repo_falls_back_to_http_when_git_sync_fails() {
160+
use std::os::unix::fs::PermissionsExt;
161+
162+
let tmp = tempdir().expect("tempdir");
163+
let bin_dir = tempfile::Builder::new()
164+
.prefix("fake-git-fail-")
165+
.tempdir()
166+
.expect("tempdir");
167+
let git_path = bin_dir.path().join("git");
168+
let sha = "0123456789abcdef0123456789abcdef01234567";
169+
170+
std::fs::write(
171+
&git_path,
172+
r#"#!/bin/sh
173+
echo "simulated git failure" >&2
174+
exit 1
175+
"#,
176+
)
177+
.expect("write fake git");
178+
let mut permissions = std::fs::metadata(&git_path)
179+
.expect("metadata")
180+
.permissions();
181+
permissions.set_mode(0o755);
182+
std::fs::set_permissions(&git_path, permissions).expect("chmod");
183+
184+
let server = MockServer::start().await;
185+
Mock::given(method("GET"))
186+
.and(path("/repos/openai/plugins"))
187+
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"default_branch":"main"}"#))
188+
.mount(&server)
189+
.await;
190+
Mock::given(method("GET"))
191+
.and(path("/repos/openai/plugins/git/ref/heads/main"))
192+
.respond_with(
193+
ResponseTemplate::new(200)
194+
.set_body_string(format!(r#"{{"object":{{"sha":"{sha}"}}}}"#)),
195+
)
196+
.mount(&server)
197+
.await;
198+
Mock::given(method("GET"))
199+
.and(path(format!("/repos/openai/plugins/zipball/{sha}")))
200+
.respond_with(
201+
ResponseTemplate::new(200)
202+
.insert_header("content-type", "application/zip")
203+
.set_body_bytes(curated_repo_zipball_bytes(sha)),
204+
)
205+
.mount(&server)
206+
.await;
207+
208+
let server_uri = server.uri();
209+
let tmp_path = tmp.path().to_path_buf();
210+
let synced_sha = tokio::task::spawn_blocking(move || {
211+
sync_openai_plugins_repo_with_transport_overrides(
212+
tmp_path.as_path(),
213+
git_path.to_str().expect("utf8 path"),
214+
&server_uri,
215+
)
216+
})
217+
.await
218+
.expect("sync task should join")
219+
.expect("fallback sync should succeed");
220+
221+
let repo_path = curated_plugins_repo_path(tmp.path());
222+
assert_eq!(synced_sha, sha);
223+
assert!(repo_path.join(".agents/plugins/marketplace.json").is_file());
224+
assert!(
225+
repo_path
226+
.join("plugins/gmail/.codex-plugin/plugin.json")
227+
.is_file()
228+
);
229+
assert_eq!(read_curated_plugins_sha(tmp.path()).as_deref(), Some(sha));
230+
}
231+
157232
#[tokio::test]
158233
async fn sync_openai_plugins_repo_skips_archive_download_when_sha_matches() {
159234
let tmp = tempdir().expect("tempdir");

0 commit comments

Comments
 (0)