Swap: lock journals to one session; record the base file in the header
- open_ctx takes a non-blocking flock on the journal. A journal locked by another kte process is left alone: this session reports it once and does not write (records from two sessions used to interleave in one file). Opening a file whose journal is live in another session skips recovery instead of replaying or removing that journal. - The header records the size and mtime of the file the journal's records apply to (flags bit 0, bytes 24..39; previously reserved, so no version change). Before offering recovery, a journal whose base no longer matches the file on disk (changed after a crash, or the crash fell between a save's rename and the journal reset) is treated as stale and offered for deletion, instead of replaying its edits onto different text. Journals from older kte (flag clear) replay as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
@@ -520,3 +520,66 @@ TEST(SwapJournal_Reload_ResetsJournal)
|
||||
ASSERT_TRUE(kte::SwapManager::ReplayFile(check, kte::SwapManager::ComputeSwapPathForTests(*cur), rerr));
|
||||
ASSERT_EQ(buffer_bytes_via_views(check), live);
|
||||
}
|
||||
|
||||
|
||||
// A journal whose base file changed on disk afterwards is not replayed onto
|
||||
// the new content; the user is offered to delete it instead.
|
||||
TEST(SwapRecoveryPrompt_StaleJournal_NotReplayed)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgSandbox sb("stale");
|
||||
const std::string file = (sb.root / "work" / "s.txt").string();
|
||||
write_file_bytes(file, "base\n");
|
||||
std::string expected;
|
||||
(void) make_journal(file, [](Buffer &b) {
|
||||
b.insert_text(0, 0, std::string("A"));
|
||||
}, expected);
|
||||
write_file_bytes(file, "completely different content\n"); // changed after the crash
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
ed.RequestOpenFile(file);
|
||||
(void) ed.ProcessPendingOpens();
|
||||
ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::DeleteCorruptSwap);
|
||||
answer(ed, "y");
|
||||
ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), std::string("completely different content\n"));
|
||||
}
|
||||
|
||||
|
||||
// A journal held by another live session is neither replayed, removed nor
|
||||
// written to by a second session opening the same file.
|
||||
TEST(SwapRecoveryPrompt_LiveJournalOfOtherSession_LeftAlone)
|
||||
{
|
||||
ktet::InstallDefaultCommandsOnce();
|
||||
XdgSandbox sb("live");
|
||||
const std::string file = (sb.root / "work" / "l.txt").string();
|
||||
write_file_bytes(file, "base\n");
|
||||
|
||||
// "Other process": a session with the journal open (and locked).
|
||||
Buffer other;
|
||||
std::string err;
|
||||
ASSERT_TRUE(other.OpenFromFile(file, err));
|
||||
kte::SwapManager other_sm;
|
||||
other_sm.Attach(&other);
|
||||
other.SetSwapRecorder(other_sm.RecorderFor(&other));
|
||||
other.insert_text(0, 0, std::string("O"));
|
||||
other_sm.Flush(&other);
|
||||
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(other);
|
||||
ASSERT_TRUE(kte::SwapManager::JournalInUse(swp));
|
||||
const std::string before = read_file_bytes(swp);
|
||||
|
||||
Editor ed;
|
||||
ed.SetDimensions(24, 80);
|
||||
ed.AddBuffer(Buffer());
|
||||
ed.RequestOpenFile(file);
|
||||
(void) ed.ProcessPendingOpens();
|
||||
ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::None);
|
||||
ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), std::string("base\n"));
|
||||
ed.CurrentBuffer()->insert_text(0, 0, std::string("X"));
|
||||
ed.Swap()->Flush(ed.CurrentBuffer());
|
||||
ASSERT_EQ(read_file_bytes(swp), before);
|
||||
|
||||
other.SetSwapRecorder(nullptr);
|
||||
other_sm.Detach(&other, true);
|
||||
}
|
||||
|
||||
@@ -124,8 +124,10 @@ TEST(SwapWriter_Header_Records_And_CRC)
|
||||
for (int i = 0; i < 8; ++i)
|
||||
ASSERT_EQ(bytes[(std::size_t) i], magic[i]);
|
||||
ASSERT_EQ(read_le32(bytes.data() + 8), (std::uint32_t) 1);
|
||||
// flags currently 0
|
||||
ASSERT_EQ(read_le32(bytes.data() + 12), (std::uint32_t) 0);
|
||||
// flags bit 0: the base file's identity (size, mtime) is recorded, since
|
||||
// the buffer is file-backed.
|
||||
ASSERT_EQ(read_le32(bytes.data() + 12), (std::uint32_t) 1);
|
||||
ASSERT_EQ(read_le64(bytes.data() + 24), (std::uint64_t) std::filesystem::file_size(b.Filename()));
|
||||
ASSERT_TRUE(read_le64(bytes.data() + 16) != 0);
|
||||
|
||||
// Records
|
||||
|
||||
Reference in New Issue
Block a user