Survive a crash, and a reboot after one
kill, a closed window, a reboot: no SessionEnd arrives and the state file stays. Each case was tried rather than reasoned about, and one of the three was broken. A killed session was already handled -- the process is gone, so the file and its lock are removed within the 20 s liveness tick. An interrupted hook write left its temporary file behind forever; those are now swept once they are five minutes old, which is late enough that a hook part-way through writing one does not lose the update. The reboot case was the broken one. State files outlive a reboot and pids are handed out afresh, so "does /proc/<pid> exist" only answers "is some process wearing that number". Verified by giving an unrelated live process the pid of a dead session: the ghost sat in the panel as a session waiting for input, and would have stayed there forever, asking for an answer nobody could give. The pid is now pinned to the process start time from /proc/<pid>/stat, recorded when the state is written and compared when it is read. Files written before that field existed compare only on existence, as before, so a session open across the upgrade is not evicted. An abandoned flock needed nothing: the kernel drops it when the holder dies, so there is no deadlock to recover from.
This commit is contained in:
@@ -36,6 +36,11 @@ ev() { # event [extra json]
|
||||
emit "$(ev SessionStart '"source":"startup"')"
|
||||
check "SessionStart -> waiting" "waiting" "$(field state)"
|
||||
|
||||
# Pins the recorded pid to one process, so a state file that outlives a reboot
|
||||
# cannot be revived by whatever inherits that pid number next.
|
||||
start=$(field pid_start)
|
||||
check "process start time recorded" "yes" "$([ -n "$start" ] && [ "$start" != "0" ] && echo yes || echo no)"
|
||||
|
||||
emit "$(ev UserPromptSubmit '"prompt":"hi"')"
|
||||
check "UserPromptSubmit -> busy" "busy" "$(field state)"
|
||||
|
||||
|
||||
+11
-3
@@ -27,11 +27,12 @@ function check(name, condition, detail = '') {
|
||||
}
|
||||
|
||||
const now = GLib.get_real_time() / 1e6;
|
||||
function write(id, state, cwd, agoSeconds, pid) {
|
||||
function write(id, state, cwd, agoSeconds, pid, pidStart = 0) {
|
||||
const payload = {
|
||||
session_id: id, state, cwd, since: now - agoSeconds,
|
||||
event_ts: now, pid, event: 'test', notification_type: '',
|
||||
message: '', zellij_session: 'ztest', zellij_pane: '1', transcript: '',
|
||||
event_ts: now, pid, pid_start: pidStart, event: 'test',
|
||||
notification_type: '', message: '', zellij_session: 'ztest',
|
||||
zellij_pane: '1', transcript: '',
|
||||
};
|
||||
GLib.file_set_contents(
|
||||
GLib.build_filenamev([STATE, `${id}.json`]), JSON.stringify(payload));
|
||||
@@ -50,6 +51,9 @@ write('s-dead', 'waiting', '/home/u/proj-dead', 5, deadPid);
|
||||
// Written by the older hook, which had a fourth state. Files like this
|
||||
// survive an upgrade in a session that was already open.
|
||||
write('s-legacy', 'idle', '/home/u/proj-legacy', 5, livePid);
|
||||
// Survived a reboot: the pid exists again, but belongs to something else now.
|
||||
// Without an identity check this sits in the panel forever as a live session.
|
||||
write('s-ghost', 'waiting', '/home/u/proj-ghost', 99999, livePid, 1);
|
||||
GLib.file_set_contents(GLib.build_filenamev([STATE, 'notes.txt']), 'ignored');
|
||||
GLib.file_set_contents(GLib.build_filenamev([STATE, 'half.json']), '{"broken');
|
||||
|
||||
@@ -67,6 +71,10 @@ store.connect('changed', () => {
|
||||
stateRank('waiting') < stateRank('busy'));
|
||||
|
||||
check('dead session dropped', !s.some(x => x.sessionId === 's-dead'));
|
||||
check('reused pid from a previous boot dropped',
|
||||
!s.some(x => x.sessionId === 's-ghost'));
|
||||
check('and its file removed',
|
||||
!GLib.file_test(GLib.build_filenamev([STATE, 's-ghost.json']), GLib.FileTest.EXISTS));
|
||||
check('truncated file skipped, others survive', s.length === 5,
|
||||
`got ${s.length}: ${s.map(x => x.sessionId).join(',')}`);
|
||||
check('non-json ignored', !s.some(x => x.sessionId.includes('notes')));
|
||||
|
||||
Reference in New Issue
Block a user