mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
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:
@@ -156,8 +156,8 @@ func (a *App) Close() {
|
|||||||
close(a.done)
|
close(a.done)
|
||||||
a.forward.Close()
|
a.forward.Close()
|
||||||
a.plugins.Close()
|
a.plugins.Close()
|
||||||
CloseTrzsz()
|
sess.Close()
|
||||||
CloseSerial()
|
|
||||||
if a.logFile != nil {
|
if a.logFile != nil {
|
||||||
_ = a.logFile.Close()
|
_ = a.logFile.Close()
|
||||||
}
|
}
|
||||||
@@ -216,7 +216,7 @@ func (a *App) writeRawToSession(data []byte) error {
|
|||||||
|
|
||||||
a.stdinMu.Lock()
|
a.stdinMu.Lock()
|
||||||
defer a.stdinMu.Unlock()
|
defer a.stdinMu.Unlock()
|
||||||
_, err := stdinPipe.Write(data)
|
_, err := sess.StdinPipe.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ func (a *App) sendCtrl(letter byte) error {
|
|||||||
letter = letter + ('a' - 'A')
|
letter = letter + ('a' - 'A')
|
||||||
}
|
}
|
||||||
control := []byte{letter & 0x1f}
|
control := []byte{letter & 0x1f}
|
||||||
_, err := serialPort.Write(control)
|
_, err := sess.Port.Write(control)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ func (a *App) readHexOutput() {
|
|||||||
|
|
||||||
buf := make([]byte, frameSize)
|
buf := make([]byte, frameSize)
|
||||||
for {
|
for {
|
||||||
n, err := stdoutPipe.Read(buf)
|
n, err := sess.StdoutPipe.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
chunk := make([]byte, n)
|
chunk := make([]byte, n)
|
||||||
copy(chunk, buf[:n])
|
copy(chunk, buf[:n])
|
||||||
@@ -333,7 +333,7 @@ func (a *App) readHexOutput() {
|
|||||||
func (a *App) readTextOutput() {
|
func (a *App) readTextOutput() {
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
n, err := stdoutPipe.Read(buf)
|
n, err := sess.StdoutPipe.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
chunk := make([]byte, n)
|
chunk := make([]byte, n)
|
||||||
copy(chunk, buf[:n])
|
copy(chunk, buf[:n])
|
||||||
|
|||||||
+7
-3
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||||
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/session"
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||||
)
|
)
|
||||||
@@ -204,11 +205,14 @@ func TestReportForwardIngress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSendCtrl(t *testing.T) {
|
func TestSendCtrl(t *testing.T) {
|
||||||
oldSp := serialPort
|
if sess == nil {
|
||||||
defer func() { serialPort = oldSp }()
|
sess = &session.SerialSession{}
|
||||||
|
}
|
||||||
|
oldSp := sess.Port
|
||||||
|
defer func() { sess.Port = oldSp }()
|
||||||
|
|
||||||
// Use a mock serial port
|
// Use a mock serial port
|
||||||
serialPort = &mockSerialPort{}
|
sess.Port = &mockSerialPort{}
|
||||||
a := &App{
|
a := &App{
|
||||||
cfg: &Config{},
|
cfg: &Config{},
|
||||||
uiEvents: make(chan event.UIEvent, 4),
|
uiEvents: make(chan event.UIEvent, 4),
|
||||||
|
|||||||
+5
-1
@@ -6,13 +6,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
|
||||||
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/session"
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
|
||||||
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestPipes() {
|
func setupTestPipes() {
|
||||||
|
if sess == nil {
|
||||||
|
sess = &session.SerialSession{}
|
||||||
|
}
|
||||||
var cr *io.PipeReader
|
var cr *io.PipeReader
|
||||||
cr, stdinPipe = io.Pipe()
|
cr, sess.StdinPipe = io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
buf := make([]byte, 4096)
|
buf := make([]byte, 4096)
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/session"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,20 +36,16 @@ func main() {
|
|||||||
if cfg.PortName == "" {
|
if cfg.PortName == "" {
|
||||||
getCliFlag()
|
getCliFlag()
|
||||||
}
|
}
|
||||||
ports, err := checkPortAvailability(cfg.PortName)
|
ports, err := session.CheckPortAvailability(cfg.PortName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
printUsage(ports)
|
printUsage(ports)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = OpenSerial(); err != nil {
|
sess, err = session.Open(cfg)
|
||||||
fmt.Fprintf(os.Stderr, "open serial failed: %v\n", err)
|
if err != nil {
|
||||||
os.Exit(1)
|
fmt.Fprintf(os.Stderr, "open session failed: %v\n", err)
|
||||||
}
|
|
||||||
|
|
||||||
if err = OpenTrzsz(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "open trzsz failed: %v\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/trzsz/trzsz-go/trzsz"
|
|
||||||
"go.bug.st/serial"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
serialPort serial.Port
|
sess *session.SerialSession
|
||||||
out io.Writer = os.Stdout
|
out io.Writer = os.Stdout
|
||||||
trzszFilter *trzsz.TrzszFilter
|
|
||||||
clientIn *io.PipeReader
|
|
||||||
stdoutPipe *io.PipeReader
|
|
||||||
stdinPipe *io.PipeWriter
|
|
||||||
clientOut *io.PipeWriter
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/trzsz/trzsz-go/trzsz"
|
|
||||||
"go.bug.st/serial"
|
|
||||||
"golang.org/x/term"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkPortAvailability(name string) ([]string, error) {
|
|
||||||
ports, err := serial.GetPortsList()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(ports) == 0 {
|
|
||||||
return nil, fmt.Errorf("无串口")
|
|
||||||
}
|
|
||||||
if name == "" {
|
|
||||||
return ports, fmt.Errorf("串口未指定")
|
|
||||||
}
|
|
||||||
for _, port := range ports {
|
|
||||||
if strings.Compare(port, name) == 0 {
|
|
||||||
return ports, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ports, fmt.Errorf("串口 " + name + " 未在线")
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenSerial() error {
|
|
||||||
mode := &serial.Mode{
|
|
||||||
BaudRate: cfg.BaudRate,
|
|
||||||
StopBits: serial.StopBits(cfg.StopBits),
|
|
||||||
DataBits: cfg.DataBits,
|
|
||||||
Parity: serial.Parity(cfg.ParityBit),
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
serialPort, err = serial.Open(cfg.PortName, mode)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func CloseSerial() {
|
|
||||||
if serialPort == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := serialPort.Close(); err != nil {
|
|
||||||
fmt.Fprint(os.Stderr, err)
|
|
||||||
fmt.Fprint(os.Stderr, "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var termch chan os.Signal
|
|
||||||
var termchOnce sync.Once
|
|
||||||
|
|
||||||
// OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ).
|
|
||||||
//
|
|
||||||
// ┌────────┐ stdinPipe ┌────────┐ ClientIn ┌─────────────┐ SerialIn ┌────────┐
|
|
||||||
// │ ├─────────────►│ ├─────────────►│ ├─────────────►│ │
|
|
||||||
// │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │
|
|
||||||
// │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │
|
|
||||||
// └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘
|
|
||||||
func OpenTrzsz() error {
|
|
||||||
fd := int(os.Stdin.Fd())
|
|
||||||
width, _, err := term.GetSize(fd)
|
|
||||||
if err != nil {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return fmt.Errorf("term get size failed: %w", err)
|
|
||||||
}
|
|
||||||
width = 80
|
|
||||||
}
|
|
||||||
|
|
||||||
clientIn, stdinPipe = io.Pipe()
|
|
||||||
stdoutPipe, clientOut = io.Pipe()
|
|
||||||
trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serialPort, serialPort,
|
|
||||||
trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true})
|
|
||||||
trzsz.SetAffectedByWindows(false)
|
|
||||||
termch = make(chan os.Signal, 1)
|
|
||||||
termchOnce = sync.Once{}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for range termch {
|
|
||||||
width, _, err := term.GetSize(fd)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("term get size failed: %s\n", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trzszFilter.SetTerminalColumns(int32(width))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CloseTrzsz() {
|
|
||||||
if termch == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
termchOnce.Do(func() {
|
|
||||||
signal.Stop(termch)
|
|
||||||
close(termch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user