use std::path::Path;
use tracing::warn;
use crate::util::UnwrapPoison;
static BOOT_DIAGNOSES: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, BootDiagnosis>>,
> = std::sync::OnceLock::new();
fn boot_diagnoses()
-> &'static std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, BootDiagnosis>> {
BOOT_DIAGNOSES.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub(crate) fn set_boot_diagnosis(db_path: &Path, diagnosis: BootDiagnosis) {
boot_diagnoses()
.lock()
.unwrap_poison()
.insert(db_path.to_path_buf(), diagnosis);
}
#[must_use]
pub(crate) fn take_boot_diagnosis(db_path: &Path) -> Option<BootDiagnosis> {
boot_diagnoses().lock().unwrap_poison().remove(db_path)
}
#[must_use]
pub(crate) fn has_boot_diagnosis(db_path: &Path) -> bool {
boot_diagnoses()
.lock()
.unwrap_poison()
.contains_key(db_path)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BootDiagnosis {
Healthy,
DurableB,
Structural,
}
impl BootDiagnosis {
#[must_use]
pub(crate) fn label(self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::DurableB => "durable-b",
Self::Structural => "structural",
}
}
#[must_use]
fn into_store_class(self) -> StoreClass {
match self {
Self::Healthy => StoreClass::Healthy,
Self::DurableB => StoreClass::DurableB,
Self::Structural => StoreClass::Structural,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum StoreClass {
Healthy,
DurableB,
Structural,
}
impl StoreClass {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Healthy => "healthy",
Self::DurableB => "durable-b",
Self::Structural => "structural",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct StoreArtifactStatus {
pub store: String,
pub class: StoreClass,
pub wal_size: u64,
pub has_stale_tshm: bool,
}
#[must_use]
pub(super) fn inspect_store_at(db_path: &Path) -> StoreArtifactStatus {
let sidecars = crate::db::store_sidecars(db_path);
let wal_size = std::fs::metadata(&sidecars.wal).map_or(0, |m| m.len());
let has_stale_tshm = sidecars.tshm.exists();
let class = classify_main_db(db_path, sidecars.wal.exists(), wal_size).into_store_class();
let store = db_path.file_stem().map_or_else(
|| db_path.display().to_string(),
|s| s.to_string_lossy().into_owned(),
);
StoreArtifactStatus {
store,
class,
wal_size,
has_stale_tshm,
}
}
#[must_use]
pub(super) fn inspect_store(root: &Path, name: &str) -> StoreArtifactStatus {
inspect_store_at(&crate::db::store_db_path(root, name))
}
const DB_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
pub(crate) const DB_HEADER_MIN_SIZE: u64 = 100;
pub(crate) fn read_db_header(db_path: &Path) -> Option<[u8; 18]> {
use std::io::Read;
let mut header = [0u8; 18];
let mut file = std::fs::File::open(db_path).ok()?;
file.read_exact(&mut header).ok()?;
Some(header)
}
#[must_use]
pub(crate) fn db_header_valid(header: &[u8; 18]) -> bool {
if &header[..16] != DB_HEADER_MAGIC {
return false;
}
let raw = u16::from_be_bytes([header[16], header[17]]);
let page_size = if raw == 1 { 65_536 } else { u32::from(raw) };
(512..=65_536).contains(&page_size) && page_size.is_power_of_two()
}
fn classify_main_db(db_path: &Path, wal_exists: bool, wal_size: u64) -> BootDiagnosis {
let Ok(meta) = std::fs::metadata(db_path) else {
return BootDiagnosis::Healthy; };
let size = meta.len();
if size == 0 {
return if wal_exists && wal_size > 0 {
BootDiagnosis::DurableB
} else {
BootDiagnosis::Healthy
};
}
if size < DB_HEADER_MIN_SIZE {
return BootDiagnosis::Structural;
}
let Some(header) = read_db_header(db_path) else {
return BootDiagnosis::Healthy;
};
if db_header_valid(&header) {
BootDiagnosis::Healthy
} else {
BootDiagnosis::Structural
}
}
pub fn diagnose_all_stores(root: &Path) {
for (name, _) in crate::db::iter_checkpoint_stores() {
let db_path = crate::db::store_db_path(root, name);
let sidecars = crate::db::store_sidecars(&db_path);
let wal_size = std::fs::metadata(&sidecars.wal).map_or(0, |m| m.len());
let diagnosis = classify_main_db(&db_path, sidecars.wal.exists(), wal_size);
set_boot_diagnosis(&db_path, diagnosis);
if diagnosis != BootDiagnosis::Healthy {
crate::boot::boot_diagnostic(format!(
"boot pre-flight: store '{name}' class {} (wal_size={}) — healing will run \
before open",
diagnosis.label(),
wal_size,
));
}
}
}
pub fn cleanup_stale_tshm(root: &Path) {
for (name, _) in crate::db::iter_checkpoint_stores() {
let tshm = crate::db::store_sidecars(&crate::db::store_db_path(root, name)).tshm;
if !tshm.exists() {
continue;
}
match std::fs::remove_file(&tshm) {
Ok(()) => warn!(
db = %name,
path = %tshm.display(),
"removed stale .tshm coordination leftover from a pre-removal multiprocess run",
),
Err(e) => warn!(
db = %name,
path = %tshm.display(),
error = %e,
"failed to remove stale .tshm leftover",
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn write(path: &std::path::Path, bytes: &[u8]) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, bytes).unwrap();
}
#[test]
fn db_header_valid_decodes_64k_page_size() {
let mut header = [0u8; 18];
header[..16].copy_from_slice(b"SQLite format 3\0");
header[16..18].copy_from_slice(&1u16.to_be_bytes());
assert!(db_header_valid(&header));
header[0] = b'X';
assert!(!db_header_valid(&header));
}
#[test]
fn inspect_store_classifies_synthetic_file_sets() {
let dir = std::env::temp_dir().join(format!("wal_guard_state_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut db = vec![0u8; 4096];
db[..16].copy_from_slice(b"SQLite format 3\0");
db[16..18].copy_from_slice(&1u16.to_be_bytes());
write(&dir.join("db/core.db"), &db);
let s = inspect_store(&dir, "board");
assert_eq!(s.class, StoreClass::Healthy);
assert!(!s.has_stale_tshm);
write(&dir.join("db/core.db"), &[0u8; 64]);
let s = inspect_store(&dir, "sessions");
assert_eq!(s.class, StoreClass::Structural);
write(&dir.join("db/core.db"), &[]);
write(&dir.join("db/core.db-wal"), &[0u8; 512]);
let s = inspect_store(&dir, "users");
assert_eq!(s.class, StoreClass::DurableB);
let _ = std::fs::remove_file(&dir.join("db/core.db"));
let s = inspect_store(&dir, "config");
assert_eq!(s.class, StoreClass::Healthy);
write(&dir.join("db/core.db-tshm"), &[0u8; 32]);
let s = inspect_store(&dir, "chat_history");
assert!(s.has_stale_tshm);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn inspect_store_visits_every_store() {
let dir = std::env::temp_dir().join(format!("wal_guard_all_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let mut db = vec![0u8; 4096];
db[..16].copy_from_slice(b"SQLite format 3\0");
db[16] = 0x10; write(&dir.join("db/core.db"), &db);
write(&dir.join("db/logs.db"), &db);
for name in crate::db::store_names() {
let s = inspect_store(&dir, name);
assert_eq!(
s.class,
StoreClass::Healthy,
"fixture store {name} must be healthy"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn diagnose_all_stores_classifies_structural_durable_b_and_healthy() {
let dir = std::env::temp_dir().join(format!("wal_guard_preflight_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
write(&dir.join("db/core.db"), &[0u8; 64]);
crate::db::wal_guard::diagnose_all_stores(&dir);
assert_eq!(
crate::db::wal_guard::take_boot_diagnosis(&crate::db::store_db_path(&dir, "board")),
Some(BootDiagnosis::Structural)
);
write(&dir.join("db/core.db"), &[]);
write(&dir.join("db/core.db-wal"), &[0u8; 512]);
crate::db::wal_guard::diagnose_all_stores(&dir);
assert_eq!(
crate::db::wal_guard::take_boot_diagnosis(&crate::db::store_db_path(&dir, "sessions")),
Some(BootDiagnosis::DurableB)
);
let _ = std::fs::remove_file(&dir.join("db/core.db-wal"));
let mut db = vec![0u8; 4096];
db[..16].copy_from_slice(b"SQLite format 3\0");
db[16..18].copy_from_slice(&1u16.to_be_bytes());
write(&dir.join("db/core.db"), &db);
crate::db::wal_guard::diagnose_all_stores(&dir);
assert_eq!(
crate::db::wal_guard::take_boot_diagnosis(&crate::db::store_db_path(
&dir,
"chat_history"
)),
Some(BootDiagnosis::Healthy)
);
let _ = std::fs::remove_file(&dir.join("db/core.db"));
crate::db::wal_guard::diagnose_all_stores(&dir);
assert_eq!(
crate::db::wal_guard::take_boot_diagnosis(&crate::db::store_db_path(&dir, "users")),
Some(BootDiagnosis::Healthy)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn cleanup_stale_tshm_removes_only_tshm_not_wal() {
let dir = std::env::temp_dir().join(format!("wal_guard_cleanup_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let db_dir = dir.join("db");
write(&db_dir.join("core.db"), &[0u8; 4096]);
write(&db_dir.join("core.db-tshm"), &[0u8; 64]);
write(&db_dir.join("core.db-wal"), &[0xAA; 512]);
write(&db_dir.join("logs.db-tshm"), &[0u8; 64]);
crate::db::wal_guard::cleanup_stale_tshm(&dir);
assert!(
!db_dir.join("core.db-tshm").exists(),
"stale .tshm should be removed"
);
assert!(
!db_dir.join("logs.db-tshm").exists(),
"stale .tshm should be removed"
);
assert!(
db_dir.join("core.db-wal").exists(),
"-wal must never be removed (class-A commit-loss footgun)"
);
assert_eq!(
std::fs::metadata(db_dir.join("core.db-wal")).unwrap().len(),
512,
"-wal contents must be untouched"
);
let _ = std::fs::remove_dir_all(&dir);
}
}