Fixes for M-02 to M-14, G-01 to G-14, K-02 to K-04, X-04 to X-14, not yet reviewed

Committed before review so Windows and WSL builds can start.
Each fix was tested and mutation-checked here; the codex review
of each item is still to come, so tasks/BUGS.md stays unticked.

M-02 is mid-review: round 1 findings are fixed (apps/ely-mark/src/tally.rs).
K-01 stays deferred. The ElyMark window test mark-outside.sh failed
once at its crash-recovery step in the last run; not yet looked into.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-10 02:29:19 -04:00
1 parent 6505f35669
commit d59159cea2
75 files changed
+1482 -354

No files matched your search

+21 -16
View File
@@ -21,7 +21,7 @@ impl Crash {
pub fn open(dir: &Path, pid: u32) -> io::Result<Self> {
std::fs::create_dir_all(dir)?;
let file = dir.join("session.lock");
std::fs::write(&file, format!("open {pid}"))?;
write_whole(&file, &format!("open {pid}"))?;
Ok(Self { file })
}
@@ -34,28 +34,24 @@ impl Crash {
Err(error) => return Err(error),
};
let mut parts = text.split_whitespace();
match (parts.next(), parts.next()) {
match (parts.next(), parts.next().map(str::parse::<u32>)) {
(Some("discarded"), _) => Ok(Marker::Discarded),
(Some("open"), Some(pid)) => {
let pid: u32 = pid.parse().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "bad pid in the session lock")
})?;
Ok(if alive(pid) {
Marker::Clean
} else {
Marker::Crashed
})
(Some("open"), Some(Ok(pid))) => Ok(if alive(pid) {
Marker::Clean
} else {
Marker::Crashed
}),
// A torn lock means no clean end: offer the points.
_ => {
log::warn!("recovery: the session lock is unreadable; treated as a crash");
Ok(Marker::Crashed)
}
_ => Err(io::Error::new(
io::ErrorKind::InvalidData,
"bad session lock",
)),
}
}
/// An explicit discard: never a crash next launch (§6.2).
pub fn discard(self) -> io::Result<()> {
std::fs::write(&self.file, "discarded 0")
write_whole(&self.file, "discarded 0")
}
/// A clean close removes the lock.
@@ -68,6 +64,15 @@ impl Crash {
}
}
/// Replaces `file` whole: a power cut leaves old or new.
fn write_whole(file: &Path, text: &str) -> io::Result<()> {
use std::io::Write as _;
let mut staging = tempfile::NamedTempFile::new_in(file.parent().unwrap_or(Path::new(".")))?;
staging.write_all(text.as_bytes())?;
staging.as_file().sync_all()?;
staging.persist(file).map(drop).map_err(|error| error.error)
}
/// Whether process `pid` still runs; a crash leaves none.
#[cfg(unix)]
pub fn alive(pid: u32) -> bool {
+6 -3
View File
@@ -53,10 +53,13 @@ pub struct RecoveryKey {
}
impl RecoveryKey {
/// The stored key, else this session's; never a file.
/// The stored key, else this session's; read once a run.
pub fn load() -> Self {
let dir = account_home().map(|home| shared_dir(&home));
Self::from(dir.and_then(|dir| load_with(&KeyringBackend, &dir)))
static LOADED: std::sync::OnceLock<RecoveryKey> = std::sync::OnceLock::new();
*LOADED.get_or_init(|| {
let dir = account_home().map(|home| shared_dir(&home));
Self::from(dir.and_then(|dir| load_with(&KeyringBackend, &dir)))
})
}
/// The loaded key, or this session's when loading failed.
+25 -19
View File
@@ -163,6 +163,11 @@ impl Recovery {
/// Every point on disk, newest first.
pub fn points(&self) -> io::Result<Vec<RecoveryPoint>> {
self.stamps()?.into_iter().map(|stamp| self.point(stamp)).collect()
}
/// The points' stamps by name alone, newest first.
fn stamps(&self) -> io::Result<Vec<u128>> {
let dir = self.dir.join("points");
let mut stamps = Vec::new();
let entries = match std::fs::read_dir(&dir) {
@@ -183,16 +188,13 @@ impl Recovery {
}
stamps.sort_unstable_by(|a, b| b.cmp(a));
stamps.dedup();
stamps
.into_iter()
.map(|stamp| self.point(stamp))
.collect::<io::Result<_>>()
Ok(stamps)
}
/// The point one stamp names.
fn point(&self, stamp: u128) -> io::Result<RecoveryPoint> {
let dir = self.dir.join("points");
let at = SystemTime::UNIX_EPOCH + Duration::from_nanos(stamp as u64);
let at = at_of(stamp);
let enc = dir.join(format!("{stamp}.enc"));
if enc.try_exists()? {
let cursor = match self.key {
@@ -223,26 +225,26 @@ impl Recovery {
/// Drops the oldest points past age, count and byte limits.
fn prune(&self, now: SystemTime) -> io::Result<()> {
let mut points = self.points()?;
while points.len() > self.keep
|| points.last().is_some_and(|point| {
now.duration_since(point.at)
// Names only: a sealed point is never opened here.
let mut stamps = self.stamps()?;
while stamps.len() > self.keep
|| stamps.last().is_some_and(|stamp| {
now.duration_since(at_of(*stamp))
.map(|age| age > self.max_age)
.unwrap_or(false)
})
|| self.bytes(&points)? > self.cap
|| self.bytes(&stamps)? > self.cap
{
let Some(oldest) = points.pop() else { break };
self.drop(oldest.at)?;
let Some(oldest) = stamps.pop() else { break };
self.drop(oldest)?;
}
Ok(())
}
/// The total size of every file of every live point.
fn bytes(&self, points: &[RecoveryPoint]) -> io::Result<u64> {
fn bytes(&self, stamps: &[u128]) -> io::Result<u64> {
let mut total = 0;
for point in points {
let stamp = stamp_of(point.at)?;
for stamp in stamps {
let dir = self.dir.join("points");
for ext in ["bin", "cursor", "history", "enc"] {
let path = dir.join(format!("{stamp}.{ext}"));
@@ -257,8 +259,7 @@ impl Recovery {
}
/// Removes a point's files, its metadata first.
fn drop(&self, at: SystemTime) -> io::Result<()> {
let stamp = stamp_of(at)?;
fn drop(&self, stamp: u128) -> io::Result<()> {
let dir = self.dir.join("points");
for ext in ["cursor", "history", "bin", "enc"] {
match std::fs::remove_file(dir.join(format!("{stamp}.{ext}"))) {
@@ -272,8 +273,8 @@ impl Recovery {
/// An explicit discard: points go, the result stays recorded (§6.2).
pub fn discard(&mut self) -> io::Result<()> {
for point in self.points()? {
self.drop(point.at)?;
for stamp in self.stamps()? {
self.drop(stamp)?;
}
std::fs::write(self.dir.join(".discarded"), "discarded")?;
self.last_ok = None;
@@ -304,6 +305,11 @@ impl Recovery {
}
}
/// The time a stamp names.
fn at_of(stamp: u128) -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_nanos(stamp as u64)
}
/// The point's timestamp as its file stem.
fn stamp_of(at: SystemTime) -> io::Result<u128> {
Ok(at
+24
View File
@@ -272,6 +272,18 @@ fn a_discard_drops_points_and_records_it() {
assert!(!recovery.discarded().expect("the marker clears"));
}
#[test]
fn a_torn_lock_counts_as_a_crash() {
let dir = tempfile::tempdir().expect("a temp dir");
for torn in ["", "op", "open x"] {
std::fs::write(dir.path().join("session.lock"), torn).expect("writes");
assert_eq!(Crash::check(dir.path(), |_| true).expect("reads"), Marker::Crashed, "{torn:?}");
}
// Opening again rewrites it whole.
Crash::open(dir.path(), 7).expect("opens");
assert_eq!(Crash::check(dir.path(), |pid| pid == 7).expect("reads"), Marker::Clean);
}
#[test]
fn the_lock_tells_crash_from_discard() {
let dir = tempfile::tempdir().expect("a temp dir");
@@ -393,3 +405,15 @@ fn the_task_status_starts_from_the_recovery() {
assert_eq!(task.last_ok(), Some(at(100)));
task.stop();
}
#[test]
fn keeping_sealed_points_opens_none_of_them() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut old = Recovery::sensitive(dir.path().to_path_buf(), key()).with_limits(2, u64::MAX);
old.write_once(b"one", Some("1:1"), at(100)).expect("lands");
// Another run's key cannot open the older points.
let mut now = Recovery::sensitive(dir.path().to_path_buf(), [9u8; 32]).with_limits(2, u64::MAX);
now.write_once(b"two", Some("2:2"), at(200)).expect("pruning reads names only");
now.write_once(b"three", None, at(300)).expect("and drops the oldest unread");
assert_eq!(now.stamps().expect("names").len(), 2);
}