Conversation
The send goroutine's defer cancel() caused the receive goroutine to be killed immediately when stdin was /dev/null (e.g. when run non-interactively), resulting in no output being printed. Only the receive goroutine should drive termination. Signed-off-by: llogen <christoph.lange@blindspot.software>
There was a problem hiding this comment.
Pull request overview
This PR fixes dutctl run streaming RPC lifecycle so that stdin reaching EOF (e.g., non-interactive /dev/null) does not prematurely terminate the RPC receive path, ensuring server output is still printed.
Changes:
- Introduce a
recvDonesignal to let the receive goroutine drive overall termination. - Remove
cancel()from the send goroutine teardown to prevent stdin EOF from cancelling the run context. - Update the final wait/termination select to wait on receive completion instead of
runCtx.Done().
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Send routine — stdin EOF must not cancel the context; only the receive | ||
| // routine drives termination so that all server responses are processed. | ||
| go func() { | ||
| defer cancel() | ||
|
|
||
| reader := bufio.NewReader(app.stdin) | ||
|
|
||
| for { |
There was a problem hiding this comment.
Now that stdin EOF no longer cancels the context (good), the send routine still exits on io.EOF without half-closing the client->server request stream. Server-side code treats client stream closure (io.EOF on Receive) as the signal that no more requests are coming; consider explicitly closing the request side (e.g., stream.CloseRequest()/equivalent for connect.BidiStreamForClient) when stdin reaches EOF so the server can stop waiting for further client messages without relying on full context cancellation.
| // any worker to report an error. | ||
| select { | ||
| case <-runCtx.Done(): | ||
| case <-recvDone: |
There was a problem hiding this comment.
recvDone is closed unconditionally in the receive goroutine, including when that goroutine hits a non-EOF error and sends it on errChan. In that situation both recvDone and errChan can become ready, and the final select may pick recvDone and return nil, silently dropping the receive error (same for a send error racing with receive completion). Consider making the completion signal carry an error (e.g., recvDone chan error) or, at minimum, ensure the recvDone path checks/drains errChan before returning nil so real worker errors can’t be lost.
| case <-recvDone: | |
| case <-recvDone: | |
| // Prefer any pending worker error over a silent success. | |
| select { | |
| case err := <-errChan: | |
| if err != nil { | |
| return err | |
| } | |
| default: | |
| } |
|
The context is: Thus, I need to take a closer look, as this change touches a critical part of goroutine synchronization and cannot pull it in quickly. |
The send goroutine's defer cancel() caused the receive goroutine to be killed immediately when stdin was /dev/null (e.g. when run non-interactively), resulting in no output being printed. Only the receive goroutine should drive termination.