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:
75 files changed
+1482
-354
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user