@@ -55,11 +55,24 @@ const deviceLossGraceDefault = 2 * time.Second
5555// staying far larger than any realistic prompt or expect pattern.
5656const maxMatchWindow = 64 * 1024
5757
58- // expectSendPair holds a compiled regex pattern and the bytes to send when it matches.
59- // A nil or empty response means the module exits on match without sending anything.
60- type expectSendPair struct {
61- pattern * regexp.Regexp
62- response []byte
58+ // stepKind distinguishes the two kinds of step in a serial sequence.
59+ type stepKind int
60+
61+ const (
62+ stepExpect stepKind = iota // wait until pattern matches the serial output
63+ stepSend // write data to the serial port
64+ )
65+
66+ // seqStep is one step of a serial automation sequence. The module walks the
67+ // steps in order: it waits for each expect-step's pattern to appear in the
68+ // output, and writes each send-step's data to the port. A single expect
69+ // pattern and the legacy expect-send pairs both compile down to a slice of
70+ // these, so the run loop has exactly one code path for all non-interactive
71+ // modes.
72+ type seqStep struct {
73+ kind stepKind
74+ pattern * regexp.Regexp // set when kind == stepExpect
75+ data []byte // set when kind == stepSend (empty = send nothing)
6376}
6477
6578// serialPort is the subset of the serial device used by Run. It is satisfied by
@@ -74,9 +87,8 @@ type Serial struct {
7487 Port string // Port is the path to the serial device on the dutagent.
7588 Baud int // Baud is the baud rate of the serial device. If unset, DefaultBaudRate is used.
7689
77- expect * regexp.Regexp // expect is a pattern to match against the serial output (single-expect mode).
78- pairs []expectSendPair // pairs are the expect-send pairs (expect-send mode).
79- timeout time.Duration // timeout is the maximum time to wait for the expect pattern to match.
90+ steps []seqStep // steps is the ordered expect/send sequence (empty = interactive mode).
91+ timeout time.Duration // timeout is the maximum time to wait for the sequence to complete.
8092
8193 // dialPort opens the serial port. It defaults to openPort and is overridden
8294 // in tests to inject a fake. Set lazily in Run so the zero value works.
@@ -101,6 +113,7 @@ const abstract = `Serial connection to the DUT
101113const usage = `
102114ARGUMENTS:
103115 [-t <duration>] [<expect> [<response> <expect> <response> ...]]
116+ [-t <duration>] expect:<regex>|send:<data> [expect:<regex>|send:<data> ...]
104117
105118`
106119
@@ -116,6 +129,16 @@ Modes of operation:
116129 response to the serial port. Pairs are processed in order; after the last
117130 pair matches the module reads serial output for 1 more second so the output
118131 of the triggered command is visible, then exits.
132+ - Sequence (every argument carries an "expect:" or "send:" tag): an ordered
133+ list of steps run one after another. An expect-step waits for its regex to
134+ match the serial output; a send-step writes its data to the port. Unlike
135+ expect-send pairs, the steps may appear in any order, so a sequence can
136+ begin with a send (e.g. an Enter to wake the console) or chain several
137+ sends or expects in a row. The whole sequence shares the -t deadline. If
138+ the last step is a send, the module drains output for 1 more second before
139+ exiting; if it is an expect, it exits as soon as that pattern matches.
140+
141+ e.g.: send:"\n" expect:"login:" send:"root\n" expect:"# " send:"reboot\n"
119142
120143If the serial device disappears mid-session (e.g. an FTDI chip that powers down
121144when the DUT loses power), the module waits for it to reappear and reconnects
@@ -124,7 +147,7 @@ automatically instead of ending the session.
124147The expect string supports regular expressions according to [1].
125148The optional -t flag specifies the maximum time to wait.
126149Quote strings containing spaces or special characters. E.g.: "(?i)hello\s+world"
127- Response strings support C-style escape sequences: \n, \r, \t, \\, \xHH.
150+ Response and send strings support C-style escape sequences: \n, \r, \t, \\, \xHH.
128151
129152[1] https://golang.org/s/re2syntax.
130153`
@@ -308,7 +331,7 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
308331 var (
309332 remainder []byte // partial CSI sequence carried across reads
310333 matchWindow []byte // bounded window of recent output for regex matching
311- currentPair int
334+ currentStep int // cursor into s.steps
312335 draining bool
313336 )
314337
@@ -328,6 +351,21 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
328351 log .Printf ("serial module: draining serial output for %s before closing" , pairsDrain )
329352 }
330353
354+ // advanceSends fires consecutive send-steps at the cursor, writing each to
355+ // the port, until the cursor reaches an expect-step or the end of the
356+ // sequence. It is called once before the first read (so a sequence may begin
357+ // with a send) and again after every expect-step matches.
358+ advanceSends := func () {
359+ for currentStep < len (s .steps ) && s .steps [currentStep ].kind == stepSend {
360+ data := s .steps [currentStep ].data
361+ currentStep ++
362+
363+ if len (data ) > 0 {
364+ writeToPort (data )
365+ }
366+ }
367+ }
368+
331369 // reconnect closes the vanished port and retries opening it until the device
332370 // reappears, the deadline (watchCtx) fires, or the session is cancelled. Once
333371 // it returns, either a fresh port is in place or watchCtx is done and the
@@ -367,6 +405,16 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
367405
368406 readBuffer := make ([]byte , bufferSize )
369407
408+ // Fire any leading send-steps before the first read, so a sequence may begin
409+ // by sending (e.g. an Enter to wake the console) rather than expecting. If
410+ // the sequence is sends only, drain briefly so the DUT's response to the
411+ // final input is visible, then exit via the drain deadline below.
412+ advanceSends ()
413+
414+ if len (s .steps ) > 0 && currentStep >= len (s .steps ) {
415+ startDrain ()
416+ }
417+
370418 for {
371419 select {
372420 case <- loopCtx .Done ():
@@ -389,8 +437,8 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
389437 emit ([]byte ("\n --- Timeout reached, no match found ---" ))
390438 flushAndWait (true )
391439
392- if s . expect != nil {
393- return fmt .Errorf ("timeout of %s reached, pattern %q not found" , s .timeout , s .expect )
440+ if len ( s . steps ) == 1 && s . steps [ 0 ]. kind == stepExpect {
441+ return fmt .Errorf ("timeout of %s reached, pattern %q not found" , s .timeout , s .steps [ 0 ]. pattern )
394442 }
395443
396444 return fmt .Errorf ("timeout of %s reached, expect-send sequence not completed" , s .timeout )
@@ -446,8 +494,8 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
446494 // without a trailing newline). out is a fresh slice owned by the pump.
447495 emit (out )
448496
449- // Matching is skipped while draining and in interactive mode.
450- if draining || ( s . expect == nil && len (s .pairs ) == 0 ) {
497+ // Matching is skipped while draining and in interactive mode (no steps) .
498+ if draining || len (s .steps ) == 0 {
451499 continue
452500 }
453501
@@ -456,37 +504,38 @@ func (s *Serial) Run(ctx context.Context, session module.Session, args ...string
456504 matchWindow = matchWindow [len (matchWindow )- maxMatchWindow :]
457505 }
458506
459- if s .expect != nil {
460- if s .expect .Match (matchWindow ) {
461- emit ([]byte ("\n --- Pattern matched, connection closed ---" ))
462- flushAndWait (true )
463-
464- return nil
465- }
466-
467- continue
468- }
469-
470- // Expect-send: fire every pair already satisfied by the current window.
471- for currentPair < len (s .pairs ) {
472- loc := s .pairs [currentPair ].pattern .FindIndex (matchWindow )
507+ // Walk the sequence: satisfy every expect-step the current window already
508+ // matches, firing the send-steps that follow each match. The cursor rests
509+ // on an expect-step here, because advanceSends consumed any leading or
510+ // trailing sends after the previous iteration.
511+ for currentStep < len (s .steps ) && s .steps [currentStep ].kind == stepExpect {
512+ loc := s .steps [currentStep ].pattern .FindIndex (matchWindow )
473513 if loc == nil {
474514 break
475515 }
476516
477- response := s .pairs [currentPair ].response
478517 matchWindow = matchWindow [loc [1 ]:] // consume through the match
479- currentPair ++
518+ currentStep ++
480519
481- if len (response ) > 0 {
482- writeToPort (response )
520+ advanceSends () // fire the send-steps that follow this match
521+
522+ if currentStep < len (s .steps ) {
523+ continue // more steps remain; keep matching the current window
483524 }
484525
485- if currentPair >= len (s .pairs ) {
526+ // Sequence complete. If it ended on a send, drain so the DUT's
527+ // response to the final input is visible; if it ended on an expect,
528+ // the match itself is the completion, so exit immediately.
529+ if s .steps [len (s .steps )- 1 ].kind == stepSend {
486530 startDrain ()
531+ } else {
532+ emit ([]byte ("\n --- Pattern matched, connection closed ---" ))
533+ flushAndWait (true )
487534
488- break
535+ return nil
489536 }
537+
538+ break
490539 }
491540 }
492541}
@@ -568,6 +617,12 @@ func newStdoutPump(baseCtx context.Context, stdout io.Writer) (func([]byte), fun
568617// pairStride is the number of positional arguments per expect-send pair.
569618const pairStride = 2
570619
620+ // Tag prefixes that mark a tagged-sequence argument.
621+ const (
622+ expectTag = "expect:"
623+ sendTag = "send:"
624+ )
625+
571626func (s * Serial ) evalArgs (args []string ) error {
572627 fs := flag .NewFlagSet ("serial" , flag .ContinueOnError )
573628 fs .SetOutput (io .Discard ) // Suppress default error output
@@ -580,26 +635,39 @@ func (s *Serial) evalArgs(args []string) error {
580635
581636 positional := fs .Args ()
582637
638+ // Tagged-sequence mode is selected when the first argument carries a tag.
639+ // It allows arbitrarily ordered expect/send steps (e.g. a leading send).
640+ if len (positional ) > 0 && isTaggedStep (positional [0 ]) {
641+ return s .evalSequenceArgs (positional )
642+ }
643+
644+ return s .evalLegacyArgs (positional )
645+ }
646+
647+ // evalLegacyArgs parses the backward-compatible positional argument forms into
648+ // s.steps: no args (interactive), one arg (single expect), or an even number of
649+ // args (expect-send pairs).
650+ func (s * Serial ) evalLegacyArgs (positional []string ) error {
583651 switch len (positional ) {
584652 case 0 :
585- // Interactive mode: no expect pattern, no pairs .
653+ // Interactive mode: no steps .
586654 case 1 :
587- // Single-expect mode (backward compatible) .
655+ // Single-expect mode.
588656 log .Printf ("serial module: Will wait for pattern: %q" , positional [0 ])
589657
590658 pattern , compileErr := regexp .Compile (positional [0 ])
591659 if compileErr != nil {
592660 return fmt .Errorf ("invalid regular expression: %w" , compileErr )
593661 }
594662
595- s .expect = pattern
663+ s .steps = [] seqStep {{ kind : stepExpect , pattern : pattern }}
596664 default :
597665 // Expect-send pairs mode.
598666 if len (positional )% pairStride != 0 {
599667 return fmt .Errorf ("expect-send requires an even number of arguments, got %d" , len (positional ))
600668 }
601669
602- s .pairs = make ([]expectSendPair , 0 , len (positional )/ pairStride )
670+ s .steps = make ([]seqStep , 0 , len (positional ))
603671
604672 for idx := 0 ; idx < len (positional ); idx += pairStride {
605673 pattern , compileErr := regexp .Compile (positional [idx ])
@@ -609,10 +677,49 @@ func (s *Serial) evalArgs(args []string) error {
609677
610678 log .Printf ("serial module: Pair %d: pattern=%q response=%q" , idx / pairStride + 1 , positional [idx ], positional [idx + 1 ])
611679
612- s .pairs = append (s .pairs , expectSendPair {
613- pattern : pattern ,
614- response : unescape (positional [idx + 1 ]),
615- })
680+ s .steps = append (s .steps ,
681+ seqStep {kind : stepExpect , pattern : pattern },
682+ seqStep {kind : stepSend , data : unescape (positional [idx + 1 ])},
683+ )
684+ }
685+ }
686+
687+ return nil
688+ }
689+
690+ // isTaggedStep reports whether arg carries an "expect:" or "send:" tag.
691+ func isTaggedStep (arg string ) bool {
692+ return strings .HasPrefix (arg , expectTag ) || strings .HasPrefix (arg , sendTag )
693+ }
694+
695+ // evalSequenceArgs parses tagged-sequence arguments into s.steps. Every
696+ // argument must carry a tag; mixing tagged and untagged arguments is rejected
697+ // so a malformed command fails loudly instead of being silently misread.
698+ func (s * Serial ) evalSequenceArgs (args []string ) error {
699+ s .steps = make ([]seqStep , 0 , len (args ))
700+
701+ for idx , arg := range args {
702+ switch {
703+ case strings .HasPrefix (arg , expectTag ):
704+ expr := arg [len (expectTag ):]
705+
706+ pattern , compileErr := regexp .Compile (expr )
707+ if compileErr != nil {
708+ return fmt .Errorf ("step %d: invalid regular expression %q: %w" , idx + 1 , expr , compileErr )
709+ }
710+
711+ log .Printf ("serial module: Step %d: expect=%q" , idx + 1 , expr )
712+
713+ s .steps = append (s .steps , seqStep {kind : stepExpect , pattern : pattern })
714+ case strings .HasPrefix (arg , sendTag ):
715+ data := arg [len (sendTag ):]
716+
717+ log .Printf ("serial module: Step %d: send=%q" , idx + 1 , data )
718+
719+ s .steps = append (s .steps , seqStep {kind : stepSend , data : unescape (data )})
720+ default :
721+ return fmt .Errorf ("step %d %q: in sequence mode every argument must start with %q or %q" ,
722+ idx + 1 , arg , expectTag , sendTag )
616723 }
617724 }
618725
0 commit comments