@@ -37,8 +37,9 @@ type Serial struct {
3737 Port string // Port is the path to the serial device on the dutagent.
3838 Baud int // Baud is the baud rate of the serial device. Is unset, DefaultBaudRate is used.
3939
40- expect * regexp.Regexp // expect is a pattern to match against the serial output.
41- timeout time.Duration // timeout is the maximum time to wait for the expect pattern to match.
40+ expect * regexp.Regexp // expect is a pattern to match against the serial output.
41+ timeout time.Duration // timeout is the maximum time to wait for the expect pattern to match.
42+ csiRemainder []byte // csiRemainder holds an incomplete CSI sequence carried over across buffer reads.
4243}
4344
4445// Ensure implementing the Module interface.
@@ -138,54 +139,73 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
138139 matchCh := make (chan struct {}, 1 )
139140
140141 // Forward client stdin to serial port.
141- // Strip ANSI CSI sequences from stdin, since the remote system may send terminal
142- // queries (e.g. DSR) that cause the local terminal to inject responses (e.g. CPR)
143- // into stdin, which would corrupt the serial input.
142+ // DSR suppression (preventing CPR injection) is handled on the output side by
143+ // filterOutputCSI, which strips non-SGR CSI sequences — including DSR (ESC[6n) —
144+ // from serial output before it reaches the client terminal. The terminal never sees
145+ // a DSR query and therefore never injects CPR responses into stdin.
144146 const stdinBufSize = 256
145147
146- go func () {
147- buf := make ([]byte , stdinBufSize )
148+ type readResult struct {
149+ data []byte
150+ err error
151+ }
148152
149- for {
150- nRead , err := stdin .Read (buf )
151- if err != nil {
152- select {
153- case <- done :
154- return // Run() exited — suppress spurious error log.
155- default :
156- }
153+ readResultCh := make (chan readResult , 1 )
157154
158- if ctx .Err () != nil {
159- return
160- }
155+ go func () { // inner goroutine — may outlive Run(); exits when stdinCh is eventually closed.
156+ buf := make ([]byte , stdinBufSize )
161157
162- log .Printf ("serial module: error reading from stdin: %v" , err )
158+ for {
159+ n , err := stdin .Read (buf )
160+ cp := make ([]byte , n )
161+ copy (cp , buf [:n ])
162+ readResultCh <- readResult {data : cp , err : err }
163163
164+ if err != nil {
164165 return
165166 }
167+ }
168+ }()
166169
167- data := stripCSI (buf [:nRead ])
168-
169- if len (data ) == 0 {
170- continue
171- }
172-
170+ go func () { // outer goroutine — exits promptly when done is closed.
171+ for {
173172 select {
174173 case <- done :
175174 return
176- default :
177- }
175+ case res := <- readResultCh :
176+ if res .err != nil {
177+ select {
178+ case <- done :
179+ return // Run() exited — suppress spurious error log.
180+ default :
181+ }
182+
183+ if ctx .Err () != nil {
184+ return
185+ }
186+
187+ log .Printf ("serial module: error reading from stdin: %v" , res .err )
188+
189+ return
190+ }
178191
179- _ , writeErr := port .Write (data )
180- if writeErr != nil {
181192 select {
182193 case <- done :
183- return // port closed on Run() exit — not an error.
194+ return
184195 default :
185- log .Printf ("serial module: error writing to serial port: %v" , writeErr )
186196 }
187197
188- return
198+ _ , writeErr := port .Write (res .data )
199+ if writeErr != nil {
200+ select {
201+ case <- done :
202+ return // port closed on Run() exit — not an error.
203+ default :
204+ log .Printf ("serial module: error writing to serial port: %v" , writeErr )
205+ }
206+
207+ return
208+ }
189209 }
190210 }
191211 }()
@@ -219,12 +239,14 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
219239 lineBuffer .Reset ()
220240 }
221241
222- var flushTimer * time. Timer
242+ var flushGen uint64
223243
224244 defer func () {
225- if flushTimer != nil {
226- flushTimer .Stop ()
227- }
245+ mutex .Lock ()
246+
247+ flushGen ++ // Invalidate any pending timer callbacks.
248+
249+ mutex .Unlock ()
228250 }()
229251
230252 const flushTimeout = 100 * time .Millisecond
@@ -236,13 +258,6 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
236258
237259 return nil
238260 case <- ctx .Done ():
239- mutex .Lock ()
240-
241- // Flush any remaining data before closing.
242- flushBuffer ()
243-
244- mutex .Unlock ()
245-
246261 if errors .Is (ctx .Err (), context .DeadlineExceeded ) {
247262 fmt .Fprintln (stdout , "\n --- Timeout reached, no match found ---" )
248263
@@ -269,19 +284,21 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
269284
270285 // Filter CSI sequences from serial output that could cause
271286 // cursor positioning artifacts. Color/style sequences (SGR)
272- // are preserved.
273- outData := filterOutputCSI (readBuffer [:sbytes ])
287+ // are preserved. Prepend any partial CSI sequence left over
288+ // from the previous read to handle sequences split across buffers.
289+ chunk := readBuffer [:sbytes ]
290+ if len (s .csiRemainder ) > 0 {
291+ chunk = append (s .csiRemainder , chunk ... )
292+ s .csiRemainder = nil
293+ }
294+
295+ outData := filterOutputCSI (chunk , & s .csiRemainder )
274296 if len (outData ) == 0 {
275297 continue
276298 }
277299
278300 mutex .Lock ()
279301
280- // Stop pending flush timer since we have new data.
281- if flushTimer != nil {
282- flushTimer .Stop ()
283- }
284-
285302 // Process the data read character by character
286303 for _ , b := range outData {
287304 lineBuffer .WriteByte (b )
@@ -312,10 +329,18 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
312329
313330 // If there's data remaining in the line buffer (no newline yet),
314331 // schedule a flush so prompts like "login: " appear promptly.
332+ // Each timer captures its generation; stale callbacks are no-ops.
315333 if lineBuffer .Len () > 0 {
316- flushTimer = time .AfterFunc (flushTimeout , func () {
334+ flushGen ++
335+ thisGen := flushGen
336+
337+ time .AfterFunc (flushTimeout , func () {
317338 mutex .Lock ()
318- flushBuffer ()
339+
340+ if flushGen == thisGen {
341+ flushBuffer ()
342+ }
343+
319344 mutex .Unlock ()
320345 })
321346 }
@@ -379,9 +404,13 @@ const csiPrefixLen = 2
379404// This prevents cursor positioning, screen clearing, and terminal query sequences
380405// from affecting the client terminal, while preserving colored output.
381406//
407+ // Incomplete CSI sequences at the end of data are stored in remainder so they
408+ // can be prepended to the next buffer read and reconstituted correctly.
409+ //
382410//nolint:cyclop,varnamelen
383- func filterOutputCSI (data []byte ) []byte {
411+ func filterOutputCSI (data []byte , remainder * [] byte ) []byte {
384412 result := make ([]byte , 0 , len (data ))
413+ * remainder = nil
385414
386415 for i := 0 ; i < len (data ); i ++ {
387416 if data [i ] == 0x1b && i + 1 < len (data ) && data [i + 1 ] == '[' {
@@ -397,7 +426,10 @@ func filterOutputCSI(data []byte) []byte {
397426 }
398427
399428 if j >= len (data ) {
400- // Incomplete sequence at end of buffer — drop it.
429+ // Incomplete sequence at end of buffer — carry it over to the next read.
430+ * remainder = make ([]byte , len (data )- i )
431+ copy (* remainder , data [i :])
432+
401433 break
402434 }
403435
@@ -417,34 +449,3 @@ func filterOutputCSI(data []byte) []byte {
417449
418450 return result
419451}
420-
421- // stripCSI removes ANSI CSI (Control Sequence Introducer) sequences from data.
422- // CSI sequences start with ESC[ (0x1b 0x5b), followed by parameter bytes (0x30-0x3f),
423- // intermediate bytes (0x20-0x2f), and a final byte (0x40-0x7e).
424- // This filters out terminal responses (like cursor position reports) that the local
425- // terminal injects into stdin when the remote system sends queries.
426- //
427- //nolint:varnamelen
428- func stripCSI (data []byte ) []byte {
429- result := make ([]byte , 0 , len (data ))
430-
431- for i := 0 ; i < len (data ); i ++ {
432- if data [i ] == 0x1b && i + 1 < len (data ) && data [i + 1 ] == '[' {
433- // Skip ESC[
434- i += 2
435-
436- // Skip parameter bytes (0x30-0x3f) and intermediate bytes (0x20-0x2f).
437- for i < len (data ) && data [i ] >= 0x20 && data [i ] < 0x40 {
438- i ++
439- }
440-
441- // i now points at the final byte (0x40-0x7e); the outer loop's
442- // i++ will advance past it on the next iteration.
443- continue
444- }
445-
446- result = append (result , data [i ])
447- }
448-
449- return result
450- }
0 commit comments