Skip to content

Lock menu.ps1's Hub Cache Through hub.lock So It Coordinates with menu.sh - #2537

Merged
ptr727 merged 3 commits into
developfrom
fix/menu-hub-lock-coordination
Oct 8, 2026
Merged

ptr727 merged 3 commits into
developfrom
fix/menu-hub-lock-coordination

Conversation

@ptr727

@ptr727 ptr727 commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Summary

Settles #1094. menu.ps1 serialized the shared hub cache with a named .NET Mutex, while menu.sh takes a flock on $DIR/hub.lock, so a menu of each kind sharing one cache directory never saw the other's lock.

  • menu.ps1 now locks hub.lock itself, opened with FileShare.None, the shape bootstrap.ps1's Lock-Directory already uses. .NET implements that as flock(LOCK_EX|LOCK_NB) on Linux, which conflicts with menu.sh's shared reader lock and its exclusive writer lock in both directions. On Windows it refuses any other open handle on the file. Every caller still takes one exclusive lock, as with the mutex.
  • The open does not block on either platform, so a held lock is waited out by retrying. It retries only where the HResult says the file is held, measured on pwsh 7.4: 0x80070020 (low word 32, or 33 for a lock violation) on Windows, and raw errno 11 (EWOULDBLOCK) on Linux. Any other failure, the cache directory's own creation included, fails the task and returns the caller's own failure value. A $false there would read as exit code 0 to a caller comparing with -eq.
  • The lock is re-entrant within a session. Invoke-FetchHub is reached through Confirm-HubRoot from inside a span that already holds it, which the mutex allowed and a second open of the file would not.
  • -DryRun runs unlocked, matching menu.sh's hub_read_lock_acquire, since a dry run never fetches or reads the cache and must not create the directory or the lock file.

Verification

  • TestPowerShellMenuHubLock, 7 cases. On Linux (pwsh 7.4.2 in the offline python:3.12-slim image): all 7 pass, including the two cross-tool cases, a shared flock making the menu wait and the menu's lock refusing a shared flock. On Windows (pwsh 7, from Git Bash): 5 pass, and the 2 Linux-only cases skip.
  • Mutations, each failing the suite: FileShare.Read in place of None (3 failures on Linux), the re-entrancy guard removed (the nested case times out), and the New-Item catch removed.
  • tests.test_bootstrap passes on Windows. ruff, mypy, PSScriptAnalyzer, the prose gate, and the EOL gate are clean.
  • Three local strict-review passes. The first two raised five findings: dry run writing files, an unbounded wait on any plain IOException, an uncaught directory-creation failure ending the session, and two test gaps. Four were fixed in this branch. One was declined: a test for a non-contention IOException, which no portable fixture produces. The wait tests cover that classification from the other side. The third pass raised nothing.

The comments label stands for the new lock's comments.

Closes #1094 on promotion.

🤖 Generated with Claude Code

ptr727 and others added 3 commits October 8, 2026 07:12
…u.sh

menu.ps1 serialized its hub cache with a named .NET Mutex while menu.sh
takes a flock on hub.lock, so a menu of each kind sharing one directory
never saw the other's lock. menu.ps1 now opens hub.lock with
FileShare.None, which .NET implements as an exclusive flock on Linux and
which Windows refuses to any other open handle, retrying while another
session holds it.

The lock is re-entrant within a session, since Invoke-FetchHub is reached
through Confirm-HubRoot from inside a span that already holds it, which the
Mutex allowed and a second open of the file would not.

Closes #1094.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dry run never fetches or reads the hub cache, so taking the lock there
created the cache directory and its lock file on a host where neither
existed, which -DryRun promises not to do. menu.sh already skips its lock
under --dry-run for the same reason.

Lock-Hub now waits only where the open failed because another session
holds the file, read from the HResult, a sharing or lock violation on
Windows and EWOULDBLOCK on Linux. A full disk or a read-only mount is a
plain IOException too, and was waited on forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lock-Hub creates the cache directory before opening its lock file, and a
refusal there threw past the open's own catch, ending the whole menu
session where it should fail only the task, as an unopenable lock file
already does. Before the file lock, reader tasks never touched the
directory at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 8, 2026 14:28
@ptr727 ptr727 added the comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses label Oct 8, 2026
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7be076ac-fcfa-4fa4-8114-6855b43994dd
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.40%. Comparing base (afc77cb) to head (1706fbd).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #2537   +/-   ##
========================================
  Coverage    58.40%   58.40%           
========================================
  Files           16       16           
  Lines         8090     8090           
========================================
  Hits          4725     4725           
  Misses        3365     3365           
Flag Coverage Δ
python-3.13 58.40% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Lock acquisition can wait indefinitely without a timeout or cancellation path.

1 open finding
What changed in this PR

Updates menu.ps1 to coordinate shared hub-cache locking with menu.sh through hub.lock.

Changes:

  • Replaces the named mutex with an exclusive file lock.
  • Adds retry, failure, dry-run, and re-entrancy handling.
  • Adds cross-tool locking and failure-path tests.
File Summary
tests/​test_bootstrap.py Adds coverage for PowerShell locking behavior and interoperability.
host-setup/​menu.ps1 Implements shared cache locking and error handling.

🧠 Review effort: Lite


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread host-setup/menu.ps1
@ptr727
ptr727 merged commit 382c0cc into develop Oct 8, 2026
13 checks passed
@ptr727
ptr727 deleted the fix/menu-hub-lock-coordination branch October 8, 2026 14:38
ptr727 added a commit that referenced this pull request Oct 8, 2026
…2538)

## Summary

Promotes one change from `develop`.

- #2537: `host-setup/menu.ps1` now locks the shared hub cache through
`$DIR/hub.lock` with `FileShare.None`, in place of a named .NET `Mutex`,
so it coordinates with `menu.sh`'s `flock` on the same file. .NET
implements `FileShare.None` as an exclusive `flock` on Linux, and on
Windows it refuses any other open handle. The lock is re-entrant within
a session and skipped under `-DryRun`, matching `menu.sh`. It waits only
where the open's HResult says the file is held. Any other failure fails
the task, using the value the task itself returns on failure.

Closes #1094.

## Verification

- `TestPowerShellMenuHubLock` passed all 7 cases on Linux, in pwsh
7.4.2. That includes the two cross-tool `flock` cases. On Windows 5
cases ran and passed, and the 2 Linux-only cases were skipped. Three
mutations each failed the suite.
- #2537 took three recorded local strict-review passes. Copilot ran one
round on its head. That round raised one finding, an unbounded lock
wait, which was declined with evidence and resolved. The base's
`WaitOne()` and `menu.sh`'s `flock` block with no timeout too, and a
hold as long as an OS upgrade is the documented case. The maintainer
approved the merge over #2480's unparsed overview after the overview was
read by hand.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ptr727 added a commit that referenced this pull request Oct 8, 2026
… Its Token Only Once Written (#2548)

## Summary

Answers the Copilot finding on promotion PR #2545: "`menu.sh` can ignore
marker-write failures and leave fetched trees behind". It also fixes
that finding's siblings in the same fetch functions of both menus. Refs
#1093.

- **Marker write.** `menu.sh`'s `fetch_hub_locked` runs with errexit
suspended (`fetch_hub_locked || rc=$?`), so a failed write to
`hub.owned` let the clone go ahead unmarked. Cleanup then skipped that
tree, and every later fetch refused it as a tree this run did not
create. The write now fails the fetch before the clone. In `menu.ps1`, a
`Set-Content` failure threw past the task. It now fails the task, the
same way #2537 made a lock that cannot be taken fail it.
- **Old-tree removal.** A file held under the old tree made `menu.ps1`'s
`Remove-Item` throw past the task and end the menu session. In `menu.sh`
it made the clone run into the surviving tree, and the clone-failure
path then dropped the marker. Both now fail the fetch and leave the tree
and its marker as they were, so the owning session's exit cleanup can
still remove them.
- **When the token becomes the session's.** Each fetch builds its token
in a local variable and assigns it to the session only once the marker
holds it. Before this, a fetch that failed after generating its token
replaced the session's own token, and the session's exit cleanup no
longer matched the tree its earlier fetch had marked.

## Verification (Windows host)

- New failure cases, one per path per menu: token read in bash, old-tree
removal, and marker write. Each stubs or provokes the failure, then
asserts the task fails, nothing is cloned, the marker is kept where it
should be, and the session's token is unchanged. The `menu.ps1` stubs
shadow the cmdlet in the harness body, so each case reaches its `catch`.
- Mutation-checked against the code before each fix. The menus from
`develop` fail both write cases. The removal guards' parent fails both
removal cases. The early token assignment fails 4 cases.
- Full suite under Git Bash: 2275 tests, OK, 100 skipped. ruff, mypy,
the prose gate and the EOL gate pass, and so do shellcheck, shfmt and
PSScriptAnalyzer through `scripts/docker_lint.py`.
- Three local strict-review passes. The second raised one finding, the
early token assignment, which is fixed in 31a69ad. The third raised
none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants