Skip to content

ci: reap leaked virtual-display helpers before create (unblock display jobs on fleet) - #6407

Merged
lawrencecchen merged 2 commits into
mainfrom
feat-vdisplay-reap-strays
Jun 19, 2026
Merged

lawrencecchen merged 2 commits into
mainfrom
feat-vdisplay-reap-strays

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

The two display-requiring jobs (tests-build-and-lag, ui-regressions) route to Warp because creating a CGVirtualDisplay appeared impossible on the self-hosted fleet. Root-caused on the macОS 26 minis: cvd creates reliably from the runner's own gui session (verified 3/3, display ids 15/17/19, no sudo). The real blocker is leaked helpers: a create-virtual-display process orphaned by a crashed/cancelled job keeps its display alive, and because only one CI virtual display identity can exist at a time, every subsequent create fails. Persistent runners accumulate these leaks; Warp VMs don't (fresh VM per job).

What

  • Add reap-strays to scripts/ci/virtual-display-lock.sh: kills orphaned create-virtual-display helpers. Token-gated (only runs while holding the host-global display lock, so any live helper is provably a leak) and compile-safe (excludes the clang … create-virtual-display.m build so a concurrent job's compile is never killed).
  • Call it in all three display-setup steps right after acquiring the lock, before launching the new helper.

No behavior change on Warp (no strays there). This unblocks pointing MACOS_RUNNER_DISPLAY at the fleet minis to drop Warp.

Tests

Extended tests/test_ci_virtual_display_lock.sh: reap-strays kills a leaked helper, preserves the clang compile, and refuses without the lock token. test_ci_self_hosted_guard.sh still passes.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
CI-only lock script and workflow hooks; no app runtime behavior, with tests covering token gating and compile exclusion.

Overview
Adds reap-strays to scripts/ci/virtual-display-lock.sh so jobs that hold the host-global display lock can kill orphaned create-virtual-display processes left behind when a prior job crashes or is cancelled. On persistent self-hosted macOS runners, those leaks keep a CGVirtualDisplay alive and block the next create; Warp VMs are unaffected.

Reaping is token-gated (requires the lock token) and compile-safe (finds helpers via ps full command lines, excluding clang … create-virtual-display.m and the lock script). It SIGTERMs strays, waits briefly, then SIGKILLs any survivors.

.github/workflows/ci.yml invokes reap-strays immediately after acquire in all three virtual-display setup paths (tests-build-and-lag, display-churn UI regression, persistent display for browser-find).

tests/test_ci_virtual_display_lock.sh now checks that reaping refuses without a token, kills a simulated stray helper, and does not kill a fake clang compile.

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


Summary by cubic

Reaps leaked virtual-display helpers before creating a new CGVirtualDisplay to prevent stuck jobs on persistent macOS runners, unblocking display jobs on the fleet. No-op on Warp VMs.

  • Bug Fixes
    • Added reap-strays to scripts/ci/virtual-display-lock.sh (token-gated, compile-safe) to kill orphaned create-virtual-display helpers, using ps -axww -o pid=,command= for consistent macOS/Linux detection.
    • Called reap-strays right after acquiring the display lock in all three display setup steps in .github/workflows/ci.yml.
    • Extended tests/test_ci_virtual_display_lock.sh to verify reaping behavior, token checks, compile exclusion, and the ps-based detection on the Linux guard host.

Written for commit 97a94ba. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Chores

    • Improved CI virtual-display lock handling by cleaning up leaked/orphaned helper processes immediately after lock acquisition.
    • Cleanup is now token-gated to avoid acting without the proper lock token, and it avoids interfering with unrelated compiler processes.
  • Tests

    • Added an end-to-end CI test covering the new cleanup behavior, including refusal without the token and safe killing of only the intended helper process.

On persistent self-hosted runners a CGVirtualDisplay helper orphaned by a
crashed or cancelled job keeps its display alive and blocks every later
create, because only one CI virtual display identity can exist at a time.
Warp VMs never hit this since each job gets a fresh VM, but the fleet
Macs do, which is why the two display jobs couldn't move off Warp.

Add a `reap-strays` subcommand to virtual-display-lock.sh that kills
orphaned create-virtual-display helpers. It is token-gated (only acts
while the caller holds the host-global display lock, so any live helper
is necessarily a leak) and excludes the clang compile of the source so a
concurrent job's build is never killed. Call it in all three display
setup steps (tests-build-and-lag, ui-regressions, persistent) right
after acquiring the lock, before launching the new helper.

No behavior change on Warp (no strays there); unblocks running the
display jobs on the fleet minis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 18, 2026 11:59pm
cmux-staging Building Building Preview, Comment Jun 18, 2026 11:59pm

@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20d1b518-39e1-42b4-acb0-b13fd1304e7e

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf1911 and 97a94ba.

📒 Files selected for processing (1)
  • scripts/ci/virtual-display-lock.sh

📝 Walkthrough

Walkthrough

Adds a reap-strays subcommand to virtual-display-lock.sh that enumerates and kills orphaned create-virtual-display helper processes (excluding clang compilations and the script itself), gated behind lock-token ownership. The CI workflow calls this in three macOS job steps immediately after acquiring the virtual display lock.

Changes

reap-strays subcommand and CI integration

Layer / File(s) Summary
stray_helper_pids and reap_strays implementation
scripts/ci/virtual-display-lock.sh
Adds stray_helper_pids() (PID enumeration filtering clang, .m source, and self), reap_strays() (token-verified graceful-then-kill -9 loop up to 50 retries), updated usage string, and reap-strays case in the CLI dispatcher.
End-to-end tests and CI workflow call sites
tests/test_ci_virtual_display_lock.sh, .github/workflows/ci.yml
Extends the lock test with token-gated refusal and compile-safe kill assertions using two spawned stray processes; adds `reap-strays

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A stray ghost process haunts the machine,
But now the rabbit sweeps the scene—
With token in paw and kill -9 in store,
No orphaned display leaks anymore.
Reap-strays hops in, the CI runs clean! 🌟


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Source Artifacts ❌ Error The commit adds 26 files in .claude/ (25 files) and .agents/ (1 file) directories, which are explicitly prohibited hidden scratch directories per `.github/review-bot-rules/source-control-artifa... Remove all files in .claude/ and .agents/ directories from the commit; add these patterns to .gitignore if not already present to prevent future accidental commits of AI agent workspace metadata.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding reap-strays to kill leaked virtual-display helpers before creating a new display, with a specific outcome (unblock display jobs on fleet).
Description check ✅ Passed The description includes a comprehensive 'Why' section explaining the root cause and motivation, a detailed 'What' section covering the implementation, and a 'Tests' section verifying the changes. However, it lacks the template sections for 'Demo Video', 'Review Trigger', and 'Checklist'.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PR contains no Swift code changes—only YAML workflow config and bash scripts. Custom check for Swift actor isolation is not applicable.
Cmux Swift Blocking Runtime ✅ Passed PR modifies only YAML workflow and shell scripts (.yml, .sh files), not Swift source. Custom check applies specifically to production Swift changes only.
Cmux Expensive Synchronous Load ✅ Passed This check applies to production Swift changes, but the PR only modifies YAML workflow and shell scripts; no Swift files are changed.
Cmux Cache Substitution Correctness ✅ Passed Custom check is not applicable: PR modifies only Bash shell scripts and YAML workflow files, not Swift/TypeScript/JavaScript code required by the cache-substitution-correctness check scope.
Cmux No Hacky Sleeps ✅ Passed Production sleep in reap_strays() is legitimate conditional polling for process termination (a real event), not a race patch. Test sleeps are allowed scaffolding.
Cmux Algorithmic Complexity ✅ Passed Code operates on system processes (100-500 items), not user-scalable collections; fixed 50-retry bound; called once per job setup, not in hot path; matches rule's exception for bounded small-input...
Cmux Swift Concurrency ✅ Passed PR modifies only bash scripts and CI workflow YAML; no Swift code changes, so concurrency check is not applicable.
Cmux Swift @Concurrent ✅ Passed PR contains only YAML and Bash script changes (CI workflows, shell scripts); no Swift code modifications present, making the Swift @concurrent annotation check inapplicable.
Cmux Swift File And Package Boundaries ✅ Passed PR contains no Swift files—only YAML workflow config and shell scripts. Check for Swift file/package boundaries is not applicable.
Cmux Swiftpm Lockfiles ✅ Passed PR modifies CI workflows and shell scripts for virtual display process management, with no SwiftPM package, Xcode project, .gitignore, or dependency changes; check does not apply.
Cmux Swift Logging ✅ Passed PR modifies only CI infrastructure (YAML workflow, Bash shell scripts); contains no Swift code. Swift logging rules apply exclusively to production Swift changes.
Cmux User-Facing Error Privacy ✅ Passed CI infrastructure scripts in scripts/ci/ are operational runbooks (allowed exception). New error messages contain no prohibited content: no vendor/provider names, env vars, credentials, or sensitiv...
Cmux Full Internationalization ✅ Passed All modified files (.github/workflows/ci.yml, scripts/ci/virtual-display-lock.sh, tests/test_ci_virtual_display_lock.sh) are CI operational infrastructure and test code excluded by the rule's allow...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code changes; modifications are limited to YAML workflows and Bash shell scripts for CI infrastructure and virtual display management.
Cmux Architecture Rethink ✅ Passed PR modifies only CI workflows and shell scripts, not Swift code; custom check explicitly applies to "Swift architecture changes" only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed This PR contains only CI/shell script changes (.github/workflows/ci.yml, scripts/ci/virtual-display-lock.sh, tests/test_ci_virtual_display_lock.sh). No Swift code is modified, so the auxiliary wind...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-vdisplay-reap-strays

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a reap-strays subcommand to virtual-display-lock.sh that kills orphaned create-virtual-display helper processes left behind by crashed or cancelled CI jobs, preventing them from blocking subsequent CGVirtualDisplay creation on persistent self-hosted macOS runners. The command is token-gated (requires holding the display lock) and compile-safe (excludes active clang builds of the helper source).

  • scripts/ci/virtual-display-lock.sh: Adds stray_helper_pids (filters ps output by command-line substring, excluding clang and this script itself) and reap_strays (SIGTERM → 5s poll → SIGKILL sequence, guarded by require_token_match).
  • .github/workflows/ci.yml: Inserts reap-strays || true immediately after lock acquisition in all three virtual-display setup paths (tests-build-and-lag, display-churn UI regressions, persistent display); env vars are exported before each call.
  • tests/test_ci_virtual_display_lock.sh: Covers token-refusal, stray-kill, and clang-compile-preservation cases using exec -a to simulate processes; the sleep 0.3 synchronization delay is test-only scaffolding.

Confidence Score: 5/5

Safe to merge; the reaping is scoped tightly by lock ownership and process-name filtering, with no behavioral change on Warp VMs

All three reap-strays call sites in ci.yml correctly export CMUX_VDISPLAY_LOCK_DIR and CMUX_VDISPLAY_LOCK_TOKEN before invoking the subprocess, so require_token_match will always have the token available. The stray_helper_pids filter is sound for macOS and Linux. Two existing review threads already track the sleep-poll pattern and post-SIGKILL verification; no new blocking concerns were found.

No files require special attention

Important Files Changed

Filename Overview
scripts/ci/virtual-display-lock.sh Adds reap_strays and stray_helper_pids functions; token-gated and compile-safe; logic is correct, previous review threads cover the sleep-poll and post-SIGKILL verification patterns
.github/workflows/ci.yml Inserts reap-strays call after lock acquisition in all three display-setup paths; env vars are properly exported before the subprocess call in every case
tests/test_ci_virtual_display_lock.sh Adds tests for reap-strays: token refusal, stray killing, and clang compile exclusion using exec -a to simulate processes; test scaffolding sleep is intentional

Reviews (2): Last reviewed commit: "ci: identify stray display helpers with ..." | Re-trigger Greptile

Comment on lines +239 to +243
for _ in $(seq 1 50); do
pids="$(stray_helper_pids)"
[ -n "$pids" ] || return 0
sleep 0.1
done

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.

P2 Fixed-sleep poll loop for process teardown synchronization

reap_strays uses a sleep 0.1 × 50 wall-clock poll to wait for SIGTERM to take effect on non-child processes, which falls under the cmux-runtime-no-hacky-sleeps rule for build/runtime scripts. In bash there is no POSIX-compatible alternative for waiting on an arbitrary non-child process (you cannot wait on a PID you didn't fork), so this pattern is the standard workaround — but consider wrapping it in a named helper (e.g., wait_for_pids_exit) with a clearly documented timeout contract, or using wait -n / lsof polling if a finer-grained cancellation hook is ever needed.

Rule Used: Flag fixed sleeps, delayed dispatch, timers, polli... (source)

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!

Comment on lines +226 to +248
reap_strays() {
require_token_match || exit 0
local pids
pids="$(stray_helper_pids)"
if [ -z "$pids" ]; then
echo "No stray virtual-display helpers to reap" >&2
return 0
fi
# shellcheck disable=SC2086
echo "Reaping stray virtual-display helpers: $(echo $pids | tr '\n' ' ')" >&2
# shellcheck disable=SC2086
kill $pids 2>/dev/null || true
local _
for _ in $(seq 1 50); do
pids="$(stray_helper_pids)"
[ -n "$pids" ] || return 0
sleep 0.1
done
# shellcheck disable=SC2086
echo "Force-killing remaining virtual-display helpers: $(echo $pids | tr '\n' ' ')" >&2
# shellcheck disable=SC2086
kill -9 $pids 2>/dev/null || true
}

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.

P2 reap_strays does not confirm stray PIDs are dead after kill -9

After the SIGKILL path, the function returns immediately with no check on whether the processes actually exited. In the overwhelmingly common case SIGKILL works instantly, but if a process is in an uninterruptible D-state (e.g., stuck on an NFS mount or kernel I/O), kill -9 will be silently accepted yet the process lives on. The new display helper would then be launched while the stray is still alive, reproducing exactly the leak-blocks-create scenario this PR is fixing. A brief post-SIGKILL stray_helper_pids check (with a warning but without aborting) would make the diagnosis clearer without changing the exit path.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/ci/virtual-display-lock.sh`:
- Around line 239-243: The reap_strays function uses a polling loop with sleep
0.1 as a synchronization mechanism, which violates the no-hacky-sleeps policy.
Replace the for loop (lines 239-243) that calls stray_helper_pids and sleeps
with a proper synchronization mechanism. Instead of polling with fixed delays,
use event-based or signal-based synchronization such as waiting for the
processes returned by stray_helper_pids to actually terminate, or checking
process existence directly without the sleep loop. This ensures the code waits
for actual completion rather than relying on timing assumptions.

In `@tests/test_ci_virtual_display_lock.sh`:
- Line 147: Remove the fixed sleep delays (sleep 0.3) from the reap-strays test
assertions at lines 147 and 164 in test_ci_virtual_display_lock.sh. Replace each
fixed sleep with a deadline-bounded polling loop that repeatedly checks a real
predicate condition until it becomes true or a timeout is reached, rather than
relying on wall-clock delays. This will make the test timing-independent and
less flaky while maintaining proper assertion synchronization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d55a778-dd87-4309-af8a-59f28f11a17b

📥 Commits

Reviewing files that changed from the base of the PR and between 784ed36 and 1cf1911.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • scripts/ci/virtual-display-lock.sh
  • tests/test_ci_virtual_display_lock.sh

Comment on lines +239 to +243
for _ in $(seq 1 50); do
pids="$(stray_helper_pids)"
[ -n "$pids" ] || return 0
sleep 0.1
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace wall-clock polling in reap_strays shutdown path.

Line 239-Line 243 uses sleep 0.1 polling as synchronization in production shell runtime. That violates the no-hacky-sleeps policy and can still be timing-fragile under load.

As per coding guidelines, “Do not use fixed delays (sleep, ... polling loops, or fixed backoff) as synchronization mechanisms in production ... shell code.”

Suggested patch
@@
-  local _
-  for _ in $(seq 1 50); do
-    pids="$(stray_helper_pids)"
-    [ -n "$pids" ] || return 0
-    sleep 0.1
-  done
+  pids="$(stray_helper_pids)"
+  [ -n "$pids" ] || return 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/virtual-display-lock.sh` around lines 239 - 243, The reap_strays
function uses a polling loop with sleep 0.1 as a synchronization mechanism,
which violates the no-hacky-sleeps policy. Replace the for loop (lines 239-243)
that calls stray_helper_pids and sleeps with a proper synchronization mechanism.
Instead of polling with fixed delays, use event-based or signal-based
synchronization such as waiting for the processes returned by stray_helper_pids
to actually terminate, or checking process existence directly without the sleep
loop. This ensures the code waits for actual completion rather than relying on
timing assumptions.

Source: Coding guidelines

STRAY_PID=$!
( exec -a "clang -framework CoreGraphics -o $TMP_DIR/create-virtual-display scripts/create-virtual-display.m" sleep 30 ) &
COMPILE_PID=$!
sleep 0.3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove fixed sleeps from reap-strays test assertions.

Line 147 and Line 164 rely on fixed wall-clock delays before assertions (sleep 0.3). This makes the test timing-dependent and flaky.

As per coding guidelines, “Tests must not introduce fixed sleep ... used to wait for async readiness before an assertion,” and “Deadline-bounded polls of a real predicate ... are allowed.”

Suggested patch
@@
-sleep 0.3
+for _ in $(seq 1 30); do
+  if kill -0 "$STRAY_PID" 2>/dev/null && kill -0 "$COMPILE_PID" 2>/dev/null; then
+    break
+  fi
+  sleep 0.1
+done
@@
-sleep 0.3
+for _ in $(seq 1 30); do
+  if ! kill -0 "$STRAY_PID" 2>/dev/null; then
+    break
+  fi
+  sleep 0.1
+done

Also applies to: 164-164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_ci_virtual_display_lock.sh` at line 147, Remove the fixed sleep
delays (sleep 0.3) from the reap-strays test assertions at lines 147 and 164 in
test_ci_virtual_display_lock.sh. Replace each fixed sleep with a
deadline-bounded polling loop that repeatedly checks a real predicate condition
until it becomes true or a timeout is reached, rather than relying on wall-clock
delays. This will make the test timing-independent and less flaky while
maintaining proper assertion synchronization.

Source: Coding guidelines

pgrep -fl prints the full argv on BSD/macOS but only the process name on
Linux, where the workflow-guard host runs the lock test. That made the
clang/.m exclusion silently no-op on Linux, so reap-strays killed the
compile fake and the guard test failed. Use `ps -axww -o pid=,command=`,
which yields the full command line identically on both platforms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen merged commit 7af0d2a into main Jun 19, 2026
21 checks passed
@lawrencecchen
lawrencecchen deleted the feat-vdisplay-reap-strays branch June 19, 2026 00:10
@lawrencecchen
lawrencecchen restored the feat-vdisplay-reap-strays branch July 18, 2026 10:19

This branch was successfully deployed

1 active deployment
Preview – cmux — 97a94bab Deployed Jun 18, 2026 by vercel[bot]
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.

1 participant