@@ -57,7 +57,7 @@ Input from the client is forwarded to the serial port, and output from the seria
5757If a regex is provided, the module will wait for the regex to match on the serial output,
5858then exit with success. If no expect string is provided, the module will read from the serial port
5959until it is terminated by a signal (e.g. Ctrl-C).
60- The expect string supports regular expressions according to [1].
60+ The expect string supports regular expressions according to [1].
6161The optional -t flag specifies the maximum time to wait for the regex to match.
6262Quote the expect string if it contains spaces or special characters. E.g.: "(?i)hello\s+world!? dutctl"
6363
@@ -100,7 +100,7 @@ func (s *Serial) Deinit() error {
100100 return nil
101101}
102102
103- //nolint:cyclop,funlen,gocognit
103+ //nolint:cyclop,funlen,gocognit,gocyclo,maintidx
104104func (s * Serial ) Run (ctx context.Context , session module.Session , args ... string ) error {
105105 log .Println ("serial module: Run called" )
106106
@@ -141,11 +141,13 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
141141 // Strip ANSI CSI sequences from stdin, since the remote system may send terminal
142142 // queries (e.g. DSR) that cause the local terminal to inject responses (e.g. CPR)
143143 // into stdin, which would corrupt the serial input.
144+ const stdinBufSize = 256
145+
144146 go func () {
145- buf := make ([]byte , 256 )
147+ buf := make ([]byte , stdinBufSize )
146148
147149 for {
148- n , err := stdin .Read (buf )
150+ nRead , err := stdin .Read (buf )
149151 if err != nil {
150152 select {
151153 case <- done :
@@ -162,7 +164,7 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
162164 return
163165 }
164166
165- data := stripCSI (buf [:n ])
167+ data := stripCSI (buf [:nRead ])
166168
167169 if len (data ) == 0 {
168170 continue
@@ -174,12 +176,13 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
174176 default :
175177 }
176178
177- if _ , err := port .Write (data ); err != nil {
179+ _ , writeErr := port .Write (data )
180+ if writeErr != nil {
178181 select {
179182 case <- done :
180183 return // port closed on Run() exit — not an error.
181184 default :
182- log .Printf ("serial module: error writing to serial port: %v" , err )
185+ log .Printf ("serial module: error writing to serial port: %v" , writeErr )
183186 }
184187
185188 return
@@ -192,17 +195,17 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
192195 readBuffer := make ([]byte , bufferSize )
193196 lineBuffer := & bytes.Buffer {}
194197
195- // mu protects lineBuffer which is accessed from the main loop
198+ // mutex protects lineBuffer which is accessed from the main loop
196199 // and the flush timer goroutine.
197- var mu sync.Mutex
200+ var mutex sync.Mutex
198201
199202 flushBuffer := func () {
200203 if lineBuffer .Len () == 0 {
201204 return
202205 }
203206
204207 line := lineBuffer .String ()
205- stdout .Write ([]byte (line )) //nolint:errcheck
208+ _ , _ = stdout .Write ([]byte (line )) // ChanWriter.Write always returns nil
206209
207210 if s .expect != nil && s .expect .MatchString (line ) {
208211 log .Printf ("serial module: pattern matched in flush" )
@@ -233,12 +236,12 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
233236
234237 return nil
235238 case <- ctx .Done ():
236- mu .Lock ()
239+ mutex .Lock ()
237240
238241 // Flush any remaining data before closing.
239242 flushBuffer ()
240243
241- mu .Unlock ()
244+ mutex .Unlock ()
242245
243246 if errors .Is (ctx .Err (), context .DeadlineExceeded ) {
244247 fmt .Fprintln (stdout , "\n --- Timeout reached, no match found ---" )
@@ -272,26 +275,31 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
272275 continue
273276 }
274277
275- mu .Lock ()
278+ mutex .Lock ()
276279
277280 // Stop pending flush timer since we have new data.
278281 if flushTimer != nil {
279282 flushTimer .Stop ()
280283 }
281284
282285 // Process the data read character by character
283- for i := range len (outData ) {
284- b := outData [i ]
286+ for _ , b := range outData {
285287 lineBuffer .WriteByte (b )
286288
287289 // If we reach a newline or a buffer limit, process the line
288290 if b == '\n' || lineBuffer .Len () >= 1024 {
289291 line := lineBuffer .String ()
290- stdout .Write ([]byte (line )) //nolint:errcheck
292+
293+ _ , writeErr := stdout .Write ([]byte (line ))
294+ if writeErr != nil {
295+ mutex .Unlock ()
296+
297+ return fmt .Errorf ("error writing to stdout: %w" , writeErr )
298+ }
291299
292300 // Check for regex match if we have one
293301 if s .expect != nil && s .expect .MatchString (line ) {
294- mu .Unlock ()
302+ mutex .Unlock ()
295303
296304 fmt .Fprintln (stdout , "\n --- Pattern matched, connection closed ---" )
297305
@@ -306,13 +314,13 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
306314 // schedule a flush so prompts like "login: " appear promptly.
307315 if lineBuffer .Len () > 0 {
308316 flushTimer = time .AfterFunc (flushTimeout , func () {
309- mu .Lock ()
317+ mutex .Lock ()
310318 flushBuffer ()
311- mu .Unlock ()
319+ mutex .Unlock ()
312320 })
313321 }
314322
315- mu .Unlock ()
323+ mutex .Unlock ()
316324 }
317325 }
318326}
@@ -363,17 +371,22 @@ func (s *Serial) openPort() (*serial.Port, error) {
363371 return port , nil
364372}
365373
374+ // csiPrefixLen is the length of the CSI prefix "ESC[".
375+ const csiPrefixLen = 2
376+
366377// filterOutputCSI removes CSI sequences from serial output data, except for
367378// SGR (Select Graphic Rendition, final byte 'm') which handles colors and styles.
368379// This prevents cursor positioning, screen clearing, and terminal query sequences
369380// from affecting the client terminal, while preserving colored output.
381+ //
382+ //nolint:cyclop,varnamelen
370383func filterOutputCSI (data []byte ) []byte {
371384 result := make ([]byte , 0 , len (data ))
372385
373386 for i := 0 ; i < len (data ); i ++ {
374387 if data [i ] == 0x1b && i + 1 < len (data ) && data [i + 1 ] == '[' {
375388 // Find the extent of this CSI sequence.
376- j := i + 2
389+ j := i + csiPrefixLen
377390
378391 for j < len (data ) && data [j ] >= 0x30 && data [j ] <= 0x3f {
379392 j ++ // parameter bytes
@@ -410,6 +423,8 @@ func filterOutputCSI(data []byte) []byte {
410423// intermediate bytes (0x20-0x2f), and a final byte (0x40-0x7e).
411424// This filters out terminal responses (like cursor position reports) that the local
412425// terminal injects into stdin when the remote system sends queries.
426+ //
427+ //nolint:varnamelen
413428func stripCSI (data []byte ) []byte {
414429 result := make ([]byte , 0 , len (data ))
415430
@@ -418,12 +433,13 @@ func stripCSI(data []byte) []byte {
418433 // Skip ESC[
419434 i += 2
420435
421- // Skip parameter and intermediate bytes, stop at final byte .
422- for i < len (data ) && data [i ] < 0x40 {
436+ // Skip parameter bytes (0x30-0x3f) and intermediate bytes (0x20-0x2f) .
437+ for i < len (data ) && data [i ] >= 0x20 && data [ i ] < 0x40 {
423438 i ++
424439 }
425- // i now points at the final byte (0x40-0x7e); skip it too.
426440
441+ // i now points at the final byte (0x40-0x7e); the outer loop's
442+ // i++ will advance past it on the next iteration.
427443 continue
428444 }
429445
0 commit comments