Skip to content

Commit 04810b8

Browse files
committed
fix: interactive serial sessions exit cleanly
The agent now closes the module stdin channel during session teardown, so a module blocked reading client input (such as the serial console) unblocks via EOF and the command returns instead of hanging. Signed-off-by: llogen <christoph.lange@blindspot.software>
1 parent ddb7085 commit 04810b8

2 files changed

Lines changed: 11 additions & 10 deletions

File tree

internal/dutagent/broker_test.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ func (s *testStream) Send(_ *pb.RunResponse) error {
3232

3333
func (s *testStream) Receive() (*pb.RunRequest, error) {
3434
if s.recvBlock {
35-
if s.unblockCh == nil {
36-
s.unblockCh = make(chan struct{})
37-
}
35+
// unblockCh must be created by the test before Start so this goroutine
36+
// only reads it, never writes it (a write here would race the test).
3837
<-s.unblockCh // will block until closed; simulates a long receive
3938
}
4039

@@ -132,7 +131,8 @@ func TestBroker_StdinForwarding(t *testing.T) {
132131
b := &Broker{}
133132
stdinPayload := []byte("user input")
134133
req := &pb.RunRequest{Msg: &pb.RunRequest_Console{Console: &pb.Console{Data: &pb.Console_Stdin{Stdin: stdinPayload}}}}
135-
stream := &testStream{recvReqs: []*pb.RunRequest{req}, recvErrs: []error{nil}} // after first req, EOF
134+
// No recvErrs: testStream returns reqs in order then EOF by default.
135+
stream := &testStream{recvReqs: []*pb.RunRequest{req}}
136136
ctx, cancel := context.WithCancel(context.Background())
137137
sess, errCh := b.Start(ctx, stream)
138138

@@ -144,8 +144,7 @@ func TestBroker_StdinForwarding(t *testing.T) {
144144
t.Fatalf("stdin mismatch: got %q want %q", string(data), string(stdinPayload))
145145
}
146146
case <-time.After(200 * time.Millisecond):
147-
// Expected to fail until refactor ensures proper sequencing / closure.
148-
// (Current code may work but channel closure semantics will still fail later.)
147+
t.Fatal("timeout: stdin payload was not forwarded to stdinCh")
149148
}
150149

151150
cancel() // simulate module completion
@@ -156,15 +155,13 @@ func TestBroker_StdinForwarding(t *testing.T) {
156155
// Cancellation during a blocked receive should terminate fromClientWorker without producing errors.
157156
func TestBroker_CancelDuringBlockedReceive(t *testing.T) {
158157
b := &Broker{}
159-
stream := &testStream{recvBlock: true}
158+
stream := &testStream{recvBlock: true, unblockCh: make(chan struct{})}
160159
ctx, cancel := context.WithCancel(context.Background())
161160
_, errCh := b.Start(ctx, stream)
162161

163162
// Cancel promptly, then unblock the fake receive so worker goroutine does not leak.
164163
cancel()
165-
if stream.unblockCh != nil {
166-
close(stream.unblockCh)
167-
}
164+
close(stream.unblockCh)
168165

169166
errs := collectErrors(t, errCh, 200*time.Millisecond)
170167
if len(errs) != 0 {

internal/dutagent/worker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ func toClientWorker(ctx context.Context, stream Stream, s *session) error {
100100
//
101101
//nolint:cyclop,funlen,gocognit
102102
func fromClientWorker(ctx context.Context, stream Stream, s *session) error {
103+
// Close stdinCh on exit so ChanReader.Read returns io.EOF and any module
104+
// goroutine blocked on stdin (e.g. the serial inner goroutine) unblocks cleanly.
105+
defer close(s.stdinCh)
106+
103107
type recvResult struct {
104108
req *pb.RunRequest
105109
err error

0 commit comments

Comments
 (0)