Skip to content

Pin npipe round-trip and pipe lifecycle invariants#5216

Open
samuv wants to merge 6 commits intomainfrom
windows-pipe-test-hardening
Open

Pin npipe round-trip and pipe lifecycle invariants#5216
samuv wants to merge 6 commits intomainfrom
windows-pipe-test-hardening

Conversation

@samuv
Copy link
Copy Markdown
Contributor

@samuv samuv commented May 7, 2026

Summary

Follow-up to #5201 (review thread). Stacked on socker-windows; will rebase onto main once #5201 merges. Sibling to #5214 and #5215.

Adds three regression tests around the named-pipe transport that the original PR did not cover. None reproduce a live bug today; they pin invariants the named-pipe path implicitly relies on so a future change can't silently break them.

  • TestWriteReadServerInfo_NamedPipe (cross-platform) — closes the producer/consumer loop for the discovery file: an npipe:// URL written by socketURL must survive WriteServerInfo + ReadServerInfo without being mangled. The individual emit/parse pieces have their own tests; this one pins the seam.
  • TestSetupUnixSocket_NamedPipe_FirstInstanceWins (Windows-only) — asserts that two winio.ListenPipe calls on the same name fail the second time. winio sets FILE_FLAG_FIRST_PIPE_INSTANCE so two thv processes cannot bind the same pipe and race for traffic; if a future winio bump silently relaxed that, this test catches it before the discovery layer does in production.
  • TestCheckHealth_NamedPipe_HungServerCancelsOnContext (Windows-only) — covers the StateUnhealthy path: a peer that accepts connections but never responds must not wedge CheckHealth past the caller's context deadline. The existing success and not-found tests don't exercise this case.

Addresses inline review comments 3201085442 (round-trip), 3201085446 (first-instance-wins), and 3201085449 (hung-peer).

Type of change

  • Test coverage / regression hardening

Test plan

  • go test ./pkg/api/... ./pkg/server/discovery/... — green on macOS (round-trip and Unix tests).
  • task lint-fix — 0 issues.
  • GOOS=windows go vet and GOOS=windows go test -c -o /dev/null for both packages — both clean.
  • Manual go test -tags windows -run NamedPipe on a Windows host — pending; deferred to a reviewer with a Windows host. The Windows-only tests are guarded by build tags and do not run on macOS / Linux.

Does this introduce a user-facing change?

No.

Made with Cursor

samuv and others added 6 commits May 7, 2026 08:34
discovery.HTTPClientForURL knew http:// and unix:// only, so a
discovery file URL written for a Windows named pipe would be
unparseable. Every thv serve startup runs discovery.Discover to
detect a previous instance via the discovery URL, which routes
through HTTPClientForURL, so without an npipe arm two concurrent
thv processes on Windows could not see each other and would
silently overwrite the discovery file.

Add an npipe:// arm whose transport DialContext goes through
winio.DialPipeContext, plus a ParseNamedPipeURL helper that
strips the scheme and validates the remaining segment
(non-empty, no path separators, no ".."). Split the actual
dialer into pipe_windows.go (winio-backed) and pipe_other.go
(returns a clear "only supported on Windows" error), so
non-Windows builds do not import winio and a misconfigured
discovery URL surfaces a useful message instead of an obscure
dial syscall failure. go-winio is already a direct dependency
at v0.6.2, so no go.mod change is required.
setupUnixSocket assumed the address was a filesystem path -- it
ran os.Stat, os.Remove, os.MkdirAll, and os.Chmod around
net.Listen("unix", ...). None of that applies to a Windows named
pipe (\.\pipe\thv-api), and net.Listen("unix", ...) is not a
substitute since named pipes are a different kernel object. As a
result, toolhive-studio could not run thv with a pipe address on
Windows.

Split setupUnixSocket and cleanupUnixSocket into per-platform
files via build tags, mirroring pkg/container/docker/sdk. On
Windows, branch on the \.\pipe\ prefix and use winio.ListenPipe
with InputBufferSize and OutputBufferSize at 64 KiB; MessageMode
stays at byte-stream because HTTP requires byte framing. Skip
stat/remove/mkdir/chmod for pipes since they are not files;
keep stat/remove/mkdir for the AF_UNIX fallback (Win10 1803+)
but drop chmod because POSIX modes do not apply on Windows.

ListenURL emits npipe://<name> for pipe addresses so the
discovery file URL stays unambiguous. createListener labels the
listener as "Windows named pipe" and rejects pipe addresses on
non-Windows up front, rather than creating a literal-backslash
file via AF_UNIX.
TestParseNamedPipeURL covers the success case plus every
rejection branch (missing scheme, wrong scheme, empty name,
forward slash, backslash, ".."). A non-Windows guard test
asserts CheckHealth("npipe://...") surfaces a clear "Windows"
error rather than a misleading dial syscall failure.

The Windows-only health_windows_test.go spins up a winio
listener with an http.Server.Serve goroutine and asserts
CheckHealth("npipe://<name>", expectedNonce) succeeds with a
matching nonce, plus a not-found case that expects "health
check failed". An atomic counter disambiguates parallel pipe
names, since the Windows pipe namespace is global.
Cover the per-platform refactor of setupUnixSocket. On all
platforms, assert socketURL emits unix:// for filesystem
addresses and that isNamedPipeAddress detects \.\pipe\
prefixes. On Windows, additionally assert socketURL emits
npipe://<name>, that setupUnixSocket against a unique pipe
path returns a winio listener that accepts a winio dial within
a 2 s timeout (proving the listener is wired to the named-pipe
namespace, not AF_UNIX), and that cleanupUnixSocket no-ops for
pipe addresses without panicking.

Tests are split via build tags so the Windows test file is
only compiled on GOOS=windows and pulls in winio there.
Address aponcedeleonch's review on PR #5201:

- Update --socket help to mention named pipes and regen CLI docs.
- Promote namedPipePrefix to discovery.NamedPipePrefix as the
  canonical definition; pkg/api re-aliases it locally so listener
  and dialer cannot drift.
- Make isNamedPipeAddress case-insensitive via strings.EqualFold;
  the Windows pipe namespace is case-insensitive at the kernel
  layer, so \\.\Pipe\foo must not silently fall through to AF_UNIX.
- Collapse stat-then-remove into a single os.Remove that tolerates
  fs.ErrNotExist on both POSIX and the Windows AF_UNIX fallback.
- Close the listener and remove the socket file when Chmod fails,
  rather than leaking a bound AF_UNIX listener.
- Replace stdruntime.GOOS with a per-platform supportsNamedPipe()
  helper, dropping the runtime-package alias and its collision
  with pkg/container/runtime.
- Rename socket_other.{go,_test.go} to socket_unix.{go,_test.go}
  to match the client_unix/client_windows convention used by
  pkg/container/docker/sdk.
- Add TestCreateListener_NamedPipe_Unsupported to round out the
  listener-side reject coverage on non-Windows builds.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add three regression tests around the named-pipe transport that
the original PR did not cover. None of these reproduce live bugs
today; they pin invariants the named-pipe path implicitly relies
on so a future change can't silently break them.

TestWriteReadServerInfo_NamedPipe (cross-platform) closes the
producer/consumer loop for the discovery file: an npipe:// URL
written by socketURL must survive WriteServerInfo + ReadServerInfo
without being mangled. The individual emit/parse pieces have
their own tests; this one pins the seam.

TestSetupUnixSocket_NamedPipe_FirstInstanceWins (Windows-only)
asserts that two ListenPipe calls on the same name fail the
second time. winio sets FILE_FLAG_FIRST_PIPE_INSTANCE so that
two thv processes cannot bind the same pipe and race for traffic;
if a future winio bump silently relaxed that, this test catches
it before the discovery layer does in production.

TestCheckHealth_NamedPipe_HungServerCancelsOnContext
(Windows-only) covers the StateUnhealthy path: a peer that
accepts connections but never responds must not wedge
CheckHealth past the caller's context deadline. The existing
success and not-found tests don't exercise this case.

Co-authored-by: Cursor <cursoragent@cursor.com>
@samuv samuv requested review from JAORMX and amirejaz as code owners May 7, 2026 13:11
@samuv samuv self-assigned this May 7, 2026
@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label May 7, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.80%. Comparing base (785ddc3) to head (a9a8b72).

Additional details and impacted files
@@                Coverage Diff                 @@
##           socker-windows    #5216      +/-   ##
==================================================
- Coverage           67.81%   67.80%   -0.01%     
==================================================
  Files                 610      610              
  Lines               62256    62244      -12     
==================================================
- Hits                42216    42204      -12     
+ Misses              16867    16864       -3     
- Partials             3173     3176       +3     

☔ View full report in Codecov by Sentry.
📢 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.

Base automatically changed from socker-windows to main May 7, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant