fix: Windows reads of lock files, update paths cfg(unix), merge main again (P11c)
check / check (push) Canceled after 0s
check / portable (macos-latest) (push) Canceled after 0s
check / portable (windows-latest) (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s

spec_vault swept every file under a book, lock files included; on Windows
a held LockFileEx lock is mandatory and the read failed. The locks are
already dedicated files (never data), so the test skips *.lock. Merged
main (P13 step 6, the macOS server.log assert, P11a's AppImage identity):
host.rs keeps main's five-field identity on Unix and size/last-write on
Windows; the update poll and set_program_version are Unix-only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-29 17:03:30 -06:00
commit 7749afc557
234 files changed
+13028 -18411

No files matched your search

+52 -6
View File
@@ -203,19 +203,39 @@ fn children_executable() -> PathBuf {
}
}
/// A program file's identity: what changes when the file is replaced.
/// A program file's identity: device and inode, size and last write. A
/// replaced file differs in the inode, and a rewritten one in the rest.
#[cfg(unix)]
type Identity = (u64, u64, u64, i64, i64);
/// Windows: size and last write (nanoseconds).
#[cfg(windows)]
type Identity = (u64, u64);
#[cfg(unix)]
fn identity_from(meta: &std::fs::Metadata) -> Identity {
(
meta.dev(),
meta.ino(),
meta.len(),
meta.mtime(),
meta.mtime_nsec(),
)
}
/// The running executable's file identity, and its file's now: they
/// differ once the file was replaced (INVARIANT 7).
/// differ once the file was replaced (INVARIANT 7). An AppImage is its
/// image file (`APPIMAGE`), not the program inside the mount.
#[cfg(unix)]
fn running_identity() -> Option<Identity> {
if let Some(image) = crate::start::own_appimage(std::env::var_os("APPIMAGE").as_deref()) {
return Some(identity_from(&std::fs::metadata(image).ok()?));
}
let running = if cfg!(target_os = "linux") {
std::fs::metadata("/proc/self/exe").ok()?
} else {
std::fs::metadata(std::env::current_exe().ok()?).ok()?
};
Some((running.dev(), running.ino()))
Some(identity_from(&running))
}
/// Windows can't replace a program that runs, so an update puts the new
@@ -232,12 +252,12 @@ fn file_identity() -> Option<Identity> {
identity_of(&crate::start::this_executable().ok()?)
}
/// The identity (device, inode) of the program at `path`: a file someone
/// may run; else None.
/// The identity of the program at `path`: a file someone may run; else
/// None.
#[cfg(unix)]
fn identity_of(path: &Path) -> Option<Identity> {
let meta = std::fs::metadata(path).ok()?;
(meta.is_file() && meta.permissions().mode() & 0o111 != 0).then(|| (meta.dev(), meta.ino()))
(meta.is_file() && meta.permissions().mode() & 0o111 != 0).then(|| identity_from(&meta))
}
/// The identity (size, last write in nanoseconds) of the program at
@@ -521,6 +541,13 @@ impl Host {
let started_as = running_identity();
let mut exe_poll = tokio::time::interval(self.knobs.exe_poll);
exe_poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
// The daily look for a newer release (P11a step 4): the work is
// a thread of its own, one at a time, and only stages a file.
let mut update_poll =
tokio::time::interval((self.knobs.update_check / 4).max(Duration::from_millis(10)));
update_poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
#[cfg(unix)]
let looking = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut idle_at = Some(Instant::now() + self.knobs.close_after);
let server_start = tokio::time::sleep(self.knobs.server_delay);
tokio::pin!(server_start);
@@ -678,6 +705,25 @@ impl Host {
}
}
}
_ = update_poll.tick(), if matches!(self.phase, Phase::Open) => {
// Staging an update replaces the program's file, which
// Windows forbids while it runs: P11a step 5.
#[cfg(unix)]
{
use std::sync::atomic::Ordering;
if !looking.swap(true, Ordering::SeqCst) {
let (root, every, looking) = (self.root.clone(), self.knobs.update_check, looking.clone());
std::thread::spawn(move || {
match crate::update::stage_if_due(&root, crate::update::program_version(), every) {
Ok(Some(version)) => eprintln!("wanderbooks host: version {version} is downloaded and checked; it starts the next time the books open, or at `wanderbooks update`"),
Ok(None) => {}
Err(why) => eprintln!("wanderbooks host: the look for a newer version failed: {why}"),
}
looking.store(false, Ordering::SeqCst);
});
}
}
}
_ = exe_poll.tick(), if matches!(self.phase, Phase::Open) => {
if started_as.is_some() && file_identity().is_some_and(|now| Some(now) != started_as) {
eprintln!("wanderbooks host: a new version is installed: restarting");