fix: retry interrupted serial drain (#33)

This commit is contained in:
Rain Seven
2026-08-16 17:09:01 +08:00
committed by GitHub
parent 82494f519b
commit 7ba30132f9
2 changed files with 58 additions and 4 deletions
+19 -3
View File
@@ -8,6 +8,7 @@ import (
"io"
"strings"
"sync"
"syscall"
"time"
)
@@ -151,7 +152,7 @@ func (session *Session) executeLocked(ctx context.Context, command string) (Resp
session.poisonLocked()
return response, fmt.Errorf("write %s: %w", command, err)
}
if err := session.transport.Drain(); err != nil {
if err := drainTransport(ctx, session.transport); err != nil {
session.poisonLocked()
return response, fmt.Errorf("drain %s: %w", command, err)
}
@@ -178,7 +179,7 @@ func (session *Session) executePromptLocked(
session.poisonLocked()
return response, fmt.Errorf("write %s: %w", command, err)
}
if err := session.transport.Drain(); err != nil {
if err := drainTransport(ctx, session.transport); err != nil {
session.poisonLocked()
return response, fmt.Errorf("drain %s: %w", command, err)
}
@@ -203,7 +204,7 @@ func (session *Session) executePromptLocked(
response.Duration = time.Since(started)
return response, fmt.Errorf("terminate %s payload: %w", command, err)
}
if err := session.transport.Drain(); err != nil {
if err := drainTransport(ctx, session.transport); err != nil {
session.poisonLocked()
response.Duration = time.Since(started)
return response, fmt.Errorf("drain %s payload: %w", command, err)
@@ -211,6 +212,21 @@ func (session *Session) executePromptLocked(
return session.readFinalLocked(ctx, started, command, string(payload), response)
}
// drainTransport retries tcdrain/TCSBRK when the kernel interrupts it with a
// signal. go.bug.st/serial already retries EINTR for Read, but its Linux
// Drain implementation currently returns the transient error directly.
func drainTransport(ctx context.Context, transport Transport) error {
for {
err := transport.Drain()
if !errors.Is(err, syscall.EINTR) {
return err
}
if err := ctx.Err(); err != nil {
return err
}
}
}
func (session *Session) readFinalLocked(
ctx context.Context,
started time.Time,
+39 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"sync"
"syscall"
"testing"
"time"
)
@@ -27,6 +28,8 @@ type transcriptTransport struct {
unexpected error
writePartial bool
writeEvents chan string
drainErrors []error
drainCount int
}
func (transport *transcriptTransport) Write(payload []byte) (int, error) {
@@ -114,7 +117,17 @@ func (transport *transcriptTransport) Read(buffer []byte) (int, error) {
return 0, nil
}
func (transport *transcriptTransport) Drain() error { return nil }
func (transport *transcriptTransport) Drain() error {
transport.mu.Lock()
defer transport.mu.Unlock()
transport.drainCount++
if len(transport.drainErrors) == 0 {
return nil
}
err := transport.drainErrors[0]
transport.drainErrors = transport.drainErrors[1:]
return err
}
func (transport *transcriptTransport) ResetInputBuffer() error {
transport.mu.Lock()
@@ -138,6 +151,31 @@ func (transport *transcriptTransport) Close() error {
return nil
}
func TestSessionRetriesInterruptedDrain(t *testing.T) {
transport := &transcriptTransport{
steps: []transportStep{{
write: "AT+CSQ\r",
chunks: []string{"\r\nAT+CSQ\r\n+CSQ: 24,99\r\nOK\r\n"},
}},
drainErrors: []error{syscall.EINTR},
}
session, err := NewSession(transport, SessionOptions{})
if err != nil {
t.Fatalf("NewSession() error = %v", err)
}
response, err := session.Execute(context.Background(), "AT+CSQ")
if err != nil {
t.Fatalf("Execute() error = %v", err)
}
if response.Final != "OK" {
t.Fatalf("response final = %q", response.Final)
}
if transport.drainCount != 2 {
t.Fatalf("Drain() calls = %d, want 2", transport.drainCount)
}
}
func TestSessionSeparatesInterleavedURCs(t *testing.T) {
transport := &transcriptTransport{steps: []transportStep{{
write: "AT+CSQ\r",