Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions pkg/module/serial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ mode: see [Interactive mode](#interactive-mode).

```
ARGUMENTS:
[-t <duration>] [-eol cr|lf|crlf|none] [-keep-escapes] (monitor: stream output)
[-t <duration>] [-eol cr|lf|crlf|none] [-keep-escapes] [--] <step>... (run a step sequence)
[-t <duration>] -i (interactive console)
[-reconnect] [-t <duration>] [-eol cr|lf|crlf|none] [-keep-escapes] (monitor)
[-reconnect] [-t <duration>] [-eol cr|lf|crlf|none] [-keep-escapes] [--] <step>... (step sequence)
[-reconnect] [-t <duration>] -i (interactive)

step := expect <regex> | send <data> | send-raw <data>
```
Expand Down Expand Up @@ -51,8 +51,8 @@ is done.
While the monitor runs, the device is busy for other users, but your own other
commands may run beside it: a power cycle from a second terminal shows up in the
monitor's output. Start it once the monitor prints `--- Connected to … ---`, or
the first lines of the boot are lost. If the DUT powers the serial adapter, the
adapter disappears with the power and the monitor ends.
the first lines of the boot are lost. If the serial adapter disappears during a
power cycle, the monitor ends unless `-reconnect` is given.

A serial port serves one command at a time, unless dutagent runs as root.
Running the same command again is refused, and another command that opens the
Expand Down Expand Up @@ -94,6 +94,42 @@ Like the monitor, an interactive session keeps the device busy for other users
until it ends, and your own other commands, a power cycle for instance, may run
beside it.

## Temporary disconnections

Pass `-reconnect` in any mode to wait for an initially absent device and reopen
it after a disconnect. Configure a stable path such as
`/dev/serial/by-id/usb-...`, so a replacement `/dev/ttyUSBn` number cannot select
another adapter. Reopening uses the same path and baud rate, at 250 ms intervals.
Permission, busy and configuration errors remain fatal. Without the flag, opening
or reading an unavailable device still fails immediately.

The session and device reservation stay open. Notices appear once per transition:

```text
--- Serial device disconnected: /dev/serial/by-id/usb-... ---
--- Waiting for serial device to reconnect: /dev/serial/by-id/usb-... ---
--- Serial device reconnected: /dev/serial/by-id/usb-... at 115200 baud ---
```

An initially absent device prints the waiting notice. Interactive notices use
stderr with CR LF; stdout continues to contain only DUT bytes. Input received
while disconnected is discarded, with a notice, rather than replayed after
reconnection. An interrupted interactive write is not replayed either.

Monitor output resumes. A pending `expect` resumes with its match buffer and
partial character-map/escape state cleared, so a match cannot span two boots.
Completed steps are never repeated. A send that has not started may wait for the
device; any failed attempted send terminates the script with uncertain delivery,
including short writes that failed partway through. It is never retried.

The original `-t` deadline includes all waiting and the final drain, and never
restarts on reconnection. Reaching it still succeeds in monitor/interactive mode;
an unfinished scripted step fails. With `-t 0`, waiting ends on cancellation (or
interactive input EOF). Ctrl-A x remains available while waiting in a console.
Newly opened ports are not flushed, preserving queued boot bytes; output emitted
while the adapter was absent cannot be recovered. Without `-reconnect`, the
initial input buffer is still flushed as before.

## Flags

| Flag | Description |
Expand All @@ -102,6 +138,7 @@ beside it.
| `-eol cr\|lf\|crlf\|none` | Line ending appended by `send`. Default `cr` (`\r`), which is what serial consoles expect on Enter. |
| `-keep-escapes` | Keep terminal escape sequences in the output instead of stripping them (e.g. for binary data or exact-byte capture). |
| `-i` | Interactive console: bridge the port to your terminal. Takes no steps, and no `-eol` or `-keep-escapes`. |
| `-reconnect` | Wait for an absent device and reopen after disconnection. Default off; see [Temporary disconnections](#temporary-disconnections). |

The `--` separator before the steps is optional; it is only needed if a step
value would otherwise look like a flag.
Expand Down Expand Up @@ -139,6 +176,9 @@ serial
# Interactive console (end it with Ctrl-A x).
serial -i

# Keep the same console through USB serial disconnections during power cycles.
serial -reconnect -i

# Wait for a boot marker, then succeed.
serial -- expect 'Welcome to'

Expand Down
6 changes: 5 additions & 1 deletion pkg/module/serial/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func bridge(
) error {
_, _ = fmt.Fprintf(con.Stderr, "--- Connected to %s at %d baud ---\r\n", name, baud)

return bridgeConsole(ctx, serialPort, incoming, con, drain)
}

func bridgeConsole(ctx context.Context, serialPort port, incoming *inbound, con module.Console, drain time.Duration) error {
// The input pump runs on its own goroutine, since a Read on Stdin and a Read
// on the port both block. stopped is closed when it returned, and writeErr
// is read only after that.
Expand Down Expand Up @@ -171,7 +175,7 @@ func forwardOutput(
}
}

if err != nil {
if err != nil && !errors.Is(err, errDisconnected) {
return err
}
}
Expand Down
24 changes: 17 additions & 7 deletions pkg/module/serial/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package serial

import (
"context"
"errors"
"io"
"regexp"
"time"
Expand Down Expand Up @@ -78,15 +79,21 @@ func (e *engine) readUntil(ctx context.Context, pattern *regexp.Regexp) error {
}

// go.bug.st returns (0, nil) on a read timeout — keep waiting and let
// the ctx check above enforce the overall deadline. Any real error
// (port closed / device gone) ends the step.
// the ctx check above enforce the overall deadline. The reconnecting
// adapter marks device loss as a boundary; other errors end the step.
out, err := e.in.read(e.p, readBuf)

werr := e.tee(out)
if werr != nil {
return werr
}

if errors.Is(err, errDisconnected) {
e.buf = e.buf[:0]

continue
}

if err != nil {
return err
}
Expand Down Expand Up @@ -163,6 +170,10 @@ func (e *engine) pump(ctx context.Context, swallowReadErr bool) error {
}
}

if errors.Is(err, errDisconnected) {
continue
}

if err != nil {
if swallowReadErr {
return nil
Expand All @@ -180,12 +191,11 @@ func (e *engine) monitor(ctx context.Context) error {
return e.pump(ctx, false)
}

// drain forwards serial output for up to d, so the DUT's reply to a final send
// is visible before the run closes. It is best-effort: a read error (e.g. the
// device rebooted from that send) ends it without failing the run.
func (e *engine) drain(ctx context.Context, d time.Duration) error {
// drain forwards serial output for up to d. Legacy runs tolerate read errors
// after a final send; reconnecting runs handle device loss and expose fatal errors.
func (e *engine) drain(ctx context.Context, d time.Duration, swallowReadErr bool) error {
drainCtx, cancel := context.WithTimeout(ctx, d)
defer cancel()

return e.pump(drainCtx, true)
return e.pump(drainCtx, swallowReadErr)
}
2 changes: 1 addition & 1 deletion pkg/module/serial/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func TestEngineDrainForwardsBestEffort(t *testing.T) {
sink := &bytes.Buffer{}
eng := newEngine(fp, newInbound(charMap{}, false), sink)

if err := eng.drain(context.Background(), 50*time.Millisecond); err != nil {
if err := eng.drain(context.Background(), 50*time.Millisecond, true); err != nil {
t.Fatalf("drain: %v", err)
}

Expand Down
8 changes: 7 additions & 1 deletion pkg/module/serial/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package serial

import (
"errors"
"fmt"
"io"
"strings"
Expand Down Expand Up @@ -110,7 +111,12 @@ func (in *inbound) read(serialPort port, buf []byte) ([]byte, error) {
return in.flush(), nil
}

return in.apply(buf[:n]), err
out := in.apply(buf[:n])
if errors.Is(err, errDisconnected) {
*in = inbound{maps: in.maps, strip: in.strip}
}

return out, err
}

// apply returns what of chunk is to be shown.
Expand Down
Loading
Loading