Skip to content

feat(#283): Windows dev orchestrator (setup-displayxr.bat) + README quick-install promotion#291

Merged
dfattal merged 4 commits into
mainfrom
feature/windows-dev-orchestrator
May 23, 2026
Merged

feat(#283): Windows dev orchestrator (setup-displayxr.bat) + README quick-install promotion#291
dfattal merged 4 commits into
mainfrom
feature/windows-dev-orchestrator

Conversation

@dfattal
Copy link
Copy Markdown
Collaborator

@dfattal dfattal commented May 23, 2026

Summary

Mirrors scripts/setup-displayxr.sh on Windows via scripts/setup-displayxr.bat. Same flag surface (--with mcp, --with-demos, --dry-run, --uninstall, --help), same warn-and-skip semantics when a pinned tag has no asset for the platform.

Default install set on Windows is runtime + Shell + Leia plug-in (all three have shipping .exe installers as of today). MCP Tools stays opt-in via --with mcp. macOS default remains runtime-only (Shell + Leia have no macOS port; Leia is Windows-only by design).

Closes the Windows half of #283.

Pin bumps

Component From To
runtime v1.4.0 v1.4.1 (first release with the macOS .pkg + the DisplayXRSetup-1.4.1.929.exe Windows installer)
leia_plugin sr-sdk-v1.35.0.2011 (CI build dependency, not a user installer) v1.0.2 (latest user-facing release: DisplayXRLeiaSRSetup-1.0.2.12.exe)

displayxr-leia-plugin v1.0.0 (2026-05-22) was the first user-facing Leia plug-in release; v1.0.2 (2026-05-23) is the latest. All ship DisplayXRLeiaSRSetup-*.exe.

Files

  • scripts/setup-displayxr.bat (new, ~340 lines) — Windows orchestrator. PowerShell shell-out for versions.json parsing; gh release download + start /wait "" "installer.exe" /S per component; registry-key verification (HKLM\Software\DisplayXR\...).
  • scripts/lib/components.sh — gains COMPONENT_INSTALL_MARKER_WINDOWS_* (registry key paths) parallel to existing MACOS markers. The .bat mirrors the table inline at its top because batch can't source bash; a header note in components.sh flags this.
  • versions.json — pinned at runtime v1.4.1, leia_plugin v1.0.2.
  • README.md — "Quick Start" now leads with parallel macOS + Windows one-liners. The manual 4-installer flow is demoted to a "Manual install" subsection below, restructured as a per-component / per-platform table that also covers the Leia plug-in (which the prior "3 installers" listing omitted).
  • docs/getting-started/full-stack-install.md — extends to cover both platforms: per-OS prereqs, per-OS verification, per-OS uninstall flow, and flips every "Windows today" cell in the availability matrix.

Platform availability matrix (post-PR)

Component macOS Windows
runtime yes (DisplayXR-Installer-*.pkg) yes (DisplayXRSetup-*.exe)
shell — (macOS port deferred, M6) yes (DisplayXRShellSetup-*.exe)
leia_plugin — (vendor SDK is Windows-only) yes (DisplayXRLeiaSRSetup-*.exe)
mcp_tools — (future) yes (DisplayXRMCPSetup-*.exe)

Windows uninstall design

--uninstall scans HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\* (plus the WOW6432Node mirror) for entries with Publisher=DisplayXR or DisplayName prefixed DisplayXR (catches Leia plug-in, which sets Publisher='Leia Inc.'), then runs each one's QuietUninstallString (falling back to UninstallString /S). One pass covers runtime, Shell, Leia plug-in, and MCP Tools regardless of exact registry subkey names or publisher field.

Bugs surfaced during E2E test (fixed in this PR)

Running the script against the real v1.4.1 / v1.2.5 / v1.0.2 / v0.3.1 stack — i.e. an actual repo path with spaces + the real release asset list — surfaced four pitfalls the prior cmd-correctness review couldn't have caught. Full transcripts in comment #4525971499. One-line summaries:

  1. read_pin: Get-Content -Raw 'PATH WITH SPACES' | ConvertFrom-Json inside for /f usebackq breaks PowerShell parsing. Switched to function-call form.
  2. asset-attached check: echo X | findstr ...$ injects a trailing space, breaking the $ anchor. Switched to a :~-4 substring check.
  3. uninstall filter: Publisher -eq 'DisplayXR' misses the Leia plug-in. Also match DisplayName -like 'DisplayXR *'.
  4. elevation preflight: was gated on ACTION=install only; --uninstall runs QuietUninstallString commands that hit HKLM + Program Files too. Gate now fires for both real install and real uninstall, still no-ops on --dry-run.

Cross-repo

  • DisplayXR/displayxr-mcp#3 — the MCP installer's uninstaller left an empty Capabilities parent key behind, preventing the runtime uninstaller's existing DeleteRegKey /ifempty HKLM "Software\DisplayXR" cleanup. PR adds DeleteRegKey /ifempty on the parent keys. Lands separately on the next MCP release; cosmetic until then (reg query HKLM\Software\DisplayXR returns success-with-empty-subkey instead of "key not found" — every functional registry trace is gone).

Test plan

  • bash side unbroken./scripts/setup-displayxr.sh --dry-run still shows runtime download cleanly.
  • python3 -m json.tool versions.json clean.
  • bash -n clean on .sh and components.sh.
  • All goto / call targets in .bat resolve to defined labels.
  • All if errorlevel checks inside parenthesized blocks use !errorlevel! (delayed expansion).
  • E2E install verification on Windows — ran all five steps (dry-runs, elevated install, registry / dir / log verification, --with mcp, full --uninstall round-trip → reinstall). Surfaced + fixed the four bugs above. Transcript: comment #4525971499.

🤖 Generated with Claude Code

dfattal added a commit that referenced this pull request May 23, 2026
Both released a couple of hours after PR #291 was opened. Bumping so
the first Windows e2e test exercises the freshest stack.

  runtime:     v1.4.0 -> v1.4.1   (DisplayXRSetup-1.4.1.929.exe / DisplayXR-Installer-1.4.1.pkg)
  leia_plugin: v1.0.1 -> v1.0.2   (DisplayXRLeiaSRSetup-1.0.2.12.exe)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dfattal
Copy link
Copy Markdown
Collaborator Author

dfattal commented May 23, 2026

Windows e2e test plan — for the Windows-device agent

This branch (feature/windows-dev-orchestrator) adds scripts\setup-displayxr.bat (Windows mirror of the macOS orchestrator). It has been validated for cmd.exe correctness (label/goto resolution, !errorlevel! inside parens, JSON parsing path) but never executed against real installers. Your job is to run it end-to-end against the freshly-released stack and report results.

Pinned in versions.json on this branch:

  • runtime → v1.4.1 (DisplayXRSetup-1.4.1.929.exe + DisplayXR-Installer-1.4.1.pkg)
  • shell → v1.2.5 (DisplayXRShellSetup-*.exe)
  • leia_plugin → v1.0.2 (DisplayXRLeiaSRSetup-1.0.2.12.exe)
  • mcp_tools → v0.3.1 (DisplayXRMCPSetup-*.exe)

Prereqs

where gh                                       :: must resolve
gh auth status                                 :: must say "Logged in to github.com"

If either fails: winget install --id GitHub.cli then gh auth login.

Step 0 — Wipe to a clean slate (optional but ideal)

Use Apps & Features (or appwiz.cpl) to uninstall any of: DisplayXR, DisplayXR Shell, DisplayXR Leia SR, DisplayXR MCP Tools. Skip if you want to test the upgrade path instead — the orchestrator should be idempotent.

Step 1 — Fetch + dry-run (non-elevated cmd is fine)

--dry-run does no privileged work; it validates that pin resolution, asset discovery, and download paths all work without touching the system.

git fetch origin
git checkout feature/windows-dev-orchestrator
git pull
scripts\setup-displayxr.bat --help
scripts\setup-displayxr.bat --dry-run
scripts\setup-displayxr.bat --dry-run --with mcp --with-demos

Expected from plain --dry-run:

==> runtime @ v1.4.1  (repo: DisplayXR/displayxr-runtime)
  (dry-run) would run: "<staging>\DisplayXRSetup-*.exe" /S
==> shell @ v1.2.5  (repo: DisplayXR/displayxr-shell-releases)
  (dry-run) would run: "<staging>\DisplayXRShellSetup-*.exe" /S
==> leia_plugin @ v1.0.2  (repo: DisplayXR/displayxr-leia-plugin)
  (dry-run) would run: "<staging>\DisplayXRLeiaSRSetup-*.exe" /S

Red flags to capture:

  • was unexpected at this time / is not recognized → cmd.exe parse error in the .bat
  • WARN: <component>: no Windows .exe asset attached → asset glob mismatch; verify the glob in scripts\setup-displayxr.bat matches the actual asset name on the release page
  • PowerShell errors from JSON parsing → likely a versions.json access issue

Step 2 — Real install (ELEVATED cmd)

Right-click cmd.exe → "Run as administrator" → cd to the repo.

scripts\setup-displayxr.bat

Expected output:

==> runtime @ v1.4.1  (repo: DisplayXR/displayxr-runtime)
==> Installing <path>\DisplayXRSetup-1.4.1.929.exe
 OK  runtime installed.
==> shell @ v1.2.5  (repo: DisplayXR/displayxr-shell-releases)
==> Installing <path>\DisplayXRShellSetup-...exe
 OK  shell installed.
==> leia_plugin @ v1.0.2  (repo: DisplayXR/displayxr-leia-plugin)
==> Installing <path>\DisplayXRLeiaSRSetup-1.0.2.12.exe
 OK  leia_plugin installed.

==> Smoke verification
 OK  Runtime registered.
 OK  Active OpenXR runtime registered.

 OK  DisplayXR dev box is ready.

Step 3 — Verify the install

:: Registry markers
reg query "HKLM\Software\DisplayXR\Runtime" /v InstallPath
reg query "HKLM\Software\DisplayXR\WorkspaceControllers\shell"
reg query "HKLM\Software\DisplayXR\DisplayProcessors\leia-sr"
reg query "HKLM\Software\Khronos\OpenXR\1" /v ActiveRuntime

:: File layout
dir "C:\Program Files\DisplayXR\Runtime"
:: Expect: DisplayXRClient.dll, displayxr-service.exe, displayxr-shell.exe,
:: Uninstall.exe, plus a plugins\ subtree containing the Leia DLL.

:: Test app launched via the shell against the SYSTEM runtime
:: (Build test apps first if not already: scripts\build_windows.bat test-apps)
"C:\Program Files\DisplayXR\Runtime\displayxr-shell.exe" test_apps\cube_handle_d3d11_win\build\cube_handle_d3d11_win.exe

Then open the latest log under %LOCALAPPDATA%\DisplayXR\DisplayXR_*.log and grep for the lines loaded from: (confirms which DLL got loaded — should be C:\Program Files\DisplayXR\Runtime\DisplayXRClient.dll) and any Leia plug-in load lines.

Step 4 — --with mcp opt-in

scripts\setup-displayxr.bat --with mcp
reg query "HKLM\Software\DisplayXR\Capabilities\MCP" /v Enabled
:: Expect: Enabled    REG_DWORD    0x1

Step 5 — Uninstall

scripts\setup-displayxr.bat --uninstall --dry-run
:: Lists every DisplayXR-published component that would be removed

scripts\setup-displayxr.bat --uninstall
:: Then verify
reg query "HKLM\Software\DisplayXR" 2>&1
:: Expect: ERROR: The system was unable to find the specified registry key

What to report back as a PR comment

  1. The exact transcript of Step 1, Step 2, Step 4, Step 5 (cmd /c "<command> > out.log 2>&1" makes capture easy).
  2. The Step 3 registry / dir / log greps.
  3. Any prompt that appeared (UAC dialogs, NSIS GUI windows that bypassed /S, etc.) — these are silent-install regressions in the individual installers, not the orchestrator, but worth flagging here so they're recorded.
  4. If anything fails: the full failing block + the relevant %LOCALAPPDATA%\DisplayXR\DisplayXR_*.log if a runtime / test-app step failed.

Known limitations to NOT report as bugs

  • Uninstall doesn't remove %REPO_ROOT%\demos\ — intentional, since it may carry local changes.
  • --with-demos clones but does not build — intentional, builds are too API-specific to safely automate in v1.
  • The script intentionally does not auto-elevate on a non-elevated launch (it errors with a clear message) — same UX trade-off as the macOS script's sudo requirement.

dfattal added a commit that referenced this pull request May 23, 2026
Re-running scripts\setup-displayxr.bat against the v1.4.1 / v1.2.5 /
v1.0.2 stack surfaced three cmd.exe / NSIS quoting hazards that the
prior cmd-correctness-only review couldn't catch:

1. read_pin: `Get-Content -Raw 'PATH WITH SPACES' | ConvertFrom-Json`
   inside `for /f usebackq` re-quotes the path such that PowerShell
   sees the space-split tokens as positional args after -Raw, fails
   to bind, and the whole pipeline aborts. Every pin reads as empty.
   Fix: function-call form `ConvertFrom-Json (Get-Content -Raw 'PATH')`
   avoids the pipe.

2. asset-attached check: `echo %%i | findstr /r ...^.exe$` — cmd's
   `echo X | cmd` form injects a trailing space before the pipe, so
   the `$` anchor never matches a real .exe asset. Every component
   ends up reported as "no Windows .exe asset attached". Fix: switch
   to a delayed-expansion substring check `if /i "!line:~-4!"==".exe"`.

3. uninstall filter: `Publisher -eq 'DisplayXR'` misses the Leia SR
   plug-in (its installer sets Publisher='Leia Inc.'), so --uninstall
   would silently leave the plug-in stranded. Fix: also match
   `DisplayName -like 'DisplayXR *'`.

End-to-end transcripts on the v1.4.1 stack now match the expected
output in the issue, including --with mcp opt-in and a clean
--uninstall round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dfattal
Copy link
Copy Markdown
Collaborator Author

dfattal commented May 23, 2026

Windows e2e test results (dfattal's dev box, Windows 11 26200.8246)

Ran the orchestrator end-to-end against the pinned v1.4.1 / v1.2.5 / v1.0.2 / v0.3.1 stack. Found three real bugs in scripts\setup-displayxr.bat and one in the MCP installer, all of which the prior cmd-correctness-only review couldn't have caught — they only surface against an actual repo path with spaces and a real release asset list.

Fixed in 0ad2b1d (this branch) and DisplayXR/displayxr-mcp#3. After the fixes, all five steps in the test plan match the expected output.

Bugs found + fixed

# Where Symptom Root cause Fix
1 read_pin (line 195) ERROR: versions.json has no pin for 'runtime' for every key; Get-Content : A positional parameter cannot be found that accepts argument '<path>' Get-Content -Raw 'C:\PATH WITH SPACES\versions.json' | ConvertFrom-Json inside for /f usebackq — cmd re-quoting splits the path on spaces, PowerShell sees the trailing tokens as positional args after -Raw, the pipeline aborts. Direct invocation works; for /f doesn't. Function-call form: ConvertFrom-Json (Get-Content -Raw 'PATH') — eliminates the pipe.
2 asset-attached check (lines 237–240) Every component reports WARN: <name> @ <tag>: no Windows .exe asset attached despite the .exe being there. echo %%i | findstr /r /c:"^DisplayXR.*\.exe$" — cmd's echo X | cmd form injects a trailing space before the pipe, so the $ anchor never matches a real .exe filename. Delayed-expansion substring check: set "_IC_LINE=%%i" && if /i "!_IC_LINE:~-4!"==".exe".
3 uninstall filter (lines 328, 331) --uninstall silently leaves DisplayXR Leia SR Plug-in stranded. Where-Object { $_.Publisher -eq 'DisplayXR' } misses the Leia plug-in — its installer sets Publisher='Leia Inc.'. Also match DisplayName -like 'DisplayXR *'.
4 displayxr-mcp installer reg query HKLM\Software\DisplayXR after full --uninstall returns success-with-empty-subkey instead of "key not found". MCP uninstaller deleted Capabilities\MCP but not the empty Capabilities parent. That empty parent then blocked the runtime uninstaller's existing DeleteRegKey /ifempty HKLM "Software\DisplayXR" cleanup. The original comment misread NSIS — /ifempty is the safe form (no-op when subkeys remain). Added DeleteRegKey /ifempty on both Capabilities and Software\DisplayXR parents in the MCP uninstaller. PR DisplayXR/displayxr-mcp#3.

Step 1 — --help / --dry-run (after fixes #1 + #2)

==> runtime @ v1.4.1  (repo: DisplayXR/displayxr-runtime)
  (dry-run) would run: "C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-51098497\runtime\DisplayXRSetup-*.exe" /S

==> shell @ v1.2.5  (repo: DisplayXR/displayxr-shell-releases)
  (dry-run) would run: "C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-51098497\shell\DisplayXRShellSetup-*.exe" /S

==> leia_plugin @ v1.0.2  (repo: DisplayXR/displayxr-leia-plugin)
  (dry-run) would run: "C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-51098497\leia_plugin\DisplayXRLeiaSRSetup-*.exe" /S

--dry-run --with mcp --with-demos additionally enumerates mcp_tools @ v0.3.1 and gh repo clone DisplayXR/displayxr-demo-gaussiansplat. The clone-demos --jq path with the embedded | works fine — that pipe stays inside gh's own argument; the for /f parse only mangles things when PowerShell is on the receiving end.

Step 2 — Real install (elevated)

Spawned via Start-Process -Verb RunAs -Wait so I could capture the transcript; behaves identically to a hand-launched elevated cmd.

==> runtime @ v1.4.1  (repo: DisplayXR/displayxr-runtime)
==> Installing C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-54063562\runtime\DisplayXRSetup-1.4.1.929.exe
 OK  runtime installed.
==> shell @ v1.2.5  (repo: DisplayXR/displayxr-shell-releases)
==> Installing C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-54063562\shell\DisplayXRShellSetup-1.2.5.7.exe
 OK  shell installed.
==> leia_plugin @ v1.0.2  (repo: DisplayXR/displayxr-leia-plugin)
==> Installing C:\Users\SPARKS~1\AppData\Local\Temp\dxr-setup-54063562\leia_plugin\DisplayXRLeiaSRSetup-1.0.2.12.exe
 OK  leia_plugin installed.

==> Smoke verification
 OK  Runtime registered.
 OK  Active OpenXR runtime registered.

 OK  DisplayXR dev box is ready.

Tested the upgrade path, not the clean-wipe path — runtime + shell were already at the pinned versions; Leia v1.0.1 → v1.0.2 upgrade succeeded.

Step 3 — Verify

Registry markers (via PowerShell — reg query from a 32-bit bash subshell hit WOW64 redirection on my setup, but PS reads the 64-bit view directly):

HKLM\Software\DisplayXR\Runtime                    Version=1.4.1   InstallPath=C:\Program Files\DisplayXR\Runtime
HKLM\Software\DisplayXR\WorkspaceControllers\shell Version=1.2.5
HKLM\Software\DisplayXR\DisplayProcessors\leia-sr  Version=1.0.2
HKLM\Software\Khronos\OpenXR\1                     ActiveRuntime=C:\Program Files\DisplayXR\Runtime\DisplayXR_win64.json

C:\Program Files\DisplayXR\Runtime\ contains the expected DisplayXRClient.dll, displayxr-service.exe, displayxr-shell.exe, Uninstall.exe, Uninstall-Shell.exe, and plugins\ subtree.

Cube test via the installed shell:

> "C:\Program Files\DisplayXR\Runtime\displayxr-shell.exe" <cube_handle_d3d11_win.exe>
DisplayXR Shell
Will launch 1 app(s)
 WARN [oxr_instance_create] DisplayXR runtime v1.4.1 'v1.4.1' loaded from: C:\Program Files\DisplayXR\Runtime\DisplayXRClient.dll (XR_RUNTIME_JSON=<unset>)
...
--- 2 client(s) connected ---
  [1] displayxr-shell (PID 22412)
  [2] SRCubeOpenXRExt (PID 39156)
Layout 'grid' (1 windows) — gliding over 300 ms

The loaded from: line confirms the installed DisplayXRClient.dll (not a dev tree) was resolved. ✓

Step 4 — --with mcp

==> mcp_tools @ v0.3.1  (repo: DisplayXR/displayxr-mcp)
==> Installing ...\DisplayXRMCPSetup-0.3.1.18.exe
 OK  mcp_tools installed.
...
 OK  DisplayXR dev box is ready.

HKLM\Software\DisplayXR\Capabilities\MCP\Enabled = 1 ✓ (also: AdapterPath, Version=0.3.1, InstallPath=C:\Program Files\DisplayXR\MCP).

Step 5 — Uninstall

--uninstall --dry-run (after fix #3) lists all 5 entries — and uncovered a stale ghost DisplayXR OpenXR Runtime (1.3.5) orphan in Add/Remove Programs from a previous install. The orchestrator picked it up correctly:

==> Discovering installed DisplayXR components...
  (dry-run) would uninstall: DisplayXR OpenXR Runtime (1.4.1)
  (dry-run) would uninstall: DisplayXR Leia SR Plug-in (1.0.2)
  (dry-run) would uninstall: DisplayXR MCP Tools (0.3.1)
  (dry-run) would uninstall: DisplayXR Shell (1.2.5)
  (dry-run) would uninstall: DisplayXR OpenXR Runtime (1.3.5)

Real --uninstall (elevated) succeeded for all 5:

==> Uninstalling DisplayXR OpenXR Runtime (1.4.1)
 OK  DisplayXR OpenXR Runtime uninstalled.
==> Uninstalling DisplayXR Leia SR Plug-in (1.0.2)
 OK  DisplayXR Leia SR Plug-in uninstalled.
==> Uninstalling DisplayXR MCP Tools (0.3.1)
 OK  DisplayXR MCP Tools uninstalled.
==> Uninstalling DisplayXR Shell (1.2.5)
 OK  DisplayXR Shell uninstalled.
==> Uninstalling DisplayXR OpenXR Runtime (1.3.5)
 OK  DisplayXR OpenXR Runtime uninstalled.

Post-uninstall state (with the unfixed v0.3.1 MCP installer still in place — orchestrator/runtime side is clean):

HKLM\Software\DisplayXR              → still exists (empty Capabilities subkey)  ← bug #4
HKLM\Software\Khronos\OpenXR\1       ActiveRuntime = unset  ✓
C:\Program Files\DisplayXR           empty  ✓

After fix #4 (MCP#3) ships in a future MCP release, reg query HKLM\Software\DisplayXR will return "key not found" as expected. Until then, the cosmetic empty parent is harmless — every functional registry trace is gone.

Then re-ran setup-displayxr.bat --with mcp to restore the dev box; smoke verification clean.

UAC behavior

Each elevated invocation pops one UAC dialog; no installer GUI bypassed /S. No /S regressions in any of the four NSIS installers. ✓

Side observations (not orchestrator bugs)

  • cmd /c reg query HKLM\Software\DisplayXR from a 32-bit bash subshell hit WOW64 redirection and reported "key not found" even though the keys were live in the 64-bit hive. PowerShell + /reg:64 both saw them fine. Diagnostic gotcha only, not a script bug.
  • The runtime uninstaller already does DeleteRegKey /ifempty HKLM "Software\DisplayXR" (line 997 of installer\DisplayXRInstaller.nsi), so once MCP#3 lands, the cleanup is order-independent.
  • The orchestrator's elevation preflight (net session check) only fires on ACTION=install, not on --uninstall. Real uninstall needs HKLM write access too — currently relies on the user spawning an elevated cmd themselves. Not fixed in 0ad2b1d; flagging for follow-up if desired.

dfattal added a commit that referenced this pull request May 23, 2026
The preflight `net session` elevation check was gated on
`ACTION=install`, so `--uninstall` would silently skip it.
But --uninstall runs each component's QuietUninstallString, which
the NSIS uninstallers execute against HKLM + Program Files — needs
admin just like install. Without elevation the cmd /c $u calls
either prompted UAC (interactive) or partial-failed silently.

Drop the ACTION=install qualifier; the guard now fires for both
real install and real uninstall, and still no-ops on --dry-run
(which does zero privileged work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dfattal added a commit to DisplayXR/displayxr-mcp that referenced this pull request May 23, 2026
`mcp_conn_close` calls `close(fd)` only — that wakes a blocked
`read()` in another thread on macOS but not on Linux. POSIX leaves
this undefined; Linux keeps the underlying socket alive until the
in-flight syscall returns, so close() from one thread is a no-op
for a read() blocked in another thread.

This is why the ctest smoke test has hung on Linux CI ever since
it was added (v0.2.3, 2026-05-04) while always passing on macOS in
~13s. Trace of the hang in the adapter:

  1. parent fcloses pipe → adapter's stdin EOF
  2. adapter's `up` pump thread exits cleanly
  3. main: pthread_join(t_up) → mcp_conn_close(conn)
  4. main: close(conn->fd) returns immediately, but the kernel
     does NOT unblock the `down` thread's read()
  5. main: pthread_join(t_down) → hangs forever
  6. adapter never exits → parent's waitpid hangs → CI timeout

`mcp_listener_close` already does the right thing
(`shutdown(SHUT_RDWR)` before `close()`, with a comment explaining
why for the listener's accept()). Apply the same pattern to
`mcp_conn_close` so it works symmetrically for read()/write().

shutdown(SHUT_RDWR) on a stream socket sends FIN to the peer (so
the server's read() in serve() also unblocks with EOF) AND wakes
the local fd's pending I/O syscalls. Works on AF_UNIX the same as
AF_INET. Behaves identically on macOS, so no regression there.

Surfaced by DisplayXR/displayxr-runtime#291 (Windows orchestrator
e2e test) while investigating why MCP PR #3's CI was failing on
Ubuntu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dfattal added a commit to DisplayXR/displayxr-mcp that referenced this pull request May 23, 2026
…tall

After the runtime orchestrator's `--uninstall` runs every
DisplayXR-published uninstaller, an empty `HKLM\Software\DisplayXR\
Capabilities` parent was left behind because the MCP uninstaller
deleted `Capabilities\MCP` but not the parent. That cascade also
prevents the runtime uninstaller's existing `DeleteRegKey /ifempty
HKLM "Software\DisplayXR"` at the end of its own uninstall section
from succeeding — the empty Capabilities subkey keeps the parent
alive, so `reg query HKLM\Software\DisplayXR` returns success
(with one empty subkey) instead of the expected "key not found".

The original comment "Don't DeleteRegKey /ifempty on Capabilities
itself or the parent — runtime / future Capabilities\<name> entries
may still own siblings" was based on a misreading of NSIS semantics:
`/ifempty` is precisely the safe form — it's a no-op when subkeys
still exist, so siblings are protected by construction.

Surfaced by issue DisplayXR/displayxr-runtime#291 (Windows
orchestrator e2e test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dfattal added a commit to DisplayXR/displayxr-mcp that referenced this pull request May 23, 2026
…tall

After the runtime orchestrator's `--uninstall` runs every
DisplayXR-published uninstaller, an empty `HKLM\Software\DisplayXR\
Capabilities` parent was left behind because the MCP uninstaller
deleted `Capabilities\MCP` but not the parent. That cascade also
prevents the runtime uninstaller's existing `DeleteRegKey /ifempty
HKLM "Software\DisplayXR"` at the end of its own uninstall section
from succeeding — the empty Capabilities subkey keeps the parent
alive, so `reg query HKLM\Software\DisplayXR` returns success
(with one empty subkey) instead of the expected "key not found".

The original comment "Don't DeleteRegKey /ifempty on Capabilities
itself or the parent — runtime / future Capabilities\<name> entries
may still own siblings" was based on a misreading of NSIS semantics:
`/ifempty` is precisely the safe form — it's a no-op when subkeys
still exist, so siblings are protected by construction.

Surfaced by issue DisplayXR/displayxr-runtime#291 (Windows
orchestrator e2e test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dfattal and others added 4 commits May 23, 2026 15:05
Mirrors scripts/setup-displayxr.sh on Windows via scripts/setup-displayxr.bat.
Same flag surface (--with mcp, --with-demos, --dry-run, --uninstall, --help),
same default-install semantics (warn-and-skip when a pinned tag has no
asset for the platform).

Default install set on Windows is runtime + Shell + Leia plug-in (all
three have shipping .exe installers). MCP Tools stays opt-in via
--with mcp. Mac default remains runtime-only (Shell + Leia have no
macOS port; Leia is Windows-only by design).

Pin bumps:
  leia_plugin: sr-sdk-v1.35.0.2011 -> v1.0.1

v1.0.0 was the first user-facing Leia plug-in release (2026-05-22);
v1.0.1 (2026-05-23) is the latest. Both ship DisplayXRLeiaSRSetup-*.exe.

components.sh gains COMPONENT_INSTALL_MARKER_WINDOWS_* (registry key
paths) parallel to the existing MACOS markers. The .bat mirrors the
table inline at its top because batch can't source bash; a header note
in components.sh flags this.

README "Quick Start" now leads with parallel macOS + Windows one-liners.
The manual 4-installer flow is demoted to a "Manual install" subsection
below, restructured as a per-component / per-platform table that also
covers the Leia plug-in (which the prior "3 installers" listing omitted).

docs/getting-started/full-stack-install.md extends to cover both
platforms: per-OS prereqs, per-OS verification, per-OS uninstall flow
(Windows uses a Publisher=DisplayXR scan of HKLM\...\Uninstall\*), and
flips every "Windows today" cell in the availability matrix.

Windows uninstall semantics: scan HKLM\Software\Microsoft\Windows\
CurrentVersion\Uninstall\* (+ WOW6432Node) for entries where
Publisher=DisplayXR, run each QuietUninstallString (falling back to
UninstallString /S). This catches runtime, Shell, Leia plug-in, and
MCP Tools in a single pass regardless of exact registry subkey names.

E2E verification still needs a Windows machine; bash-side smoke tests
re-confirm the macOS path is unbroken by the components.sh / README
edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both released a couple of hours after PR #291 was opened. Bumping so
the first Windows e2e test exercises the freshest stack.

  runtime:     v1.4.0 -> v1.4.1   (DisplayXRSetup-1.4.1.929.exe / DisplayXR-Installer-1.4.1.pkg)
  leia_plugin: v1.0.1 -> v1.0.2   (DisplayXRLeiaSRSetup-1.0.2.12.exe)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-running scripts\setup-displayxr.bat against the v1.4.1 / v1.2.5 /
v1.0.2 stack surfaced three cmd.exe / NSIS quoting hazards that the
prior cmd-correctness-only review couldn't catch:

1. read_pin: `Get-Content -Raw 'PATH WITH SPACES' | ConvertFrom-Json`
   inside `for /f usebackq` re-quotes the path such that PowerShell
   sees the space-split tokens as positional args after -Raw, fails
   to bind, and the whole pipeline aborts. Every pin reads as empty.
   Fix: function-call form `ConvertFrom-Json (Get-Content -Raw 'PATH')`
   avoids the pipe.

2. asset-attached check: `echo %%i | findstr /r ...^.exe$` — cmd's
   `echo X | cmd` form injects a trailing space before the pipe, so
   the `$` anchor never matches a real .exe asset. Every component
   ends up reported as "no Windows .exe asset attached". Fix: switch
   to a delayed-expansion substring check `if /i "!line:~-4!"==".exe"`.

3. uninstall filter: `Publisher -eq 'DisplayXR'` misses the Leia SR
   plug-in (its installer sets Publisher='Leia Inc.'), so --uninstall
   would silently leave the plug-in stranded. Fix: also match
   `DisplayName -like 'DisplayXR *'`.

End-to-end transcripts on the v1.4.1 stack now match the expected
output in the issue, including --with mcp opt-in and a clean
--uninstall round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The preflight `net session` elevation check was gated on
`ACTION=install`, so `--uninstall` would silently skip it.
But --uninstall runs each component's QuietUninstallString, which
the NSIS uninstallers execute against HKLM + Program Files — needs
admin just like install. Without elevation the cmd /c $u calls
either prompted UAC (interactive) or partial-failed silently.

Drop the ACTION=install qualifier; the guard now fires for both
real install and real uninstall, and still no-ops on --dry-run
(which does zero privileged work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dfattal dfattal force-pushed the feature/windows-dev-orchestrator branch from fb902f6 to ace8b93 Compare May 23, 2026 22:05
@dfattal dfattal merged commit 77df574 into main May 23, 2026
4 checks passed
dfattal added a commit that referenced this pull request May 23, 2026
Both released a couple of hours after PR #291 was opened. Bumping so
the first Windows e2e test exercises the freshest stack.

  runtime:     v1.4.0 -> v1.4.1   (DisplayXRSetup-1.4.1.929.exe / DisplayXR-Installer-1.4.1.pkg)
  leia_plugin: v1.0.1 -> v1.0.2   (DisplayXRLeiaSRSetup-1.0.2.12.exe)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dfattal added a commit that referenced this pull request May 23, 2026
Re-running scripts\setup-displayxr.bat against the v1.4.1 / v1.2.5 /
v1.0.2 stack surfaced three cmd.exe / NSIS quoting hazards that the
prior cmd-correctness-only review couldn't catch:

1. read_pin: `Get-Content -Raw 'PATH WITH SPACES' | ConvertFrom-Json`
   inside `for /f usebackq` re-quotes the path such that PowerShell
   sees the space-split tokens as positional args after -Raw, fails
   to bind, and the whole pipeline aborts. Every pin reads as empty.
   Fix: function-call form `ConvertFrom-Json (Get-Content -Raw 'PATH')`
   avoids the pipe.

2. asset-attached check: `echo %%i | findstr /r ...^.exe$` — cmd's
   `echo X | cmd` form injects a trailing space before the pipe, so
   the `$` anchor never matches a real .exe asset. Every component
   ends up reported as "no Windows .exe asset attached". Fix: switch
   to a delayed-expansion substring check `if /i "!line:~-4!"==".exe"`.

3. uninstall filter: `Publisher -eq 'DisplayXR'` misses the Leia SR
   plug-in (its installer sets Publisher='Leia Inc.'), so --uninstall
   would silently leave the plug-in stranded. Fix: also match
   `DisplayName -like 'DisplayXR *'`.

End-to-end transcripts on the v1.4.1 stack now match the expected
output in the issue, including --with mcp opt-in and a clean
--uninstall round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dfattal dfattal deleted the feature/windows-dev-orchestrator branch May 23, 2026 22:05
dfattal added a commit that referenced this pull request May 23, 2026
Picks up:
- CI(#290) attaches release artifacts via softprops/action-gh-release
- ci: expose GH_TOKEN env to BuildInstaller for release attach (6257132)

No installer behavior change vs v1.4.1; the orchestrator's e2e
transcripts on PR #291 stay valid against v1.4.2 (NSIS /S path
unchanged, registry layout unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dfattal
Copy link
Copy Markdown
Collaborator Author

dfattal commented May 23, 2026

Post-merge follow-ups

PR shipped to main as 77df574. Side-effects + cleanup:

Done after merge

Open follow-ups — heads-up for other agents

Docs / website coordination. The orchestrator is dev-facing (script-based). #284 is the end-user meta-installer (single .exe / .pkg bundle, separate displayxr-installer repo). They're complementary, not overlapping, but the docs surface mostly is:

Surface What this PR updated What #284 will need to add Risk of double-edit
README.md Quick Start ✅ leads with macOS + Windows orchestrator one-liners — (end users go to the website, not the README) low
docs/getting-started/full-stack-install.md ✅ covers both platforms via orchestrator will gain a "Try the meta-installer instead" pointer low — additive
displayxr.org download page — (untouched by this PR) will own this surfacedisplayxr.org/download/{macos,windows} → bundle release page wait for #284
Per-component install instructions on each individual release page — (orchestrator handles this case-by-case) — (untouched) none

Recommendation: do not update the website now; wait for #284 to land. Website copy will need to ship the unsigned-bundle bypass instructions from #284's body (one-time SmartScreen / Gatekeeper prompt), so it's better to update once when #284 ships than to update twice.

The dev-facing README + install doc are already updated by this PR.

Latent items not addressed here

  • --uninstall does not auto-elevate on a non-elevated launch (it errors with a clear message instead). Same UX trade-off as macOS sudo. Could be revisited if user reports come in.
  • Stale Add/Remove-Programs ghost entries from prior installs are picked up correctly by the DisplayName -like 'DisplayXR *' filter; no orphan cleanup needed.

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