Conversation
…follow process logs
`process-compose process logs <proc>` (no --follow) prints
`write close: write unix ->/…/pc.sock: write: broken pipe`
on stderr after a successful log tail, and exits 0. The output is
correct but the noise makes every invocation look like a failure —
downstream tools that forward stderr surface it to end users.
Root cause: LogClient.CloseChannel unconditionally sends a WebSocket
CloseNormalClosure frame during teardown. In non-follow mode the
server (handleLog) hard-closes the underlying conn via
`defer ws.Close()` once the requested tail has streamed, so by the
time the CLI reaches its `<-done` branch and calls CloseChannel the
peer conn is already gone. The courtesy write returns EPIPE and gets
printed by
fmt.Fprintln(os.Stderr, "write close:", err)
PR F1bonacc1#216 (2024) moved this line from stdout to stderr but did not
address the underlying race.
Fix, two layers:
1. Primary — add readDone atomic.Bool set at readLogs exit (LIFO
defer ordering so it fires before close(done); a CloseChannel
racing on <-done observes the flag). CloseChannel checks it and
skips the courtesy write when the read side has already observed
the peer close.
2. Defense in depth — if the write is still attempted and fails with
a peer-closed error (syscall.EPIPE, io.EOF, net.ErrClosed, or a
*websocket.CloseError), classify it benign via isPeerClosedErr and
stay silent. Real errors surface exactly as before.
Errors from the final ws.Close are also swallowed — they indicate an
already-torn-down conn, not a client fault worth reporting.
Tests (src/client/logs_test.go):
- TestCloseChannel_NoStderrAfterPeerClose — end-to-end against a real
httptest WebSocket server that closes the conn the way handleLog
does.
- TestCloseChannel_SkipsWriteWhenReadClosed — whitebox: primary
readDone path.
- TestCloseChannel_SwallowsPeerClosedWrite — whitebox: defense-in-
depth path.
- TestIsPeerClosedErr — 9 subtests on the classification helper.
Verified end-to-end against a live session: non-follow `process logs`
now exits with clean stderr; --follow + Ctrl-C behavior unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Note on the SonarCloud Quality Gate failure — the two
My changes added imports and the Happy to defer on whether to mark them "Safe" / "Won't Fix" in SonarCloud, but they seem out of scope for this fix. |




Summary
process-compose process logs <proc>(no--follow) printson stderr after a successful log tail, and exits
0. The output on stdout is correct, but the noise makes every invocation look like it failed — downstream tools that forward stderr surface it to end users.Root cause
LogClient.CloseChannel(src/client/logs.go) unconditionally sends a WebSocketCloseNormalClosureframe during teardown. In non-follow mode the server (handleLoginsrc/api/ws_api.go) hard-closes the underlying conn viadefer ws.Close()once the requested tail has streamed, so by the time the CLI reaches its<-donebranch and callsCloseChannelthe peer conn is already gone. The courtesy write returnsEPIPEand gets printed by#216 (2024) moved this line from stdout to stderr but did not address the underlying race.
Fix — two layers
Errors from the final `ws.Close` are also swallowed — they indicate an already-torn-down conn, not a client fault worth reporting.
Tests
`src/client/logs_test.go`:
Verified end-to-end
Against a live process-compose instance running via a session manager that shells out to `process-compose process logs`: