Skip to content

Backstop: call stat through a fixed path, so GNU stat on PATH cannot stop an install - #90

Merged
krishhgg merged 3 commits into
krishhgg:mainfrom
nsd97:fix-gnu-stat
Oct 12, 2026
Merged

krishhgg merged 3 commits into
krishhgg:mainfrom
nsd97:fix-gnu-stat

Conversation

@nsd97

@nsd97 nsd97 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What happened

On a Mac with Homebrew's coreutils first on PATH, a source install stopped before installing anything. That PATH resolved stat to /opt/homebrew/opt/coreutils/libexec/gnubin/stat (GNU coreutils 9.12):

==> Ending any stale session and checking the recovery journal

Install stopped: the backstop could not fully undo a previous session
(exit status 75).

The Mac had never run Insomnia. insomnia.log had one line:

backstop: recovery lock .../.recovery.lock still held after 10s (lockf exit 75); nothing changed, will retry

backstop.sh decides whether its fd 9 is the caller's handle on the lock with stat -f %i, and stat was the only tool it still took from PATH. GNU stat reads -f as --file-system, so both sides print file system text instead of an inode, and the two never match.

  • The backstop that install.sh runs under its own recovery lock therefore reopens the lock and waits on its caller for LOCK_TIMEOUT_SECONDS, then exits 75.
  • uninstall.sh hands its lock down the same way.
  • The LaunchAgent is not affected: launchd runs it with the system PATH.
  • The same bare stat reads the size of the app binary's --resume-frozen answer with -f %z.

What changes

  • backstop.sh: STAT=/usr/bin/stat joins the fixed tool paths, and both calls use it. A comment says why.
  • RecoveryScriptTests:
    • testBackstopSharesLockHandedDownOnFd9WithGNUStatFirstOnPath repeats the existing hand-down test with a GNU-style stat stub first on PATH. The stub logs and prints what GNU prints.
    • On the old script it fails the way the install did: exit 75, the stub ran, "still held" in the log.
    • With the change it passes, along with testBackstopSharesLockHandedDownOnFd9 and testBackstopWithUnrelatedFd9DoesNotShareAForeignLock.

Checks

Sources

  • GNU stat's -f is --file-system: "Report information about the file systems where the given files are located instead of information about the files themselves." GNU Coreutils, stat invocation
  • BSD stat's -f format, "Display information using the specified format", with i the inode number and z the size in bytes: FreeBSD stat(1). macOS's man 1 stat says the same.
  • The fixed tool paths rule: .greptile/config.json, scripts-call-tools-through-fixed-path-variables
  • Script and test requirements: CONTRIBUTING.md

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

This PR appears safe to merge; no actionable issue remains.

Summary

The backstop now calls /usr/bin/stat through STAT, so a GNU stat earlier on PATH cannot disrupt recovery.

  • The backstop recognizes a handed-down lock when GNU stat is first on PATH.

Reviews (2) · Last reviewed commit: "Tests: write the GNU stat stub and its w..." · Reviewed by Greptile

nsd97 and others added 3 commits October 11, 2026 19:36
…stop an install

backstop.sh ran `stat -f %i` and `stat -f %z` by bare name, the only tools
it still took from PATH. In a shell with GNU coreutils first on PATH
(Homebrew's gnubin), GNU stat reads -f as --file-system and prints no inode
or size. The fd 9 check then never matches the lock file, so the backstop
that install.sh runs under its own recovery lock reopens the lock and waits
on its caller until it exits 75. install.sh stops with "the backstop could
not fully undo a previous session" on a Mac that never had one. uninstall.sh
hands its lock down the same way. The LaunchAgent is not affected: launchd
gives it the system PATH.

Add STAT=/usr/bin/stat to the fixed tool paths and use it for both calls.
The regression test hands the lock down on fd 9 with a GNU-style stat stub
first on PATH; on the old script it exits 75 and the stub runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Brings in krishhgg#86's TestScripts.write, which the next commit uses for this
PR's test scripts, and the other merges since bb5cf2b.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This branch started before krishhgg#86, so the new test wrote both scripts
directly and CI's ShellCheck step never saw them. They now go through
TestScripts.write like the sibling hand-down test's wrapper.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@krishhgg

Copy link
Copy Markdown
Owner

I pushed two commits to this branch:

  • 7bf2569 merges main. It merged cleanly.
  • 636a33f is a test-only fix from review. This branch started before Tests: run ShellCheck over every shell script the tests write #86, which added TestScripts.write, so the new test wrote the GNU stat stub and its wrapper straight to disk, and CI's ShellCheck step never saw them. Both now go through TestScripts.write, like the sibling hand-down test's wrapper. The stub passes ShellCheck, and the three hand-down tests pass locally.

The backstop change itself had no findings.

🤖 Generated with Claude Code

@krishhgg
krishhgg merged commit 28f1f43 into krishhgg:main Oct 12, 2026
5 checks passed
@krishhgg

Copy link
Copy Markdown
Owner

Thanks again for this fix. Before it ships in a release, I need test evidence from your Mac for the code as it is now on main. The evidence in this PR's description came from an earlier build. Since then the branch got review fixes and merges of main, so it doesn't cover the code that merged. Any change that can affect how Insomnia behaves needs this, so I'm asking every contributor from this round.

Checks for this change (on your macOS 27 MacBook Pro)

Test main alone this time. Your earlier build also had #76 and #74 applied, which aren't merged.

Run checks 1 to 3 in a Terminal with GNU coreutils first on PATH: export PATH="$(brew --prefix coreutils)/libexec/gnubin:$PATH". Post command -v stat and stat --version | head -1 from that shell.

  1. Install over an existing install. Run ./scripts/install.sh. Expected: it finishes and prints "LaunchAgent com.insomnia.backstop loaded (launchctl print confirms)", and insomnia.log has no "still held" line. Post the full install output.
  2. Uninstall. In the same shell, run ./scripts/uninstall.sh. Expected: it finishes and removes the app and the agent. Then reinstall the same way.
  3. Resuming a frozen test process through the backstop. Install with INSOMNIA_LID_SIMULATION=1 ./scripts/install.sh from the same shell. Put a disposable app such as TextEdit on the freeze list, start a session, and run scripts/simulate-lid.sh closed. ps -o stat= -p <pid> should show T. Then kill -9 Insomnia and run ./scripts/uninstall.sh. Expected: the backstop resumes the test app through the installed Insomnia --resume-frozen (no T any more), and sleep is restored. This path now reads a size with /usr/bin/stat. Reinstall a normal build afterwards.
  4. Normal PATH. In a new Terminal with the default PATH, run ./scripts/install.sh again. Expected: it finishes and prints the same line as check 1.
  5. Recovery agent after a crash. Start a session, kill -9 Insomnia, and let the session's end time pass. Expected: the agent restores sleep (SleepDisabled reads 0), and the log shows its lines.

Build and what to post

  • Build. A clean checkout of current main (db9085e or later), with no other PRs applied, installed with ./scripts/install.sh. Post git rev-parse HEAD and the output of git status --short, which should be empty.
  • Mac. Post sw_vers, sysctl -n hw.model and sysctl -n machdep.cpu.brand_string.
  • Samples. Before each check, start a sampler in a Terminal window and leave it running. It keeps logging while the lid is closed, because the Mac stays awake:
    while :; do date '+%T'; pmset -g | grep SleepDisabled; pmset -g custom | sed -n '/Battery Power/,/AC Power/p' | grep -E 'powermode|lowpowermode'; sleep 5; done | tee -a ~/insomnia-samples.txt
  • For each check, post the steps you ran with times, pass or fail, and the raw output that shows it:
    • the lines of ~/Library/Logs/Insomnia/insomnia.log for that run;
    • the samples for that run;
    • pmset -g log | grep -E ' (Sleep|Wake|DarkWake) ' for that time window, which shows whether the Mac slept;
    • the extra readings named in the check.
  • Not run. List anything you couldn't run, and why.

Post the results as a comment on this PR. Put long logs in a gist and link it. Run everything while you're with the Mac, on a stable, ventilated surface, never in a bag, and not while you have unsaved work open.

🤖 Generated with Claude Code

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