Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
29
crates/codegen/xai-sqlite-journal/Cargo.toml
Normal file
29
crates/codegen/xai-sqlite-journal/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
license = "Apache-2.0"
|
||||
name = "xai-sqlite-journal"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
description = "Filesystem-aware SQLite journal-mode selection: WAL on local disks, rollback journal on network mounts where WAL's mmap'd -shm is unsafe"
|
||||
|
||||
[dependencies]
|
||||
# Matches the pinned rusqlite of every consumer (bundled = self-contained SQLite).
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
# Same per-crate pin as xai-crash-handler / xai-system-power / xai-grok-pager
|
||||
# (windows-sys has no workspace entry).
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_WindowsProgramming",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
779
crates/codegen/xai-sqlite-journal/src/lib.rs
Normal file
779
crates/codegen/xai-sqlite-journal/src/lib.rs
Normal file
|
|
@ -0,0 +1,779 @@
|
|||
//! Filesystem-aware SQLite journal-mode selection.
|
||||
//!
|
||||
//! WAL keeps its wal-index in an mmap'd `-shm` file and relies on coherent
|
||||
//! shared memory plus reliable POSIX locks — guarantees network filesystems
|
||||
//! do not provide. When `$HOME` (and thus `~/.grok`) is NFS-mounted on
|
||||
//! several machines at once, a peer host truncating/rebuilding the `-shm`
|
||||
//! during WAL recovery or close rips the backing out from under our mapping
|
||||
//! and the next wal-index read dies with SIGBUS. On such mounts we use a
|
||||
//! rollback journal instead (SQLite's documented "WAL does not work over a
|
||||
//! network filesystem" limitation), and each host opens its own per-host DB
|
||||
//! file (see [`JournalMode::effective_db_path`]) so no peer — including
|
||||
//! pre-fix binaries that would flip a shared DB back to WAL — ever shares
|
||||
//! the file.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Wait for peers' locks instead of failing instantly; matches what every
|
||||
/// consumer historically set.
|
||||
const BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(5000);
|
||||
|
||||
/// Journal mode chosen for a SQLite database based on where it lives.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum JournalMode {
|
||||
/// Write-ahead logging — the historical default, local filesystems only.
|
||||
Wal,
|
||||
/// Rollback journal truncated (not unlinked) at commit — safe on network
|
||||
/// filesystems, and cheaper there than DELETE mode: no per-commit
|
||||
/// create/unlink namespace round-trips and no NFS `.nfsXXXX`
|
||||
/// silly-rename litter.
|
||||
Truncate,
|
||||
}
|
||||
|
||||
impl JournalMode {
|
||||
/// Pick the journal mode for a database at `db_path`.
|
||||
///
|
||||
/// Classifies the parent directory (the DB file itself may not exist
|
||||
/// yet), so callers must create it first. `GROK_SQLITE_JOURNAL_MODE`
|
||||
/// (`wal`|`truncate`) overrides detection as a field kill-switch.
|
||||
pub fn for_db_path(db_path: &Path) -> Self {
|
||||
let env = std::env::var("GROK_SQLITE_JOURNAL_MODE").ok();
|
||||
match mode_from_env(env.as_deref()) {
|
||||
EnvOverride::Mode(mode) => {
|
||||
// Loud so field flips of the kill-switch are greppable in logs.
|
||||
tracing::info!(
|
||||
db = %db_path.display(),
|
||||
mode = mode.as_str(),
|
||||
source = "env",
|
||||
"sqlite journal mode forced by GROK_SQLITE_JOURNAL_MODE"
|
||||
);
|
||||
return mode;
|
||||
}
|
||||
EnvOverride::Invalid => {
|
||||
// A typo in the emergency kill-switch must be loud, not silently ignored.
|
||||
tracing::warn!(
|
||||
value = env.as_deref().unwrap_or_default(),
|
||||
"invalid GROK_SQLITE_JOURNAL_MODE (accepted: wal, truncate); using detection"
|
||||
);
|
||||
}
|
||||
EnvOverride::Unset => {}
|
||||
}
|
||||
let dir = match db_path.parent() {
|
||||
Some(p) if !p.as_os_str().is_empty() => p,
|
||||
// Bare filename (or root): classify the CWD it resolves against.
|
||||
_ => Path::new("."),
|
||||
};
|
||||
let mode = if is_network_fs(dir) {
|
||||
Self::Truncate
|
||||
} else {
|
||||
Self::Wal
|
||||
};
|
||||
tracing::debug!(
|
||||
db = %db_path.display(),
|
||||
mode = mode.as_str(),
|
||||
source = "statfs",
|
||||
"sqlite journal mode"
|
||||
);
|
||||
mode
|
||||
}
|
||||
|
||||
/// The `PRAGMA journal_mode` value for this mode.
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Wal => "WAL",
|
||||
Self::Truncate => "TRUNCATE",
|
||||
}
|
||||
}
|
||||
|
||||
/// The path actually opened for `db_path` under this mode.
|
||||
///
|
||||
/// `Wal` (local): unchanged. `Truncate` (network): a per-host sibling
|
||||
/// (`worktrees.db` → `worktrees.h-<host>.db`). Journal mode is a
|
||||
/// database-wide property, so a live pre-fix binary on a peer host (or
|
||||
/// this host) can flip a *shared* DB back to WAL at any time and our
|
||||
/// long-lived connections would silently adopt it, re-creating the
|
||||
/// mmap'd `-shm`. Old binaries never know the per-host name, so the
|
||||
/// no-WAL invariant — and the end of cross-host sharing, the root
|
||||
/// hazard — holds by construction. These DBs are all rebuildable
|
||||
/// indexes/caches, so each host starting fresh is acceptable.
|
||||
///
|
||||
/// Idempotent (an already-suffixed path is returned unchanged) so
|
||||
/// callers may pre-resolve the path for sidecar file operations. Falls
|
||||
/// back to `db_path` unchanged (still TRUNCATE) if no hostname is
|
||||
/// available.
|
||||
pub fn effective_db_path(self, db_path: &Path) -> PathBuf {
|
||||
if self != Self::Truncate {
|
||||
return db_path.to_path_buf();
|
||||
}
|
||||
let Some(host) = host_discriminator() else {
|
||||
return db_path.to_path_buf();
|
||||
};
|
||||
let Some(name) = db_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
else {
|
||||
return db_path.to_path_buf();
|
||||
};
|
||||
let tag = format!(".h-{host}");
|
||||
// Idempotent: pre-resolved paths pass through unchanged.
|
||||
if name.ends_with(&tag) || name.contains(&format!("{tag}.")) {
|
||||
return db_path.to_path_buf();
|
||||
}
|
||||
// Insert before the final extension ("worktrees.db" -> "worktrees.h-x.db"),
|
||||
// else append; rsplit keeps dotfile names like ".hidden" on the append arm.
|
||||
let new_name = match name.rsplit_once('.') {
|
||||
Some((stem, ext)) if !stem.is_empty() => format!("{stem}{tag}.{ext}"),
|
||||
_ => format!("{name}{tag}"),
|
||||
};
|
||||
db_path.with_file_name(new_name)
|
||||
}
|
||||
|
||||
/// Open (or create) a read-write connection with this journal mode
|
||||
/// applied, at [`Self::effective_db_path`] (per-host on network mounts).
|
||||
/// Sets a 5s `busy_timeout` before the journal pragma (see
|
||||
/// [`Self::apply`] for the conversion-lock semantics).
|
||||
pub fn open(self, db_path: &Path) -> rusqlite::Result<rusqlite::Connection> {
|
||||
let conn = rusqlite::Connection::open(self.effective_db_path(db_path))?;
|
||||
conn.busy_timeout(BUSY_TIMEOUT)?;
|
||||
self.apply(&conn)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Open a connection for read-only use, safely for this mode's
|
||||
/// filesystem, at [`Self::effective_db_path`] (per-host on network
|
||||
/// mounts — until a read-write open creates that file, this errors and
|
||||
/// callers fall back to their defaults). Errors if the database file
|
||||
/// does not exist (never creates it).
|
||||
///
|
||||
/// `Wal` (local): a plain read-only open — the historical behavior.
|
||||
///
|
||||
/// `Truncate` (network): reading a legacy WAL-stamped DB read-only would
|
||||
/// mmap its `-shm` (the SIGBUS), and the read-only escape hatch does not
|
||||
/// exist: EXCLUSIVE locking's heap wal-index takes an exclusive file lock,
|
||||
/// and POSIX forbids write-locking an O_RDONLY fd (`SQLITE_IOERR_LOCK`).
|
||||
/// So open read-write (without CREATE) and run the idempotent conversion
|
||||
/// (needed e.g. after remote sync drops in a WAL-stamped file). The fd
|
||||
/// stays writable (conversion and hot-journal rollback need it), but
|
||||
/// `query_only` is then set so SQL writes are rejected on this arm too —
|
||||
/// both arms honor the name.
|
||||
pub fn open_readonly(self, db_path: &Path) -> rusqlite::Result<rusqlite::Connection> {
|
||||
use rusqlite::OpenFlags;
|
||||
let flags = match self {
|
||||
Self::Wal => OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
// Read-write (no CREATE): the conversion needs a writable fd.
|
||||
Self::Truncate => OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
};
|
||||
let conn = rusqlite::Connection::open_with_flags(self.effective_db_path(db_path), flags)?;
|
||||
// Readers can hit SQLITE_BUSY too (peer recovery, a rollback writer's
|
||||
// exclusive window), and the conversion below requires a busy handler.
|
||||
conn.busy_timeout(BUSY_TIMEOUT)?;
|
||||
if let Self::Truncate = self {
|
||||
self.apply(&conn)?;
|
||||
// Make the name true: reject SQL writes (statement-level) while
|
||||
// the fd stays writable for lock/rollback purposes.
|
||||
conn.pragma_update(None, "query_only", true)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Apply this journal mode to a freshly opened read-write connection.
|
||||
///
|
||||
/// Busy semantics (single source of truth): converting a database between
|
||||
/// WAL and rollback journaling takes a brief exclusive lock whose
|
||||
/// acquisition only PARTIALLY honors the busy handler — some lock paths
|
||||
/// wait out `busy_timeout`, others (e.g. a peer connection holding a WAL
|
||||
/// read-mark) fail fast with `SQLITE_BUSY`. Callers must set
|
||||
/// `busy_timeout` first ([`Self::open`] does), and sites that swallow
|
||||
/// open errors need a bounded retry on busy as well (see
|
||||
/// `set_journal_mode` in `xai-fast-worktree`).
|
||||
pub fn apply(self, conn: &rusqlite::Connection) -> rusqlite::Result<()> {
|
||||
match self {
|
||||
Self::Wal => conn.pragma_update(None, "journal_mode", "WAL"),
|
||||
Self::Truncate => {
|
||||
// EXCLUSIVE locking keeps the wal-index in heap memory, so
|
||||
// converting an already-WAL-stamped DB never mmaps a `-shm`
|
||||
// that a peer NFS client may be rebuilding (SIGBUS).
|
||||
conn.pragma_update(None, "locking_mode", "EXCLUSIVE")?;
|
||||
conn.pragma_update(None, "journal_mode", "TRUNCATE")?;
|
||||
// Legal only after leaving WAL; the exclusive lock is released
|
||||
// by the caller's next database access (e.g. schema init).
|
||||
conn.pragma_update(None, "locking_mode", "NORMAL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse result of the `GROK_SQLITE_JOURNAL_MODE` kill-switch.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EnvOverride {
|
||||
Unset,
|
||||
/// Set to something unrecognized — observable (warned) but non-fatal.
|
||||
Invalid,
|
||||
Mode(JournalMode),
|
||||
}
|
||||
|
||||
/// `GROK_SQLITE_JOURNAL_MODE` override parsing (pure for testability).
|
||||
fn mode_from_env(value: Option<&str>) -> EnvOverride {
|
||||
// Set-but-empty counts as unset: a deliberate blank, not a typo.
|
||||
match value {
|
||||
None | Some("") => EnvOverride::Unset,
|
||||
Some(v) if v.eq_ignore_ascii_case("wal") => EnvOverride::Mode(JournalMode::Wal),
|
||||
Some(v) if v.eq_ignore_ascii_case("truncate") => EnvOverride::Mode(JournalMode::Truncate),
|
||||
Some(_) => EnvOverride::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Short per-host discriminator for per-host DB filenames (lowercased
|
||||
/// alphanumeric hostname, other bytes mapped to `-`, capped at 24 chars).
|
||||
/// `None` when no hostname is available. Sanitization collisions across
|
||||
/// hosts only degrade to plain shared-TRUNCATE behavior, never to WAL.
|
||||
fn host_discriminator() -> Option<String> {
|
||||
let raw = hostname_raw()?;
|
||||
let mut s: String = raw
|
||||
.trim()
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
c.to_ascii_lowercase()
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
s.truncate(24);
|
||||
let s = s.trim_matches('-');
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn hostname_raw() -> Option<String> {
|
||||
let mut buf = [0u8; 256];
|
||||
// SAFETY: buf is a valid out-buffer of the given length; gethostname
|
||||
// NUL-terminates on success (position() guards the not-terminated case).
|
||||
if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } != 0 {
|
||||
return None;
|
||||
}
|
||||
let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||
String::from_utf8(buf[..len].to_vec()).ok()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn hostname_raw() -> Option<String> {
|
||||
std::env::var("COMPUTERNAME").ok()
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
fn hostname_raw() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Best-effort: whether `path` lives on a network/remote filesystem.
|
||||
///
|
||||
/// Any detection failure returns `false` (treat as local) so unclassifiable
|
||||
/// filesystems keep the historical WAL behavior.
|
||||
pub fn is_network_fs(path: &Path) -> bool {
|
||||
imp::is_network_fs(path)
|
||||
}
|
||||
|
||||
/// Classify a Linux `statfs(2)` `f_type` as a network/remote filesystem.
|
||||
///
|
||||
/// Magic values from `include/uapi/linux/magic.h` (Lustre's from its module
|
||||
/// sources). Only the low 32 bits are compared: `f_type` is a signed word
|
||||
/// whose width varies by architecture, so 32-bit kernels sign-extend magics
|
||||
/// with the high bit set (e.g. CIFS 0xFF534D42).
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn is_network_fs_magic(f_type: u64) -> bool {
|
||||
const NFS_SUPER_MAGIC: u64 = 0x6969;
|
||||
const SMB_SUPER_MAGIC: u64 = 0x517B;
|
||||
const SMB2_SUPER_MAGIC: u64 = 0xFE53_4D42;
|
||||
const CIFS_SUPER_MAGIC: u64 = 0xFF53_4D42;
|
||||
const V9FS_MAGIC: u64 = 0x0102_1997;
|
||||
const CODA_SUPER_MAGIC: u64 = 0x7375_7245;
|
||||
const AFS_SUPER_MAGIC: u64 = 0x5346_414F;
|
||||
const AFS_FS_MAGIC: u64 = 0x6B41_4653; // in-kernel kAFS client
|
||||
const CEPH_SUPER_MAGIC: u64 = 0x00C3_6400;
|
||||
const LUSTRE_SUPER_MAGIC: u64 = 0x0BD0_0BD0;
|
||||
const GFS2_MAGIC: u64 = 0x0116_1970;
|
||||
const GPFS_SUPER_MAGIC: u64 = 0x4750_4653; // "GPFS" (Spectrum Scale)
|
||||
const OCFS2_SUPER_MAGIC: u64 = 0x7461_636F;
|
||||
// WekaFS: parallel filesystem sometimes used for network-mounted home
|
||||
// directories. Its magic is not in linux/magic.h — the value is confirmed
|
||||
// empirically from `statfs` on wekafs mounts (`findmnt` reports
|
||||
// FSTYPE=wekafs; coreutils `stat -f` still prints it as UNKNOWN). Like NFS
|
||||
// it offers no coherent cross-host shared memory, so WAL's mmap'd `-shm`
|
||||
// SIGBUSes when a peer host rebuilds it.
|
||||
const WEKAFS_SUPER_MAGIC: u64 = 0x1803_1977;
|
||||
// FUSE is deliberately treated as network: sshfs/s3fs/gluster and other
|
||||
// FUSE-backed homes cannot guarantee coherent mmap across writers, and
|
||||
// rollback journaling costs little there.
|
||||
const FUSE_SUPER_MAGIC: u64 = 0x6573_5546;
|
||||
|
||||
matches!(
|
||||
f_type & 0xFFFF_FFFF,
|
||||
NFS_SUPER_MAGIC
|
||||
| SMB_SUPER_MAGIC
|
||||
| SMB2_SUPER_MAGIC
|
||||
| CIFS_SUPER_MAGIC
|
||||
| V9FS_MAGIC
|
||||
| CODA_SUPER_MAGIC
|
||||
| AFS_SUPER_MAGIC
|
||||
| AFS_FS_MAGIC
|
||||
| CEPH_SUPER_MAGIC
|
||||
| LUSTRE_SUPER_MAGIC
|
||||
| GFS2_MAGIC
|
||||
| GPFS_SUPER_MAGIC
|
||||
| OCFS2_SUPER_MAGIC
|
||||
| WEKAFS_SUPER_MAGIC
|
||||
| FUSE_SUPER_MAGIC
|
||||
)
|
||||
}
|
||||
|
||||
/// Mirrors macOS `libc::MNT_LOCAL` so the pure classifier is testable on
|
||||
/// non-mac hosts; pinned to libc by a macOS-only test.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
const MNT_LOCAL: u32 = 0x0000_1000;
|
||||
|
||||
/// Classify a macOS `statfs(2)` result as a network/remote filesystem.
|
||||
///
|
||||
/// Absence of `MNT_LOCAL` in `f_flags` is the authoritative remote signal
|
||||
/// (covers unknown/future remote fs types). The `f_fstypename` allowlist is
|
||||
/// kept as a conservative extra trigger for remote-backed mounts that still
|
||||
/// set MNT_LOCAL (e.g. FUSE bridges).
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn is_network_fs_mac(f_flags: u32, fstype: &str) -> bool {
|
||||
(f_flags & MNT_LOCAL) == 0 || is_network_fs_name(fstype)
|
||||
}
|
||||
|
||||
/// Classify a macOS `statfs(2)` `f_fstypename` as a network/remote filesystem.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn is_network_fs_name(fstype: &str) -> bool {
|
||||
// macfuse/osxfuse mirror Linux's FUSE-is-network stance (sshfs etc.);
|
||||
// fuse-t needs no entry — its mounts already surface as "nfs".
|
||||
fstype.eq_ignore_ascii_case("nfs")
|
||||
|| fstype.eq_ignore_ascii_case("smbfs")
|
||||
|| fstype.eq_ignore_ascii_case("cifs")
|
||||
|| fstype.eq_ignore_ascii_case("afpfs")
|
||||
|| fstype.eq_ignore_ascii_case("webdav")
|
||||
|| fstype.eq_ignore_ascii_case("macfuse")
|
||||
|| fstype.eq_ignore_ascii_case("osxfuse")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn is_network_fs(path: &Path) -> bool {
|
||||
let Ok(cpath) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: statfs is zero-initializable POD; cpath is NUL-terminated
|
||||
// and st is a valid out-pointer for the duration of the call.
|
||||
let mut st: libc::statfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statfs(cpath.as_ptr(), &mut st) } != 0 {
|
||||
return false;
|
||||
}
|
||||
// Cast through u64: f_type is i64 on 64-bit targets, i32 on some
|
||||
// 32-bit ones; the classifier masks to the meaningful low 32 bits.
|
||||
super::is_network_fs_magic(st.f_type as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn is_network_fs(path: &Path) -> bool {
|
||||
let Ok(cpath) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: statfs is zero-initializable POD; cpath is NUL-terminated
|
||||
// and st is a valid out-pointer for the duration of the call.
|
||||
let mut st: libc::statfs = unsafe { std::mem::zeroed() };
|
||||
if unsafe { libc::statfs(cpath.as_ptr(), &mut st) } != 0 {
|
||||
return false;
|
||||
}
|
||||
// Stack-copy the fixed array to u8 (c_char is i8 here), no heap;
|
||||
// take up to the first NUL.
|
||||
let bytes = st.f_fstypename.map(|c| c as u8);
|
||||
let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
|
||||
let name = std::str::from_utf8(&bytes[..len]).unwrap_or("");
|
||||
super::is_network_fs_mac(st.f_flags, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a Windows path string as UNC (network) — `\\server\share` or
|
||||
/// `\\?\UNC\server\share`; the `\\.\` and `\\?\C:\` device/verbatim-local
|
||||
/// forms are not network. Pure for testability; mapped drives are caught by
|
||||
/// the `GetDriveTypeW` probe instead.
|
||||
#[cfg(any(windows, test))]
|
||||
fn is_windows_unc(path: &str) -> bool {
|
||||
let Some(rest) = path.strip_prefix(r"\\") else {
|
||||
return false;
|
||||
};
|
||||
if let Some(verbatim) = rest.strip_prefix(r"?\") {
|
||||
return verbatim
|
||||
.get(..4)
|
||||
.is_some_and(|p| p.eq_ignore_ascii_case(r"UNC\"));
|
||||
}
|
||||
!rest.starts_with(r".\")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use windows_sys::Win32::Storage::FileSystem::{GetDriveTypeW, GetVolumePathNameW};
|
||||
use windows_sys::Win32::System::WindowsProgramming::DRIVE_REMOTE;
|
||||
|
||||
pub(crate) fn is_network_fs(path: &Path) -> bool {
|
||||
if super::is_windows_unc(&path.to_string_lossy()) {
|
||||
return true;
|
||||
}
|
||||
// Mapped drives (e.g. Z: on SMB): resolve the volume root and ask
|
||||
// for its drive type; any failure → local (historical behavior).
|
||||
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let mut root = [0u16; 261];
|
||||
// SAFETY: wide is NUL-terminated; root is a valid out-buffer whose
|
||||
// length is passed in characters, as the API requires.
|
||||
unsafe {
|
||||
if GetVolumePathNameW(wide.as_ptr(), root.as_mut_ptr(), root.len() as u32) == 0 {
|
||||
return false;
|
||||
}
|
||||
GetDriveTypeW(root.as_ptr()) == DRIVE_REMOTE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else: no cheap, reliable remote-FS probe and no field reports of
|
||||
// the WAL-over-network crash there — keep the WAL default.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
|
||||
mod imp {
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn is_network_fs(_path: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn journal_mode(conn: &rusqlite::Connection) -> String {
|
||||
conn.query_row("PRAGMA journal_mode", [], |r| r.get(0))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_magics_classify_as_network() {
|
||||
for magic in [
|
||||
0x6969u64, // NFS
|
||||
0x517B, // SMB
|
||||
0xFE53_4D42, // SMB2
|
||||
0xFF53_4D42, // CIFS
|
||||
0x0102_1997, // 9p
|
||||
0x7375_7245, // CODA
|
||||
0x5346_414F, // AFS
|
||||
0x6B41_4653, // kAFS
|
||||
0x00C3_6400, // CEPH
|
||||
0x0BD0_0BD0, // Lustre
|
||||
0x0116_1970, // GFS2
|
||||
0x4750_4653, // GPFS
|
||||
0x7461_636F, // OCFS2
|
||||
0x1803_1977, // wekafs
|
||||
0x6573_5546, // FUSE
|
||||
] {
|
||||
assert!(is_network_fs_magic(magic), "magic {magic:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_magics_classify_as_local() {
|
||||
for magic in [
|
||||
0xEF53u64, // ext2/3/4
|
||||
0x0102_1994, // tmpfs
|
||||
0x9123_683E, // btrfs
|
||||
0x5846_5342, // XFS
|
||||
0x794C_7630, // overlayfs
|
||||
0x2FC1_2FC1, // zfs
|
||||
0x0,
|
||||
] {
|
||||
assert!(!is_network_fs_magic(magic), "magic {magic:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_extended_magic_still_matches() {
|
||||
// A 32-bit kernel reports CIFS's 0xFF534D42 as a negative f_type.
|
||||
assert!(is_network_fs_magic(0xFFFF_FFFF_FF53_4D42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fstypenames_classify() {
|
||||
for name in [
|
||||
"nfs", "smbfs", "cifs", "afpfs", "webdav", "NFS", "macfuse", "osxfuse",
|
||||
] {
|
||||
assert!(is_network_fs_name(name), "{name}");
|
||||
}
|
||||
for name in ["apfs", "hfs", "tmpfs", "devfs", ""] {
|
||||
assert!(!is_network_fs_name(name), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_override_parses() {
|
||||
use EnvOverride::{Invalid, Mode, Unset};
|
||||
assert_eq!(mode_from_env(Some("wal")), Mode(JournalMode::Wal));
|
||||
assert_eq!(mode_from_env(Some("WAL")), Mode(JournalMode::Wal));
|
||||
assert_eq!(mode_from_env(Some("truncate")), Mode(JournalMode::Truncate));
|
||||
assert_eq!(mode_from_env(Some("TRUNCATE")), Mode(JournalMode::Truncate));
|
||||
// Typos are Invalid (warned), not silently treated as unset.
|
||||
assert_eq!(mode_from_env(Some("delete")), Invalid);
|
||||
assert_eq!(mode_from_env(Some("wall")), Invalid);
|
||||
assert_eq!(mode_from_env(Some("")), Unset);
|
||||
assert_eq!(mode_from_env(None), Unset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mac_classifier_uses_mnt_local_and_name_override() {
|
||||
// Unknown remote type without MNT_LOCAL → network.
|
||||
assert!(is_network_fs_mac(0, "somefutfs"));
|
||||
assert!(is_network_fs_mac(0, "macfuse_sshfs"));
|
||||
// Plain local APFS → local.
|
||||
assert!(!is_network_fs_mac(MNT_LOCAL, "apfs"));
|
||||
// Allowlisted name wins even when the mount claims MNT_LOCAL.
|
||||
assert!(is_network_fs_mac(MNT_LOCAL, "smbfs"));
|
||||
assert!(is_network_fs_mac(MNT_LOCAL, "macfuse"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn mnt_local_matches_libc() {
|
||||
// libc::MNT_LOCAL is c_int; ours is u32 to match statfs.f_flags.
|
||||
assert_eq!(u64::from(MNT_LOCAL), libc::MNT_LOCAL as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_unc_classifies() {
|
||||
assert!(is_windows_unc(r"\\server\share\grok"));
|
||||
assert!(is_windows_unc(r"\\?\UNC\server\share\grok"));
|
||||
assert!(is_windows_unc(r"\\?\unc\server\share"));
|
||||
assert!(!is_windows_unc(r"\\?\C:\Users\x"));
|
||||
assert!(!is_windows_unc(r"\\.\pipe\grok"));
|
||||
assert!(!is_windows_unc(r"C:\Users\x"));
|
||||
assert!(!is_windows_unc("/home/x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_and_error_paths_are_not_network() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
// CI runners use plain local filesystems (ext4/tmpfs/APFS).
|
||||
assert!(!is_network_fs(tmp.path()));
|
||||
assert!(!is_network_fs(&tmp.path().join("does-not-exist")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_db_path_defaults_to_wal_on_local_fs() {
|
||||
// Ambient override would invalidate the assertion; skip if set.
|
||||
if std::env::var("GROK_SQLITE_JOURNAL_MODE").is_ok() {
|
||||
return;
|
||||
}
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mode = JournalMode::for_db_path(&tmp.path().join("x.sqlite"));
|
||||
assert_eq!(mode, JournalMode::Wal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_db_path_is_per_host_only_in_truncate_mode() {
|
||||
let p = Path::new("/tmp/dir/worktrees.db");
|
||||
// Local mode: untouched.
|
||||
assert_eq!(JournalMode::Wal.effective_db_path(p), p);
|
||||
|
||||
// Network mode: deterministic per-host sibling in the same dir.
|
||||
let a = JournalMode::Truncate.effective_db_path(p);
|
||||
let b = JournalMode::Truncate.effective_db_path(p);
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, p, "CI hosts always have a hostname");
|
||||
assert_eq!(a.parent(), p.parent());
|
||||
let name = a.file_name().unwrap().to_string_lossy().into_owned();
|
||||
assert!(name.starts_with("worktrees.h-"), "{name}");
|
||||
assert!(name.ends_with(".db"), "{name}");
|
||||
|
||||
// Idempotent: pre-resolved paths (for sidecar file ops) don't re-suffix.
|
||||
assert_eq!(JournalMode::Truncate.effective_db_path(&a), a);
|
||||
|
||||
// Extension-less names get the suffix appended.
|
||||
let bare = JournalMode::Truncate.effective_db_path(Path::new("/tmp/dir/state"));
|
||||
let bare_name = bare.file_name().unwrap().to_string_lossy().into_owned();
|
||||
assert!(bare_name.starts_with("state.h-"), "{bare_name}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_mode_survives_legacy_wal_flip_back() {
|
||||
// Journal mode is database-wide: a live old binary can flip a SHARED
|
||||
// DB back to WAL underneath us. The per-host file makes that
|
||||
// impossible by construction — old binaries never open it.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let legacy = tmp.path().join("db.sqlite");
|
||||
|
||||
let a = JournalMode::Truncate.open(&legacy).unwrap();
|
||||
a.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('new');")
|
||||
.unwrap();
|
||||
assert_eq!(journal_mode(&a), "truncate");
|
||||
|
||||
// Simulated old binary: opens the legacy path, stamps WAL, writes.
|
||||
let b = rusqlite::Connection::open(&legacy).unwrap();
|
||||
b.pragma_update(None, "journal_mode", "WAL").unwrap();
|
||||
b.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('old');")
|
||||
.unwrap();
|
||||
|
||||
// A is unaffected: still truncate, reads and writes fine, and its
|
||||
// own file never grows WAL sidecars.
|
||||
assert_eq!(journal_mode(&a), "truncate");
|
||||
a.execute("INSERT INTO t VALUES ('more')", []).unwrap();
|
||||
let n: i64 = a
|
||||
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(n, 2);
|
||||
|
||||
let eff = JournalMode::Truncate.effective_db_path(&legacy);
|
||||
assert_ne!(eff, legacy);
|
||||
let eff_base = eff.display().to_string();
|
||||
assert!(!std::fs::exists(format!("{eff_base}-wal")).unwrap());
|
||||
assert!(!std::fs::exists(format!("{eff_base}-shm")).unwrap());
|
||||
// The legacy file really is in WAL with live sidecars — the two
|
||||
// connections diverged onto different files.
|
||||
let legacy_base = legacy.display().to_string();
|
||||
assert!(std::fs::exists(format!("{legacy_base}-wal")).unwrap());
|
||||
drop(b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_sets_requested_mode() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let wal = rusqlite::Connection::open(tmp.path().join("wal.sqlite")).unwrap();
|
||||
JournalMode::Wal.apply(&wal).unwrap();
|
||||
assert_eq!(journal_mode(&wal), "wal");
|
||||
|
||||
let trunc = rusqlite::Connection::open(tmp.path().join("trunc.sqlite")).unwrap();
|
||||
JournalMode::Truncate.apply(&trunc).unwrap();
|
||||
assert_eq!(journal_mode(&trunc), "truncate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wal_stamped_db_converts_to_truncate() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("db.sqlite");
|
||||
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&path).unwrap();
|
||||
JournalMode::Wal.apply(&conn).unwrap();
|
||||
conn.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('keep');")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let conn = rusqlite::Connection::open(&path).unwrap();
|
||||
JournalMode::Truncate.apply(&conn).unwrap();
|
||||
assert_eq!(journal_mode(&conn), "truncate");
|
||||
let v: String = conn.query_row("SELECT v FROM t", [], |r| r.get(0)).unwrap();
|
||||
assert_eq!(v, "keep");
|
||||
// Exercise the locking-mode downgrade: the next write transaction
|
||||
// must acquire and release locks normally.
|
||||
conn.execute("INSERT INTO t VALUES ('more')", []).unwrap();
|
||||
drop(conn);
|
||||
|
||||
let base = path.display().to_string();
|
||||
assert!(!std::fs::exists(format!("{base}-wal")).unwrap());
|
||||
assert!(!std::fs::exists(format!("{base}-shm")).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_applies_mode() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let wal = JournalMode::Wal
|
||||
.open(&tmp.path().join("wal.sqlite"))
|
||||
.unwrap();
|
||||
assert_eq!(journal_mode(&wal), "wal");
|
||||
|
||||
let trunc = JournalMode::Truncate
|
||||
.open(&tmp.path().join("trunc.sqlite"))
|
||||
.unwrap();
|
||||
assert_eq!(journal_mode(&trunc), "truncate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_readonly_missing_db_errors_and_creates_nothing() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("missing.sqlite");
|
||||
assert!(JournalMode::Wal.open_readonly(&path).is_err());
|
||||
assert!(JournalMode::Truncate.open_readonly(&path).is_err());
|
||||
// The Truncate arm opens read-write but must never create a file —
|
||||
// neither the given path nor its per-host sibling.
|
||||
assert!(!std::fs::exists(&path).unwrap());
|
||||
assert!(!std::fs::exists(JournalMode::Truncate.effective_db_path(&path)).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_readonly_wal_rejects_writes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("db.sqlite");
|
||||
{
|
||||
let conn = JournalMode::Wal.open(&path).unwrap();
|
||||
conn.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('ro');")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let conn = JournalMode::Wal.open_readonly(&path).unwrap();
|
||||
let v: String = conn.query_row("SELECT v FROM t", [], |r| r.get(0)).unwrap();
|
||||
assert_eq!(v, "ro");
|
||||
assert!(conn.execute("INSERT INTO t VALUES ('nope')", []).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_readonly_truncate_rejects_writes() {
|
||||
// The network arm's fd is read-write (conversion needs it), but
|
||||
// query_only must make the connection honor the name.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("db.sqlite");
|
||||
{
|
||||
let conn = JournalMode::Truncate.open(&path).unwrap();
|
||||
conn.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('ro');")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let conn = JournalMode::Truncate.open_readonly(&path).unwrap();
|
||||
assert_eq!(journal_mode(&conn), "truncate");
|
||||
let v: String = conn.query_row("SELECT v FROM t", [], |r| r.get(0)).unwrap();
|
||||
assert_eq!(v, "ro");
|
||||
assert!(conn.execute("INSERT INTO t VALUES ('nope')", []).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_readonly_truncate_never_opens_legacy_file() {
|
||||
// A legacy (possibly WAL-poisoned) file at the shared path must not
|
||||
// be touched: network-mode opens resolve to the per-host sibling,
|
||||
// which doesn't exist yet → error → callers use their defaults.
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let legacy = tmp.path().join("db.sqlite");
|
||||
{
|
||||
let conn = rusqlite::Connection::open(&legacy).unwrap();
|
||||
JournalMode::Wal.apply(&conn).unwrap();
|
||||
conn.execute_batch("CREATE TABLE t (v TEXT); INSERT INTO t VALUES ('legacy');")
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert!(JournalMode::Truncate.open_readonly(&legacy).is_err());
|
||||
// The legacy file stays WAL-stamped and unconverted.
|
||||
let conn = rusqlite::Connection::open(&legacy).unwrap();
|
||||
assert_eq!(journal_mode(&conn), "wal");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue