Make the Linux app's single-instance claim race-free and private - #455
Conversation
Take a lock file before binding the focus socket, so two launches at once can't both become the app, and fall back to a per-user, per-host directory instead of the shared temp dir when XDG_RUNTIME_DIR is unset. Clear an interrupted update's staged AppImage before staging another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| let runtime = std::env::var_os("XDG_RUNTIME_DIR") | ||
| .map(PathBuf::from) | ||
| .unwrap_or_else(std::env::temp_dir); | ||
| // SAFETY: getuid has no failure mode. | ||
| dir.join(format!("openresearch-app-{}.sock", unsafe { | ||
| libc::getuid() | ||
| })) | ||
| .filter(|dir| dir.is_absolute()); | ||
| if let Some(dir) = runtime { | ||
| return Some((dir, "openresearch-app".to_string())); |
There was a problem hiding this comment.
Shared runtime directory blocks launches If
XDG_RUNTIME_DIR points to an absolute shared directory such as /tmp, another user can hold the predictable openresearch-app.lock. This launch then treats that lock as a running instance, waits for a socket that may not exist, and exits without opening the app. Check that the directory is private before trusting the lock. How this was verified: An absolute shared runtime directory reaches the predictable lock path, and a held lock causes an unanswered focus attempt followed by exit.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/app.rs
Line: 365-369
Comment:
**Shared runtime directory blocks launches** If `XDG_RUNTIME_DIR` points to an absolute shared directory such as `/tmp`, another user can hold the predictable `openresearch-app.lock`. This launch then treats that lock as a running instance, waits for a socket that may not exist, and exits without opening the app. Check that the directory is private before trusting the lock. **How this was verified:** An absolute shared runtime directory reaches the predictable lock path, and a held lock causes an unanswered focus attempt followed by exit.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| Ok((Err(TryLockError::WouldBlock), _)) => { | ||
| ask_running_app_to_focus(&socket); | ||
| return None; |
There was a problem hiding this comment.
Failed bind strands waiting launch If the first launch gets the lock but cannot bind its socket, it releases the lock and runs without a listener. A simultaneous launch that already found the lock held still retries only the nonexistent socket for five seconds, then exits instead of trying to claim the now-available lock. This makes that second launch appear to do nothing.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/app.rs
Line: 394-396
Comment:
**Failed bind strands waiting launch** If the first launch gets the lock but cannot bind its socket, it releases the lock and runs without a listener. A simultaneous launch that already found the lock held still retries only the nonexistent socket for five seconds, then exits instead of trying to claim the now-available lock. This makes that second launch appear to do nothing.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| let Some((dir, name)) = claim_paths() else { | ||
| return Some(FocusRequests(None)); | ||
| }; |
There was a problem hiding this comment.
Fallback failure silently disables focus If there is no usable
XDG_RUNTIME_DIR and the app cannot read /proc/sys/kernel/hostname or resolve a cache directory, this branch runs without a listener or a diagnostic. In restricted Linux environments, later launches can therefore open separate windows instead of focusing the running app. Provide another usable claim path or report that the guard is unavailable.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/app.rs
Line: 380-382
Comment:
**Fallback failure silently disables focus** If there is no usable `XDG_RUNTIME_DIR` and the app cannot read `/proc/sys/kernel/hostname` or resolve a cache directory, this branch runs without a listener or a diagnostic. In restricted Linux environments, later launches can therefore open separate windows instead of focusing the running app. Provide another usable claim path or report that the guard is unavailable.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| let lock = match lock.map(|file| (file.try_lock(), file)) { | ||
| Ok((Ok(()), file)) => Some(file), | ||
| Ok((Err(TryLockError::WouldBlock), _)) => { | ||
| ask_running_app_to_focus(&socket); | ||
| return None; |
There was a problem hiding this comment.
Instance handoff lacks tests The new lock and socket handoff has no Linux instance tests for simultaneous claims, bind failure, or relaunch. Those paths determine whether a launch opens a duplicate window or exits without focusing anything, so regressions in the race fix could go unnoticed. Add focused tests with isolated claim paths and competing launches.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/app.rs
Line: 392-396
Comment:
**Instance handoff lacks tests** The new lock and socket handoff has no Linux instance tests for simultaneous claims, bind failure, or relaunch. Those paths determine whether a launch opens a duplicate window or exits without focusing anything, so regressions in the race fix could go unnoticed. Add focused tests with isolated claim paths and competing launches.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…ed lock Use XDG_RUNTIME_DIR only when the user owns it and no one else can write it, retry the lock alongside the socket so a launch waiting on an app that gave the lock up takes over, say when no private directory exists, and test the handoff in a scratch directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #445, addressing Greptile's review of the Linux desktop app.
What changes
File::try_lock) onopenresearch-app.lockbefore binding the focus socket, and keeps it until exit; files are close-on-exec, so an update's exec releases it. A launch that finds the lock held asks the running app to focus, retrying for up to 5 s while the winner binds, instead of deleting its socket. Previously two launches at once could both connect-fail, and the second would unlink the first's socket and run as a second app.XDG_RUNTIME_DIRunset (or relative), the lock and socket go in~/.cache/openresearchinstead of/tmp, where another user could create the socket first and make every launch exit. The name includes the hostname there, since a home on NFS is shared across machines.sweep_leftoversbefore staging, removing.OpenResearch-update-*files over an hour old that a killed update left beside the AppImage.Not changed
APPIMAGEfrom/proc/self/exe(orrealpath), so it is already the target file.fetch_release_asset; streaming it is its own change.Test plan
cargo fmt --check,cargo clippy --all-targets -D warnings,cargo test --lockedlinux app (x86_64|aarch64)compiles the Linux-only code and the smoke test passes🤖 Generated with Claude Code