mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
171 lines
6.5 KiB
Rust
171 lines
6.5 KiB
Rust
//! CLI handling for local state database startup failures.
|
|
//!
|
|
//! This keeps user-facing backup and lock-contention handling out of the main
|
|
//! CLI dispatch path while preserving the TUI startup error as the boundary type.
|
|
|
|
use codex_state::RuntimeDbBackup;
|
|
use codex_tui::LocalStateDbStartupError;
|
|
use std::io::IsTerminal;
|
|
use std::path::Path;
|
|
|
|
pub(crate) fn startup_error(err: &std::io::Error) -> Option<&LocalStateDbStartupError> {
|
|
err.get_ref()
|
|
.and_then(|err| err.downcast_ref::<LocalStateDbStartupError>())
|
|
}
|
|
|
|
pub(crate) fn is_locked(detail: &str) -> bool {
|
|
codex_state::sqlite_error_detail_is_lock(detail)
|
|
}
|
|
|
|
pub(crate) fn is_corruption(detail: &str) -> bool {
|
|
codex_state::sqlite_error_detail_is_corruption(detail)
|
|
}
|
|
|
|
pub(crate) fn is_migration_incompatible(detail: &str) -> bool {
|
|
codex_state::sqlite_error_detail_is_migration_incompatible(detail)
|
|
}
|
|
|
|
pub(crate) fn is_auto_backup_recoverable(startup_error: &LocalStateDbStartupError) -> bool {
|
|
is_corruption(startup_error.detail()) || sqlite_home_is_blocking_file(startup_error)
|
|
}
|
|
|
|
fn sqlite_home_is_blocking_file(startup_error: &LocalStateDbStartupError) -> bool {
|
|
startup_error
|
|
.database_path()
|
|
.parent()
|
|
.and_then(|path| std::fs::metadata(path).ok())
|
|
.is_some_and(|metadata| metadata.is_file())
|
|
}
|
|
|
|
pub(crate) fn print_auto_backup_start(startup_error: &LocalStateDbStartupError) {
|
|
eprintln!("Codex couldn't start because its local database appears to be damaged.");
|
|
eprintln!("Moving the damaged local database aside so Codex can rebuild it from saved data.");
|
|
print_technical_details(startup_error);
|
|
}
|
|
|
|
pub(crate) async fn backup_files_for_fresh_start(
|
|
startup_error: &LocalStateDbStartupError,
|
|
) -> std::io::Result<Vec<RuntimeDbBackup>> {
|
|
codex_state::backup_runtime_db_for_fresh_start(startup_error.database_path()).await
|
|
}
|
|
|
|
pub(crate) fn confirm_fresh_start_rebuild(
|
|
startup_error: &LocalStateDbStartupError,
|
|
backups: &[RuntimeDbBackup],
|
|
) -> std::io::Result<()> {
|
|
eprintln!("Codex rebuilt its local database.");
|
|
eprintln!(
|
|
"Codex detected a damaged local database, moved it into a backup folder, and will continue startup with a fresh database."
|
|
);
|
|
eprintln!("Database path: {}", startup_error.database_path().display());
|
|
if let Some(backup_folder) = backup_folder(backups) {
|
|
eprintln!("Backup folder: {}", backup_folder.display());
|
|
} else {
|
|
eprintln!("Backup folder: unavailable");
|
|
}
|
|
|
|
if std::io::stdin().is_terminal() && std::io::stderr().is_terminal() {
|
|
eprintln!("Press Enter to continue.");
|
|
let mut input = String::new();
|
|
std::io::stdin().read_line(&mut input)?;
|
|
} else {
|
|
eprintln!("Continuing startup with a fresh local database...");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn print_diagnostic_guidance(startup_error: &LocalStateDbStartupError) {
|
|
if is_migration_incompatible(startup_error.detail()) {
|
|
eprintln!(
|
|
"Codex couldn't start because its local database migration history is incompatible with this version."
|
|
);
|
|
eprintln!("Run `codex doctor --fix` to back up and rebuild the affected database.");
|
|
} else {
|
|
eprintln!("Codex couldn't start because its local database appears to be damaged.");
|
|
eprintln!("Run `codex doctor` to check your setup and get next-step guidance.");
|
|
}
|
|
eprintln!("If this keeps happening, share the technical details below when asking for help.");
|
|
print_technical_details(startup_error);
|
|
}
|
|
|
|
pub(crate) fn print_locked_guidance(startup_error: &LocalStateDbStartupError) {
|
|
eprintln!("Codex couldn't start because another Codex process is using its local data.");
|
|
eprintln!("Quit any other copies of Codex that may still be running, then try again.");
|
|
print_technical_details(startup_error);
|
|
}
|
|
|
|
fn print_technical_details(startup_error: &LocalStateDbStartupError) {
|
|
eprintln!("Technical details:");
|
|
eprintln!(" Location: {}", startup_error.database_path().display());
|
|
eprintln!(" Cause: {}", startup_error.detail());
|
|
}
|
|
|
|
fn backup_folder(backups: &[RuntimeDbBackup]) -> Option<&Path> {
|
|
backups.first()?.backup_path.parent()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
#[tokio::test]
|
|
async fn backup_backs_up_only_failed_database_file() -> std::io::Result<()> {
|
|
let temp_dir = TempDir::new()?;
|
|
let state_path = codex_state::state_db_path(temp_dir.path());
|
|
let failed_db_path = codex_state::logs_db_path(temp_dir.path());
|
|
tokio::fs::write(state_path.as_path(), b"state").await?;
|
|
tokio::fs::write(failed_db_path.as_path(), b"logs").await?;
|
|
|
|
let startup_error =
|
|
LocalStateDbStartupError::new(failed_db_path.clone(), "corrupt".to_string());
|
|
let backups = backup_files_for_fresh_start(&startup_error).await?;
|
|
|
|
assert_eq!(
|
|
backups
|
|
.iter()
|
|
.map(|backup| &backup.original_path)
|
|
.collect::<Vec<_>>(),
|
|
vec![&failed_db_path]
|
|
);
|
|
assert!(!tokio::fs::try_exists(failed_db_path.as_path()).await?);
|
|
assert!(tokio::fs::try_exists(state_path.as_path()).await?);
|
|
assert!(tokio::fs::try_exists(backups[0].backup_path.as_path()).await?);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn backup_replaces_blocking_sqlite_home_file() -> std::io::Result<()> {
|
|
let temp_dir = TempDir::new()?;
|
|
let sqlite_home = temp_dir.path().join("sqlite-home");
|
|
tokio::fs::write(sqlite_home.as_path(), b"not-a-directory").await?;
|
|
let startup_error = LocalStateDbStartupError::new(
|
|
codex_state::state_db_path(sqlite_home.as_path()),
|
|
"File exists".to_string(),
|
|
);
|
|
|
|
assert!(is_auto_backup_recoverable(&startup_error));
|
|
let backups = backup_files_for_fresh_start(&startup_error).await?;
|
|
|
|
assert_eq!(backups.len(), 1);
|
|
assert!(tokio::fs::metadata(sqlite_home.as_path()).await?.is_dir());
|
|
assert!(tokio::fs::try_exists(backups[0].backup_path.as_path()).await?);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn backup_folder_uses_parent_of_first_backup_path() {
|
|
let backups = vec![RuntimeDbBackup {
|
|
original_path: PathBuf::from("/tmp/state_5.sqlite"),
|
|
backup_path: PathBuf::from("/tmp/db-backups/sqlite-1-0/state_5.sqlite"),
|
|
}];
|
|
|
|
assert_eq!(
|
|
backup_folder(&backups),
|
|
Some(Path::new("/tmp/db-backups/sqlite-1-0"))
|
|
);
|
|
}
|
|
}
|