Skip to content

desktop: report whether the previous run ended in a crash - #1461

Draft
hisco wants to merge 1 commit into
mainfrom
rad-304-desktop-crash-marker
Draft

desktop: report whether the previous run ended in a crash#1461
hisco wants to merge 1 commit into
mainfrom
rad-304-desktop-crash-marker

Conversation

@hisco

@hisco hisco commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Split out of #1459 so it can be judged on its own. That PR is now the fifteen-line webview log line and nothing else.

Why

The desktop app cannot log its own segfault. A bug report therefore has no way to say whether the last session died or the user simply quit — the journal shows a process that stopped, either way. Working that out for #1360 meant reading Go stack traces out of a 10MB journal export.

What

Each run creates a file named after its PID under ~/.radar/desktop-sessions/<host>/ and holds an exclusive lock on it for its lifetime. On the next launch, a marker that can be locked belonged to a run that is gone:

[desktop] previous run (pid 41446) did not exit cleanly — it crashed or was force-quit

Passive. No behaviour change, no UI, nothing to opt into.

Why it is shaped this way

Nearly every design decision here is a correction of something that produced a wrong signal in review. Recording them because they are the reason the code is not simpler:

  • The lock carries the signal, not the PID. PIDs are reused. Probing whether a PID is alive made a crashed run look like a running second window — suppressing the real crash and printing that another instance was running when none was. A lock is released by the kernel on death, however death happens, so reuse stops mattering.
  • The file must also be stamped as running, under the lock. A marker is briefly present and unlocked at both ends of a run: after creation before the lock is taken, and after the lock drops before the unlink. A launch landing in either gap could report a deliberate quit as a crash. Reordering the unlink does not fix this alone — Windows will not unlink a file with an open handle.
  • Claim after the startup checks. Claiming earlier stranded a marker on every configuration error that ends in os.Exit, reporting a phantom crash on the next launch.
  • Release after teardown returns. Releasing first meant a shutdown that hung or was killed still read as clean, which is exactly the failure worth catching.
  • A pre-exit hook in the updater. Relaunch ends in os.Exit(0) and skips the Wails shutdown hook entirely, so a routine self-update would have reported a crash every time.
  • Both the hook and the session handle are guarded. Relaunch runs on its own goroutine and the HTTP server accepts requests before registration, so closing the window mid-relaunch reaches both paths.
  • One file per PID, scoped per host. A shared marker let a second window adopt and then delete the first one's. A shared home directory across machines let one host read another's markers, where a lock says nothing at all.

Honest assessment

This is roughly a hundred lines whose entire output is one log line, and it has now been through five review rounds that found seven correctness bugs between them — every one in the question "is the previous run dead", which is harder than it looks.

It is worth knowing whether that trade is one this codebase wants. The alternative is that a crash report keeps arriving without the answer, and someone reads stack traces by hand. Declining this is a reasonable call.

Testing

19 tests: the unclean exit, reported once and not forever; a live instance ignored and its marker preserved; a crash under a reused PID still reported; both unlocked-marker windows; the stamp written under the lock; concurrent claim and release; per-host scoping; clean shutdown leaving nothing; malformed entries; a missing home directory; directory permissions. All green under -race on macOS and Linux. Builds clean for linux, windows and darwin.

The limit worth stating: these drive the helpers directly. Nobody has segfaulted a real Wails build and watched the message appear on the next launch, and no test covers a filesystem where locking misbehaves — on one that cannot lock, the code stays silent rather than guessing.

https://claude.ai/code/session_01KEyZgyPtfpXdbWqPPsTXZe


Note

Low Risk
Local diagnostic logging only under ~/.radar; no auth or cluster paths touched, with mutex-guarded release and conservative behavior when locking fails.

Overview
Adds passive crash detection for the desktop app: on launch it may log that the previous session did not exit cleanly, with no UI or behavior change beyond that line.

Each run creates a PID-named marker under ~/.radar/desktop-sessions/<host>/, holds an exclusive file lock for the process lifetime, and writes running only while locked. The next launch treats a lockable marker still stamped running as an unclean exit (lock release on death, not PID liveness). Claim runs only after startup config checks pass; release runs last in Wails shutdown (after server teardown) and via a new updater.OnBeforeExit hook invoked from runBeforeExit() before os.Exit on self-update relaunch (darwin/linux/windows), so updates and early exits do not false-positive.

Platform tryLockFile helpers use flock (Unix) and LockFileEx (Windows). Extensive tests cover one-shot reporting, live second instances, reused PIDs, release races, and host-scoped dirs.

Reviewed by Cursor Bugbot for commit 34bfa49. Bugbot is set up for automated code reviews on this repo. Configure here.

The app cannot log its own segfault, so a bug report has no way to say whether
the last session died or was quit — the journal shows a process that stopped
either way.

Each run creates a file named after its PID and holds an exclusive lock on it
for its lifetime. The lock carries the signal, not the file: the kernel
releases it when the process dies, however it dies. A marker that can be locked
belonged to a run that is gone; one that cannot belongs to an instance still
alive. PID liveness is deliberately not used, because PIDs are reused and a
recycled one would make a crashed run look like a running second window.

The file must also be stamped as running, under the lock, before it counts. A
marker is briefly present and unlocked at both ends of a run — after creation
before the lock is taken, and after the lock drops before the unlink — and a
launch landing in either gap would otherwise report a deliberate quit as a
crash.

Claiming happens once the process is committed to starting the window, so the
startup checks that end in os.Exit cannot strand a marker. Releasing happens
after teardown returns, so a shutdown that hangs or is killed still reads as
unclean. Self-update relaunch exits via os.Exit and skips the shutdown hook
entirely, so the updater gained a pre-exit callback; that hook and the session
handle are both guarded, since relaunch runs on its own goroutine and can
overlap a window close.

Markers are scoped per host: a home directory can be shared across machines,
and a lock taken on one host says nothing about a process on another.

Claude-Session: https://claude.ai/code/session_01KEyZgyPtfpXdbWqPPsTXZe
@hisco
hisco requested a review from nadaverell as a code owner August 19, 2026 16:10

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 34bfa49. Configure here.

Comment thread cmd/desktop/app.go
app.Shutdown(a.srv)
// Last, so a teardown that hangs or is killed still reads as an unclean
// exit — that is precisely the failure worth knowing about.
markSessionEnd()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean quit reported as crash

High Severity

markSessionEnd runs only after app.Shutdown, but srv.Stop closes the HTTP listener and the existing server goroutine treats that as a fatal StartWithReady error and calls os.Exit(1). That exit wins the race with marker release, so a normal window close leaves a running file and the next launch logs a crash.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 34bfa49. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it reproduces in a real log rather than just in theory. From the journal a user attached to #1360, on a normal window close:

14:31:08 Desktop app shutting down...
14:31:08 Shutting down...
14:31:08 Server error: accept tcp 127.0.0.1:41947: use of closed network connection

srv.Stop() closes the listener, http.Serve returns net.ErrClosed — which is not http.ErrServerClosed, so it is not filtered — and the server goroutine calls os.Exit(1). Every clean quit would race that exit and, often enough, leave a running marker behind and report a phantom crash on the next launch. That is the worst failure this feature can have, on the most common action a user takes.

Notably this was introduced by the previous fix, which moved the release after teardown so that a hung shutdown would still read as unclean. That traded a rare false negative for a frequent false positive.

Marking this PR draft rather than patching again. Full reasoning in the PR comment.

Separately, that log line is a pre-existing bug worth its own fix: radar-desktop exits with status 1 on every normal window close, and prints an alarming "Server error" in the process. It shows up in user bug reports today and looks like a fault when nothing is wrong.

if err != nil {
log.Printf("[desktop] could not lock session marker: %v", err)
}
file.Close()
// Only now, holding the lock, does the marker mean "a run is in progress".
if _, err := file.WriteString(runningMarker); err != nil {
log.Printf("[desktop] could not record session marker: %v", err)
file.Close()
if err := held.Truncate(0); err != nil {
log.Printf("[desktop] could not clear session marker: %v", err)
}
held.Close() // releases the lock
if err != nil || !locked {
// Held by a live instance, or the filesystem cannot lock. Saying
// nothing beats guessing at a crash that may not have happened.
file.Close()
if markedRunning(file) {
log.Printf("[desktop] previous run (pid %d) did not exit cleanly — it crashed or was force-quit", pid)
}
file.Close()
@hisco
hisco marked this pull request as draft August 19, 2026 16:23
@hisco

hisco commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Marking this draft. Recording why, so the next person does not rediscover it.

The change is roughly a hundred lines whose entire output is one log line. It has now been through five review rounds, which between them found seven correctness bugs — every one in the same question, "did the previous run die", and every one producing a wrong signal rather than a crash or a hang:

  1. The marker was stranded on every startup path that ends in os.Exit, reporting a phantom crash next launch.
  2. A second window overwrote the first one's marker, then deleted it on its own clean exit, losing the first one's crash.
  3. PID liveness gave a false negative under PID reuse, and printed that another instance was running when none was.
  4. The marker was briefly present and unlocked at both ends of a run, so a concurrent launch could report a deliberate quit as a crash.
  5. Releasing before teardown meant a shutdown that hung or was killed still read as clean.
  6. The session handle was racy between the Wails shutdown callback and the self-update relaunch goroutine.
  7. Fixing (5) by releasing after teardown created a false positive on every clean quit, because srv.Stop() closes the listener and the server goroutine reaches os.Exit(1) first.

The last one is the argument. A fix applied to remove a rare false negative introduced a frequent false positive, on the most common action a user takes, and it took another review round to catch. That is not a signal anyone should trust during triage.

The value it was meant to add is also smaller than it first looked. In the report that motivated it, the journal already contained full Go stack traces with SIGSEGV — the crash was legible without any marker. It only helps when someone sends a diagnostics snapshot with no logs attached.

The webview library line that shipped alongside this in #1459 is the part that actually saved a round-trip with a reporter, and it drew no findings in any round.

Leaving this open as a draft rather than closing it, because the underlying need is real. Anything that revives it should first fix the bug in (7) on its own merits: radar-desktop exits with status 1 on every normal window close, and prints Server error: use of closed network connection while doing it. That is visible in user bug reports today and reads as a fault when nothing is wrong. Until a clean shutdown is actually clean, no marker scheme built on top of it can be trusted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants