Skip to content

feat: Windows support for the bridge via herdr's named pipe#27

Closed
mikebenner wants to merge 2 commits into
AltanS:mainfrom
mikebenner:feat/windows-named-pipe-bridge
Closed

feat: Windows support for the bridge via herdr's named pipe#27
mikebenner wants to merge 2 commits into
AltanS:mainfrom
mikebenner:feat/windows-named-pipe-bridge

Conversation

@mikebenner

Copy link
Copy Markdown
Contributor

What

Runs the Collie bridge natively on Windows against herdr's Windows beta. One new file (bridge/dial.ts) plus routing the two Bun.connect sites in bridge/herdr-client.ts through it. No behavior change on Linux/macOS — off win32 the shim delegates straight to Bun.connect({unix}).

Why it works

herdr's Windows beta doesn't use AF_UNIX: the .sock path on disk is a small pointer file (<pid>:<start_ns>), and the actual transport is a named pipe whose name is the full socket path\\.\pipe\C:\Users\<you>\AppData\Roaming\herdr\herdr.sock. Bun.connect({unix}) can't dial named pipes, but Bun's node:net can, and it adapts cleanly to the same write/flush/end handler shape both call sites already use (the settled/fireDown re-entrancy guards work unchanged; the shim maps end() to destroy() so close stays prompt for the one-shot RPCs).

Scope / limitations

  • Bridge only. collie-ctl.sh, the systemd unit, and tailscale serve are untouched — on Windows I run bun run bridge/index.ts directly, in the README's Variant C posture (loopback bind, own ingress in front, COLLIE_PUBLIC_HOSTS pinned).
  • Multi-session discovery isn't adapted (POSIX path derivation), so COLLIE_MULTI_SESSION=off is the right setting on Windows.
  • herdr-plugin.toml platforms are untouched (the plugin actions are bash).

Tested

  • Live against herdr 0.7.4-preview (protocol 16) on Windows 11: session.snapshot polling, events.subscribe stream up + resubscribe on pane changes, pane reads and replies, web UI served — running as my daily driver behind a NetBird mesh.
  • bun run build (both typechecks) passes on Windows.
  • bun run test: 205 pass / 14 fail — identical counts on a pristine checkout of afa53fe; the 14 are pre-existing Windows-environment assumptions in tests (0600 permission fixtures, POSIX path fixtures), none in code this PR touches.

Versioning

Left the version/CHANGELOG for your release flow (matching how #22 landed) — happy to push a 0.15.0 bump + CHANGELOG entry citing the feature hash if you'd rather have it in the PR, and likewise a short README "Windows (experimental)" note.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VutEkP9A8oY72ZQBfFJdSw

herdr's Windows beta does not expose AF_UNIX: the .sock path on disk is
a <pid>:<start_ns> pointer file, and the real transport is a named pipe
whose name is the full socket path (\.\pipe\C:\...\herdr.sock).
Bun.connect({unix}) cannot open named pipes, but Bun's node:net can, so
the two dial sites in herdr-client.ts now route through bridge/dial.ts,
which adapts node:net to the same write/flush/end handler shape on
win32 and stays on Bun.connect everywhere else.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VutEkP9A8oY72ZQBfFJdSw
…ization, APPDATA default, tests

Review follow-ups from #25/#27 cross-review:
- dialHerdr exposes onDial(cancel) so a caller timeout that fires
  mid-connect aborts the pending dial instead of leaking the OS handle
  (both request() and subscribeEvents() wire it up); a dial destroyed
  before connect now settles its promise instead of pending forever.
- toPipeName() passes through already-prefixed pipe names (either slash
  direction) instead of double-prefixing.
- defaultSocketPath() resolves %APPDATA%\herdr\herdr.sock on win32 so
  HERDR_SOCKET_PATH is no longer required there; pure and unit-tested.
- New dial.test.ts: pure toPipeName cases everywhere, plus live named-
  pipe round-trip, mid-codepoint chunk split, connect-error, and cancel
  coverage on win32 (skipped elsewhere).
- SockHandle.end() documented as immediate destroy, not a drained close.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VutEkP9A8oY72ZQBfFJdSw
@mikebenner

Copy link
Copy Markdown
Contributor Author

Pushed 85a7411 addressing the gaps flagged in the #25/#27 cross-review: onDial cancellation so a timeout mid-connect can't leak a pending dial (wired into both request() and subscribeEvents()), toPipeName() normalization for already-prefixed pipe names, a win32 %APPDATA%\herdr default so HERDR_SOCKET_PATH is no longer required, and a new dial.test.ts with live named-pipe coverage on Windows (round-trip, mid-codepoint chunk split, connect error, cancel) — pure parts run everywhere, pipe tests skip off win32. Suite: 214 pass / same 14 pre-existing Windows fixture failures as main. Still deliberately bridge-only; happy to rebase onto or combine with #25's packaging if that's the direction you choose.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VutEkP9A8oY72ZQBfFJdSw

AltanS pushed a commit that referenced this pull request Jul 26, 2026
…ization, APPDATA default, tests

Review follow-ups from #25/#27 cross-review:
- dialHerdr exposes onDial(cancel) so a caller timeout that fires
  mid-connect aborts the pending dial instead of leaking the OS handle
  (both request() and subscribeEvents() wire it up); a dial destroyed
  before connect now settles its promise instead of pending forever.
- toPipeName() passes through already-prefixed pipe names (either slash
  direction) instead of double-prefixing.
- defaultSocketPath() resolves %APPDATA%\herdr\herdr.sock on win32 so
  HERDR_SOCKET_PATH is no longer required there; pure and unit-tested.
- New dial.test.ts: pure toPipeName cases everywhere, plus live named-
  pipe round-trip, mid-codepoint chunk split, connect-error, and cancel
  coverage on win32 (skipped elsewhere).
- SockHandle.end() documented as immediate destroy, not a drained close.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VutEkP9A8oY72ZQBfFJdSw
AltanS added a commit that referenced this pull request Jul 26, 2026
Windows support for the bridge via herdr's named pipe. Supersedes #25 and #27:
mikebenner's dial shim as the base (authorship preserved), plus the testability
and packaging work from bwright's PR.
@AltanS

AltanS commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Thanks, this is the right shape. One shim, one seam, HerdrClient untouched. The pointer-file plus full-path pipe name detail is what made the competing diagnosis in #25 make sense, and it matches the interprocess name mapping in the crate source.

Your onDial(cancel) also fixes a real pre-existing bug on Linux and macOS, not just Windows. A request that timed out mid-connect had no handle to end() and leaked the pending dial. That would have been worth a PR on its own.

Both your commits are cherry-picked onto main with authorship preserved.

What I added on top:

  • COLLIE_HERDR_DIAL=auto|net|bun, from bwright's PR. Forcing net runs your win32 branch on POSIX, since net.connect(path) takes an AF_UNIX path there. Your four live-dial tests were describe.skip off win32, so the code Windows depends on was unexercised on any machine that could review it. They now run everywhere, 6 pass and 0 skip, and sabotaging net.connect fails them.
  • Verified end to end against a live herd, both dialers, protocol 17: identical snapshots and events.subscribe acked over node:net.
  • The packaging you scoped out, .env.example, a README "Windows (experimental)" section, and an ARCHITECTURE note.
  • A note at the top of dial.ts recording that there is no handshake, sourced from the crate docs, so nobody re-derives it.

I left herdr-plugin.toml platforms alone, agreeing with your reasoning. The bridge runs on Windows, the launcher does not.

On your review of #25: it was accurate and well evidenced, and re-verifying the central claim before commenting was the right call. Thanks for doing that rather than just advocating for your own PR.

Merged to main as dd6610d. Closing this as merged via cherry-pick.

@AltanS AltanS closed this Jul 26, 2026
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