use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result, ensure};
use crate::db::test_support::{PROBES, probe_store, sqlite3_available};
async fn verify() -> Result<String> {
crate::config::load_or_init().await?;
let root = crate::config::CONFIG
.try_storage_root()
.context("verify: config::load_or_init() did not set the storage root")?;
let store_dir = super::store_dir(&root);
ensure!(
!store_dir.exists(),
"refusing to run against an existing store root ({}) — this check writes to the \
root it is given and must be pointed at a fresh hermetic $HOME",
store_dir.display(),
);
crate::boot::open_stores().await?;
let conn = super::DOMAIN_CONN
.get()
.context("the consolidated store connection is not initialized")?;
let snapshot = super::snapshot_store_via_engine(
conn,
&super::store_db_path(&root, super::CONSOLIDATED_DB_NAME),
)
.await?;
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
for _ in 0..3 {
crate::db::checkpoint::periodic_checkpoint_and_verify().await;
}
let mut gated = Vec::new();
for (name, store_conn) in super::iter_checkpoint_stores() {
let Some(store_conn) = store_conn else {
continue;
};
ensure!(
crate::db::shrink_gate::shrink_allowed(store_conn, name, Some(&root)).await,
"the pre-shrink gate refused a healthy store ({name})",
);
gated.push(name);
}
let before = dir_fingerprint(&store_dir)?;
let probes = if sqlite3_available() {
let mut refusals = Vec::new();
for (name, _) in super::iter_checkpoint_stores() {
let store = super::store_db_path(&root, name);
for (kind, sql) in PROBES {
refusals.push(format!("{name} {kind}: {}", probe_store(&store, sql)?));
}
}
refusals.join("\n")
} else {
"foreign-client probes SKIPPED: no stock sqlite3 on PATH".to_string()
};
let after = dir_fingerprint(&store_dir)?;
ensure!(
before == after,
"the store file set (or a main store file) changed under the foreign probes:\n \
before: {before:?}\n after: {after:?}",
);
Ok(format!(
"store-lock check: ok on {}\nsnapshot: {}\npre-shrink gate allowed on {}\n{probes}\nstore directory unchanged ({})",
std::env::consts::OS,
snapshot.display(),
gated.join(", "),
store_dir.display(),
))
}
#[derive(Debug, PartialEq)]
struct EntryFingerprint {
name: String,
size: Option<u64>,
mtime: Option<std::time::SystemTime>,
}
fn dir_fingerprint(dir: &Path) -> Result<Vec<EntryFingerprint>> {
let mut fingerprint = Vec::new();
for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
let entry = entry?;
let metadata = entry.metadata()?;
let name = entry.file_name().to_string_lossy().into_owned();
let main = Path::new(&name).extension().is_some_and(|e| e == "db");
let (size, mtime) = if main {
(Some(metadata.len()), metadata.modified().ok())
} else {
(None, None)
};
fingerprint.push(EntryFingerprint { name, size, mtime });
}
fingerprint.sort_by(|a, b| a.name.cmp(&b.name));
Ok(fingerprint)
}
#[cfg(test)]
mod tests {
use std::os::fd::AsRawFd;
use super::*;
use crate::util::UnwrapPoison;
const CHILD_ENV: &str = "MAHBOT_STORE_LOCK_CHECK_CHILD";
const BOOT_CHILD_ENV: &str = "MAHBOT_STORE_BOOT_CHILD";
fn child(home: &tempfile::TempDir) -> Command {
let mut command = Command::new(std::env::current_exe().expect("test binary path"));
command.env("HOME", home.path());
command
}
fn boot_stores(home: &tempfile::TempDir) -> std::process::Output {
child(home)
.args(["--ignored", "--nocapture", "store_boot_child"])
.env(BOOT_CHILD_ENV, "1")
.output()
.expect("spawn the store boot child")
}
fn core_family(dir: &Path) -> Result<Vec<EntryFingerprint>> {
Ok(dir_fingerprint(dir)?
.into_iter()
.filter(|entry| entry.name.starts_with("core.db"))
.collect())
}
fn child_text(output: &std::process::Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
)
}
fn hold_store_lock(store: &Path) -> std::fs::File {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(store)
.expect("open the store to lock");
let mut lock: libc::flock = unsafe { std::mem::zeroed() };
lock.l_type = libc::c_short::try_from(libc::F_WRLCK).expect("F_WRLCK fits a c_short");
lock.l_whence = libc::c_short::try_from(libc::SEEK_SET).expect("SEEK_SET fits a c_short");
let locked = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock) };
assert_eq!(
locked,
0,
"taking the store lock failed: {}",
std::io::Error::last_os_error()
);
file
}
#[test]
fn live_stores_stay_locked_for_a_foreign_client() {
let _env = crate::util::test::env_lock().lock().unwrap_poison();
let home = tempfile::TempDir::new().expect("hermetic home");
let output = child(&home)
.args(["--ignored", "--nocapture", "store_lock_check_child"])
.env(CHILD_ENV, "1")
.output()
.expect("spawn the child store-lock check");
let text = child_text(&output);
assert!(
output.status.success(),
"store-lock check failed ({}):\n{text}",
output.status,
);
assert!(
text.contains("store-lock check: ok"),
"the separate process must report the check's evidence:\n{text}"
);
}
#[test]
fn a_store_held_by_another_process_refuses_the_boot() {
let _env = crate::util::test::env_lock().lock().unwrap_poison();
let home = tempfile::TempDir::new().expect("hermetic home");
let created = boot_stores(&home);
let text = String::from_utf8_lossy(&created.stdout).into_owned();
assert!(
created.status.success(),
"the stores must be created first:\n{text}"
);
let store_dir = home.path().join(".mahbot/db");
let _holder = hold_store_lock(&store_dir.join("core.db"));
let before = core_family(&store_dir).expect("fingerprint the core store");
let refused = boot_stores(&home);
let text = child_text(&refused);
assert!(
!refused.status.success(),
"a store another process holds must refuse the boot:\n{text}"
);
assert!(
text.to_lowercase().contains("locked"),
"the refusal must name the lock, not something else:\n{text}"
);
assert!(
text.contains("refusing to start: a store is locked by another process"),
"the refusal must be RECORDED on the boot-diagnostic channel (stderr), not only \
returned to the GUI:\n{text}"
);
assert_eq!(
before,
core_family(&store_dir).expect("fingerprint the core store"),
"a store that cannot be opened must be left exactly as it is",
);
}
#[test]
fn an_unusable_store_file_refuses_the_boot() {
let _env = crate::util::test::env_lock().lock().unwrap_poison();
let home = tempfile::TempDir::new().expect("hermetic home");
let root = home.path().join(".mahbot");
let store_dir = root.join("db");
std::fs::create_dir_all(&store_dir).expect("create the store dir");
std::fs::write(store_dir.join("core.db"), []).expect("write the empty store");
let before = core_family(&store_dir).expect("fingerprint the core store");
let refused = boot_stores(&home);
let text = child_text(&refused);
assert!(
!refused.status.success(),
"an unusable store file must refuse the boot:\n{text}"
);
assert!(
text.contains("refusing to start: store 'core' is not usable"),
"the refusal must name the store and the reason:\n{text}"
);
assert!(
!store_dir.join("logs.db").exists(),
"the gate must run before the logs store is opened — nothing may be created",
);
assert_eq!(
before,
core_family(&store_dir).expect("fingerprint the core store"),
"the refused store must be left completely untouched",
);
let record = std::fs::read_to_string(root.join("error.log")).expect("error.log");
assert!(
record.contains("MahBot start-up refusal")
&& record.contains("store: core")
&& record.contains("the data file is empty"),
"the refusal must be recorded durably:\n{record}"
);
}
#[test]
fn an_unreadable_store_file_refuses_the_boot_as_an_environment_cause() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
println!("skipped: running as root, mode 000 does not deny access");
return;
}
let _env = crate::util::test::env_lock().lock().unwrap_poison();
let home = tempfile::TempDir::new().expect("hermetic home");
let root = home.path().join(".mahbot");
let store_dir = root.join("db");
std::fs::create_dir_all(&store_dir).expect("create the store dir");
let core = store_dir.join("core.db");
std::fs::write(&core, [0x42; 512]).expect("write the store");
std::fs::set_permissions(&core, std::fs::Permissions::from_mode(0o000))
.expect("make the store unreadable");
let before = core_family(&store_dir).expect("fingerprint the core store");
let refused = boot_stores(&home);
let text = child_text(&refused);
assert!(
!refused.status.success(),
"a store the process cannot read must refuse the boot:\n{text}"
);
assert!(
text.contains("refusing to start: store 'core' is not usable"),
"the refusal must name the store and the reason:\n{text}"
);
assert_eq!(
before,
core_family(&store_dir).expect("fingerprint the core store"),
"the refused store must be left completely untouched",
);
let record = std::fs::read_to_string(root.join("error.log")).expect("error.log written");
assert!(
record.contains("MahBot start-up refusal")
&& record.contains("store: core")
&& record.contains(crate::db::failure_record::ENVIRONMENT_CAUSE),
"a refusal caused by the environment must say so, never read as damage:\n{record}"
);
}
#[test]
fn both_unusable_stores_are_named_in_the_durable_record() {
let _env = crate::util::test::env_lock().lock().unwrap_poison();
let home = tempfile::TempDir::new().expect("hermetic home");
let root = home.path().join(".mahbot");
let store_dir = root.join("db");
std::fs::create_dir_all(&store_dir).expect("create the store dir");
std::fs::write(store_dir.join("core.db"), []).expect("write the empty core store");
std::fs::write(store_dir.join("logs.db"), [0x42; 128]).expect("write the bad logs store");
let before = dir_fingerprint(&store_dir).expect("fingerprint the store dir");
let refused = boot_stores(&home);
let text = child_text(&refused);
assert!(
!refused.status.success(),
"unusable stores must refuse the boot:\n{text}"
);
for store in ["core", "logs"] {
assert!(
text.contains(&format!("refusing to start: store '{store}' is not usable")),
"the refusal must name the {store} store:\n{text}"
);
}
let record = std::fs::read_to_string(root.join("error.log")).expect("error.log");
assert!(
record.contains("store: core") && record.contains("store: logs"),
"the durable record must name both damaged stores:\n{record}"
);
assert_eq!(
before,
dir_fingerprint(&store_dir).expect("fingerprint the store dir"),
"both refused stores must be left completely untouched",
);
}
#[test]
#[ignore = "driven by a_store_held_by_another_process_refuses_the_boot"]
fn store_boot_child() {
if std::env::var_os(BOOT_CHILD_ENV).is_none() {
return;
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime");
let booted = runtime.block_on(async {
crate::config::load_or_init().await?;
crate::boot::open_stores().await.map(|_| ())
});
match booted {
Ok(()) => println!("store boot: ok"),
Err(e) => panic!("store boot FAILED: {e:#}"),
}
}
#[test]
#[ignore = "driven by the driver above, in a child process"]
fn store_lock_check_child() {
if std::env::var_os(CHILD_ENV).is_none() {
return; }
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime");
let evidence = runtime
.block_on(verify())
.unwrap_or_else(|e| panic!("store-lock check FAILED: {e:#}"));
println!("{evidence}");
}
}