Skip to content

Commit 87a4da9

Browse files
committed
feat: make serial module interactive
Enable bidirectional I/O for serial console access: - Use Console() for stdin/stdout instead of Print() - Forward client input to serial port in realtime - Set terminal to raw input mode (no echo, no line buffering) - Filter terminal escape sequences from serial I/O - Add flush timeout for prompts without newlines Signed-off-by: llogen <christoph.lange@blindspot.software>
1 parent 2dd0cbb commit 87a4da9

3 files changed

Lines changed: 255 additions & 16 deletions

File tree

cmds/dutctl/rpc.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package main
66

77
import (
8-
"bufio"
98
"context"
109
"errors"
1110
"fmt"
@@ -17,6 +16,7 @@ import (
1716

1817
"connectrpc.com/connect"
1918
"github.com/BlindspotSoftware/dutctl/internal/output"
19+
"golang.org/x/sys/unix"
2020

2121
pb "github.com/BlindspotSoftware/dutctl/protobuf/gen/dutctl/v1"
2222
)
@@ -92,10 +92,44 @@ func (app *application) detailsRPC(device, command, keyword string) error {
9292
return nil
9393
}
9494

95+
// setRawInput puts the terminal into raw input mode: disables local echo and
96+
// canonical (line-buffered) mode so each keystroke is available immediately.
97+
// It returns a restore function, or nil if the fd is not a terminal.
98+
func setRawInput(fd int) func() {
99+
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
100+
if err != nil {
101+
return nil
102+
}
103+
104+
old := *termios
105+
106+
termios.Iflag &^= unix.ICRNL
107+
termios.Lflag &^= unix.ECHO | unix.ICANON
108+
termios.Cc[unix.VMIN] = 1
109+
termios.Cc[unix.VTIME] = 0
110+
111+
if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil {
112+
return nil
113+
}
114+
115+
return func() {
116+
_ = unix.IoctlSetTermios(fd, unix.TCSETS, &old)
117+
}
118+
}
119+
95120
//nolint:funlen,cyclop,gocognit
96121
func (app *application) runRPC(device, command string, cmdArgs []string) error {
97122
const numWorkers = 2 // The send and receive worker goroutines
98123

124+
// Set raw input mode: disable local echo and canonical mode so each
125+
// keystroke is sent immediately. Interactive modules like serial rely
126+
// on the remote side to echo input.
127+
if f, ok := app.stdin.(*os.File); ok {
128+
if restore := setRawInput(int(f.Fd())); restore != nil {
129+
defer restore()
130+
}
131+
}
132+
99133
runCtx, cancel := context.WithCancel(context.Background())
100134
defer cancel()
101135

@@ -230,7 +264,7 @@ func (app *application) runRPC(device, command string, cmdArgs []string) error {
230264
go func() {
231265
defer cancel()
232266

233-
reader := bufio.NewReader(app.stdin)
267+
buf := make([]byte, 256)
234268

235269
for {
236270
select {
@@ -241,7 +275,7 @@ func (app *application) runRPC(device, command string, cmdArgs []string) error {
241275
default:
242276
}
243277

244-
text, err := reader.ReadString('\n')
278+
n, err := app.stdin.Read(buf)
245279
if err != nil {
246280
if !errors.Is(err, io.EOF) {
247281
errChan <- fmt.Errorf("reading stdin: %w", err)
@@ -254,7 +288,7 @@ func (app *application) runRPC(device, command string, cmdArgs []string) error {
254288
Msg: &pb.RunRequest_Console{
255289
Console: &pb.Console{
256290
Data: &pb.Console_Stdin{
257-
Stdin: []byte(text),
291+
Stdin: buf[:n],
258292
},
259293
},
260294
},

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
1212
golang.org/x/crypto v0.48.0
1313
golang.org/x/net v0.50.0
14+
golang.org/x/sys v0.41.0
1415
google.golang.org/protobuf v1.36.11
1516
gopkg.in/yaml.v3 v3.0.1
1617
)
@@ -32,6 +33,5 @@ require (
3233
github.com/olekukonko/tablewriter v1.0.9 // indirect
3334
github.com/rivo/uniseg v0.2.0 // indirect
3435
github.com/rogpeppe/go-internal v1.6.1 // indirect
35-
golang.org/x/sys v0.41.0 // indirect
3636
golang.org/x/text v0.34.0 // indirect
3737
)

0 commit comments

Comments
 (0)