Skip to content

Commit af2dd18

Browse files
committed
feat: review changes
Signed-off-by: llogen <christoph.lange@blindspot.software>
1 parent 0c18ab6 commit af2dd18

1 file changed

Lines changed: 117 additions & 98 deletions

File tree

pkg/module/serial/serial.go

Lines changed: 117 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
@@ -371,6 +396,9 @@ func (s *Serial) openPort() (*serial.Port, error) {
371396
return port, nil
372397
}
373398

399+
// escByte is the ASCII escape character that starts ANSI/VT escape sequences.
400+
const escByte = 0x1b
401+
374402
// csiPrefixLen is the length of the CSI prefix "ESC[".
375403
const csiPrefixLen = 2
376404

@@ -379,71 +407,62 @@ const csiPrefixLen = 2
379407
// This prevents cursor positioning, screen clearing, and terminal query sequences
380408
// from affecting the client terminal, while preserving colored output.
381409
//
410+
// Incomplete CSI sequences at the end of data are stored in remainder so they
411+
// can be prepended to the next buffer read and reconstituted correctly.
412+
//
382413
//nolint:cyclop,varnamelen
383-
func filterOutputCSI(data []byte) []byte {
414+
func filterOutputCSI(data []byte, remainder *[]byte) []byte {
384415
result := make([]byte, 0, len(data))
416+
*remainder = nil
385417

386418
for i := 0; i < len(data); i++ {
387-
if data[i] == 0x1b && i+1 < len(data) && data[i+1] == '[' {
388-
// Find the extent of this CSI sequence.
389-
j := i + csiPrefixLen
390-
391-
for j < len(data) && data[j] >= 0x30 && data[j] <= 0x3f {
392-
j++ // parameter bytes
393-
}
419+
if data[i] != escByte {
420+
result = append(result, data[i])
394421

395-
for j < len(data) && data[j] >= 0x20 && data[j] <= 0x2f {
396-
j++ // intermediate bytes
397-
}
422+
continue
423+
}
398424

399-
if j >= len(data) {
400-
// Incomplete sequence at end of buffer — drop it.
401-
break
402-
}
425+
// ESC at end of buffer: might be the start of a CSI sequence split across reads.
426+
if i+1 >= len(data) {
427+
*remainder = []byte{escByte}
403428

404-
if data[j] == 'm' {
405-
// SGR (colors/styles) — keep it.
406-
result = append(result, data[i:j+1]...)
407-
}
429+
break
430+
}
408431

409-
// All other CSI sequences are dropped.
410-
i = j
432+
if data[i+1] != '[' {
433+
// ESC not followed by '[' — not a CSI sequence, emit as-is.
434+
result = append(result, data[i])
411435

412436
continue
413437
}
414438

415-
result = append(result, data[i])
416-
}
439+
// CSI sequence: ESC [
440+
// Find the extent of this CSI sequence.
441+
j := i + csiPrefixLen
417442

418-
return result
419-
}
443+
for j < len(data) && data[j] >= 0x30 && data[j] <= 0x3f {
444+
j++ // parameter bytes
445+
}
420446

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))
447+
for j < len(data) && data[j] >= 0x20 && data[j] <= 0x2f {
448+
j++ // intermediate bytes
449+
}
430450

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
451+
if j >= len(data) {
452+
// Incomplete sequence at end of buffer — carry it over to the next read.
453+
*remainder = make([]byte, len(data)-i)
454+
copy(*remainder, data[i:])
435455

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-
}
456+
break
457+
}
440458

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
459+
if data[j] == 'm' {
460+
// SGR (colors/styles) — keep it.
461+
result = append(result, data[i:j+1]...)
444462
}
445463

446-
result = append(result, data[i])
464+
// All other CSI sequences are dropped.
465+
i = j
447466
}
448467

449468
return result

0 commit comments

Comments
 (0)