refactor: extract internal/session and eliminate I/O globals

Move serial port, trzsz filter, and pipe lifecycle into
internal/session.SerialSession. Replace 8 global I/O vars
(serialPort, trzszFilter, stdinPipe, stdoutPipe, clientIn,
clientOut, termch, termchOnce) with single sess variable.
Delete utils.go.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-23 21:49:43 +08:00
parent 31dd9da490
commit a1524a7e17
7 changed files with 165 additions and 137 deletions
+138
View File
@@ -0,0 +1,138 @@
// Package session manages the serial port connection and its associated pipes.
package session
import (
"fmt"
"io"
"os"
"os/signal"
"runtime"
"sync"
"github.com/trzsz/trzsz-go/trzsz"
"go.bug.st/serial"
"golang.org/x/term"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/config"
)
// SerialSession owns the serial port, trzsz filter, and pipe pair.
type SerialSession struct {
Port serial.Port
TrzszFilter *trzsz.TrzszFilter
StdinPipe *io.PipeWriter
StdoutPipe *io.PipeReader
ClientIn *io.PipeReader
ClientOut *io.PipeWriter
termCh chan os.Signal
closeOnce sync.Once
}
// Open creates a SerialSession by opening the serial port and initializing trzsz.
func Open(cfg *config.Config) (*SerialSession, error) {
mode := &serial.Mode{
BaudRate: cfg.BaudRate,
StopBits: serial.StopBits(cfg.StopBits),
DataBits: cfg.DataBits,
Parity: serial.Parity(cfg.ParityBit),
}
port, err := serial.Open(cfg.PortName, mode)
if err != nil {
return nil, err
}
fd := int(os.Stdin.Fd())
width, _, err := term.GetSize(fd)
if err != nil {
if runtime.GOOS != "windows" {
port.Close()
return nil, fmt.Errorf("term get size failed: %w", err)
}
width = 80
}
clientIn, stdinPipe := io.Pipe()
stdoutPipe, clientOut := io.Pipe()
trzszFilter := trzsz.NewTrzszFilter(clientIn, clientOut, port, port,
trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true})
trzsz.SetAffectedByWindows(false)
s := &SerialSession{
Port: port,
TrzszFilter: trzszFilter,
StdinPipe: stdinPipe,
StdoutPipe: stdoutPipe,
ClientIn: clientIn,
ClientOut: clientOut,
termCh: make(chan os.Signal, 1),
}
go func() {
for range s.termCh {
w, _, err := term.GetSize(fd)
if err != nil {
fmt.Printf("term get size failed: %s\n", err)
continue
}
trzszFilter.SetTerminalColumns(int32(w))
}
}()
return s, nil
}
// Write writes data to the stdin pipe (toward serial port, through trzsz).
func (s *SerialSession) Write(data []byte) (int, error) {
return s.StdinPipe.Write(data)
}
// Read reads data from the stdout pipe (from serial port, through trzsz).
func (s *SerialSession) Read(buf []byte) (int, error) {
return s.StdoutPipe.Read(buf)
}
// SendCtrl sends a control character directly to the serial port (bypasses trzsz).
func (s *SerialSession) SendCtrl(letter byte) (int, error) {
if letter >= 'A' && letter <= 'Z' {
letter = letter + ('a' - 'A')
}
control := []byte{letter & 0x1f}
return s.Port.Write(control)
}
// Close tears down the session: stops term signals, closes trzsz, then serial port.
func (s *SerialSession) Close() {
s.closeOnce.Do(func() {
if s.termCh != nil {
signal.Stop(s.termCh)
close(s.termCh)
}
if s.Port != nil {
if err := s.Port.Close(); err != nil {
fmt.Fprint(os.Stderr, err)
fmt.Fprint(os.Stderr, "\n")
}
}
})
}
// CheckPortAvailability returns the list of available ports and verifies the named port exists.
func CheckPortAvailability(name string) ([]string, error) {
ports, err := serial.GetPortsList()
if err != nil {
return nil, err
}
if len(ports) == 0 {
return nil, fmt.Errorf("no serial ports found")
}
if name == "" {
return ports, fmt.Errorf("port name not specified")
}
for _, port := range ports {
if port == name {
return ports, nil
}
}
return ports, fmt.Errorf("port " + name + " is not available")
}