mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
b4b63ce1a4
Extract processChunk and readLoopError helpers to eliminate ~30 lines of duplicated read-validate-notify logic across readLoop, readLoopPacket, and readLoopSerial. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
662 lines
13 KiB
Go
662 lines
13 KiB
Go
// Package forward manages TCP/UDP/COM forwarding targets for serial data.
|
|
package forward
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"go.bug.st/serial"
|
|
)
|
|
|
|
// Mode is the forwarding protocol mode.
|
|
type Mode int
|
|
|
|
const (
|
|
None Mode = 0
|
|
TCP Mode = 1
|
|
UDP Mode = 2
|
|
TCPServer Mode = 3
|
|
UDPServer Mode = 4
|
|
COMPort Mode = 5
|
|
)
|
|
|
|
// ParseMode parses a mode string.
|
|
func ParseMode(v string) (Mode, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "tcp", "tcp-c", "tcpc", "1":
|
|
return TCP, true
|
|
case "udp", "udp-c", "udpc", "2":
|
|
return UDP, true
|
|
case "tcp-s", "tcps", "tcp-server", "3":
|
|
return TCPServer, true
|
|
case "udp-s", "udps", "udp-server", "4":
|
|
return UDPServer, true
|
|
case "com", "serial", "5":
|
|
return COMPort, true
|
|
default:
|
|
return None, false
|
|
}
|
|
}
|
|
|
|
func (m Mode) Network() string {
|
|
switch m {
|
|
case TCP, TCPServer:
|
|
return "tcp"
|
|
case UDP, UDPServer:
|
|
return "udp"
|
|
case COMPort:
|
|
return "serial"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (m Mode) String() string {
|
|
switch m {
|
|
case TCP:
|
|
return "tcp"
|
|
case UDP:
|
|
return "udp"
|
|
case TCPServer:
|
|
return "tcp-s"
|
|
case UDPServer:
|
|
return "udp-s"
|
|
case COMPort:
|
|
return "com"
|
|
default:
|
|
return "none"
|
|
}
|
|
}
|
|
|
|
// Stats holds I/O statistics for a forward target.
|
|
type Stats struct {
|
|
ReadBytes uint64
|
|
WrittenBytes uint64
|
|
LastError string
|
|
}
|
|
|
|
// Target represents a single forwarding connection.
|
|
type Target struct {
|
|
ID int
|
|
Mode Mode
|
|
Address string
|
|
Enabled bool
|
|
Connected bool
|
|
CreatedAt time.Time
|
|
|
|
// Client-mode connection (TCP/UDP client)
|
|
conn net.Conn
|
|
|
|
// Server-mode fields
|
|
listener net.Listener // TCP server listener
|
|
conns map[net.Conn]struct{} // TCP server accepted connections
|
|
connsMu sync.Mutex
|
|
|
|
// UDP server
|
|
packetConn net.PacketConn // UDP server listener
|
|
remoteAddrs map[string]net.Addr // known UDP remotes
|
|
|
|
// COM port
|
|
serialPort serial.Port
|
|
|
|
stats Stats
|
|
mu sync.Mutex
|
|
closeCh chan struct{}
|
|
closed bool
|
|
}
|
|
|
|
// AcceptedConns returns the number of accepted connections (TCP server only).
|
|
func (t *Target) acceptedConns() int {
|
|
t.connsMu.Lock()
|
|
defer t.connsMu.Unlock()
|
|
return len(t.conns)
|
|
}
|
|
|
|
// Snapshot is a read-only view of a forward target for display.
|
|
type Snapshot struct {
|
|
ID int
|
|
Mode string
|
|
Address string
|
|
Enabled bool
|
|
Connected bool
|
|
ReadBytes uint64
|
|
WriteByte uint64
|
|
LastError string
|
|
Conns int // accepted connection count (TCP server)
|
|
}
|
|
|
|
// Manager coordinates forwarding targets.
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
targets map[int]*Target
|
|
nextID int
|
|
writeToSerial func([]byte) error
|
|
notify func(string, ...any)
|
|
onInbound func(int, []byte)
|
|
}
|
|
|
|
// NewManager creates a forwarding manager.
|
|
func NewManager(writeToSerial func([]byte) error, notify func(string, ...any)) *Manager {
|
|
return &Manager{
|
|
targets: make(map[int]*Target),
|
|
nextID: 1,
|
|
writeToSerial: writeToSerial,
|
|
notify: notify,
|
|
}
|
|
}
|
|
|
|
// SetInboundReporter sets a callback invoked when inbound data arrives from a target.
|
|
func (m *Manager) SetInboundReporter(fn func(int, []byte)) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.onInbound = fn
|
|
}
|
|
|
|
// Add creates and connects a new forward target.
|
|
func (m *Manager) Add(mode Mode, address string) (int, error) {
|
|
if mode == None {
|
|
return 0, fmt.Errorf("forward mode cannot be none")
|
|
}
|
|
|
|
t := &Target{
|
|
Mode: mode,
|
|
Address: address,
|
|
Enabled: true,
|
|
CreatedAt: time.Now(),
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
|
|
switch mode {
|
|
case TCP, UDP:
|
|
conn, err := net.Dial(mode.Network(), address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return 0, err
|
|
}
|
|
t.conn = conn
|
|
t.Connected = true
|
|
|
|
m.mu.Lock()
|
|
t.ID = m.nextID
|
|
m.nextID++
|
|
m.targets[t.ID] = t
|
|
m.mu.Unlock()
|
|
|
|
go m.readLoop(t, conn, t.closeCh)
|
|
|
|
case TCPServer:
|
|
listener, err := net.Listen("tcp", address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return 0, err
|
|
}
|
|
t.listener = listener
|
|
t.conns = make(map[net.Conn]struct{})
|
|
t.Connected = true
|
|
|
|
m.mu.Lock()
|
|
t.ID = m.nextID
|
|
m.nextID++
|
|
m.targets[t.ID] = t
|
|
m.mu.Unlock()
|
|
|
|
go m.acceptLoop(t)
|
|
|
|
case UDPServer:
|
|
pc, err := net.ListenPacket("udp", address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return 0, err
|
|
}
|
|
t.packetConn = pc
|
|
t.remoteAddrs = make(map[string]net.Addr)
|
|
t.Connected = true
|
|
|
|
m.mu.Lock()
|
|
t.ID = m.nextID
|
|
m.nextID++
|
|
m.targets[t.ID] = t
|
|
m.mu.Unlock()
|
|
|
|
go m.readLoopPacket(t)
|
|
|
|
case COMPort:
|
|
sp, err := serial.Open(address, &serial.Mode{BaudRate: 115200, DataBits: 8, StopBits: 0, Parity: 0})
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return 0, err
|
|
}
|
|
t.serialPort = sp
|
|
t.Connected = true
|
|
|
|
m.mu.Lock()
|
|
t.ID = m.nextID
|
|
m.nextID++
|
|
m.targets[t.ID] = t
|
|
m.mu.Unlock()
|
|
|
|
go m.readLoopSerial(t)
|
|
}
|
|
|
|
m.notify("[forward] #%d %s %s connected", t.ID, t.Mode.String(), t.Address)
|
|
return t.ID, nil
|
|
}
|
|
|
|
func (m *Manager) acceptLoop(t *Target) {
|
|
for {
|
|
conn, err := t.listener.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-t.closeCh:
|
|
return
|
|
default:
|
|
}
|
|
t.stats.LastError = err.Error()
|
|
m.notify("[forward] #%d accept error: %v", t.ID, err)
|
|
return
|
|
}
|
|
|
|
t.connsMu.Lock()
|
|
t.conns[conn] = struct{}{}
|
|
t.connsMu.Unlock()
|
|
|
|
m.notify("[forward] #%d accepted %s", t.ID, conn.RemoteAddr())
|
|
go m.readLoop(t, conn, t.closeCh)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) processChunk(t *Target, data []byte) {
|
|
if len(data) == 0 {
|
|
return
|
|
}
|
|
n := len(data)
|
|
atomic.AddUint64(&t.stats.ReadBytes, uint64(n))
|
|
chunk := make([]byte, n)
|
|
copy(chunk, data)
|
|
if wErr := m.writeToSerial(chunk); wErr != nil {
|
|
t.stats.LastError = wErr.Error()
|
|
m.notify("[forward] #%d write serial error: %v", t.ID, wErr)
|
|
} else if m.onInbound != nil {
|
|
m.onInbound(t.ID, chunk)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) readLoopError(t *Target, err error) {
|
|
select {
|
|
case <-t.closeCh:
|
|
return
|
|
default:
|
|
}
|
|
t.Connected = false
|
|
t.stats.LastError = err.Error()
|
|
m.notify("[forward] #%d disconnected: %v", t.ID, err)
|
|
}
|
|
|
|
func (m *Manager) readLoopPacket(t *Target) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, addr, err := t.packetConn.ReadFrom(buf)
|
|
if n > 0 {
|
|
m.processChunk(t, buf[:n])
|
|
t.mu.Lock()
|
|
t.remoteAddrs[addr.String()] = addr
|
|
t.mu.Unlock()
|
|
}
|
|
if err != nil {
|
|
m.readLoopError(t, err)
|
|
return
|
|
}
|
|
select {
|
|
case <-t.closeCh:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) readLoopSerial(t *Target) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := t.serialPort.Read(buf)
|
|
if n > 0 {
|
|
m.processChunk(t, buf[:n])
|
|
}
|
|
if err != nil {
|
|
m.readLoopError(t, err)
|
|
return
|
|
}
|
|
select {
|
|
case <-t.closeCh:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) readLoop(t *Target, conn net.Conn, stop <-chan struct{}) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := conn.Read(buf)
|
|
if n > 0 {
|
|
m.processChunk(t, buf[:n])
|
|
}
|
|
if err != nil {
|
|
t.Connected = false
|
|
t.stats.LastError = err.Error()
|
|
if t.Mode == TCPServer {
|
|
t.connsMu.Lock()
|
|
delete(t.conns, conn)
|
|
t.connsMu.Unlock()
|
|
}
|
|
m.notify("[forward] #%d disconnected: %v", t.ID, err)
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
select {
|
|
case <-stop:
|
|
_ = conn.Close()
|
|
if t.Mode == TCPServer {
|
|
t.connsMu.Lock()
|
|
delete(t.conns, conn)
|
|
t.connsMu.Unlock()
|
|
}
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove disconnects and removes a target.
|
|
func (m *Manager) Remove(id int) error {
|
|
m.mu.Lock()
|
|
t, ok := m.targets[id]
|
|
if !ok {
|
|
m.mu.Unlock()
|
|
return fmt.Errorf("forward #%d not found", id)
|
|
}
|
|
delete(m.targets, id)
|
|
m.mu.Unlock()
|
|
|
|
t.close()
|
|
m.notify("[forward] #%d removed", id)
|
|
return nil
|
|
}
|
|
|
|
// Enable (re)connects a target.
|
|
func (m *Manager) Enable(id int) error {
|
|
m.mu.RLock()
|
|
t, ok := m.targets[id]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("forward #%d not found", id)
|
|
}
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.Enabled && t.Connected {
|
|
return nil
|
|
}
|
|
|
|
switch t.Mode {
|
|
case TCP, UDP:
|
|
conn, err := net.Dial(t.Mode.Network(), t.Address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return err
|
|
}
|
|
t.conn = conn
|
|
t.Connected = true
|
|
t.closeCh = make(chan struct{})
|
|
t.closed = false
|
|
go m.readLoop(t, conn, t.closeCh)
|
|
|
|
case TCPServer:
|
|
listener, err := net.Listen("tcp", t.Address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return err
|
|
}
|
|
t.listener = listener
|
|
t.conns = make(map[net.Conn]struct{})
|
|
t.Connected = true
|
|
t.closeCh = make(chan struct{})
|
|
t.closed = false
|
|
go m.acceptLoop(t)
|
|
|
|
case UDPServer:
|
|
pc, err := net.ListenPacket("udp", t.Address)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return err
|
|
}
|
|
t.packetConn = pc
|
|
t.remoteAddrs = make(map[string]net.Addr)
|
|
t.Connected = true
|
|
t.closeCh = make(chan struct{})
|
|
t.closed = false
|
|
go m.readLoopPacket(t)
|
|
|
|
case COMPort:
|
|
sp, err := serial.Open(t.Address, &serial.Mode{BaudRate: 115200, DataBits: 8, StopBits: 0, Parity: 0})
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
return err
|
|
}
|
|
t.serialPort = sp
|
|
t.Connected = true
|
|
t.closeCh = make(chan struct{})
|
|
t.closed = false
|
|
go m.readLoopSerial(t)
|
|
}
|
|
|
|
t.Enabled = true
|
|
m.notify("[forward] #%d enabled", id)
|
|
return nil
|
|
}
|
|
|
|
// Update changes a target's mode and address, reconnecting if enabled.
|
|
func (m *Manager) Update(id int, mode Mode, address string) error {
|
|
if mode == None {
|
|
return fmt.Errorf("forward mode cannot be none")
|
|
}
|
|
|
|
m.mu.RLock()
|
|
t, ok := m.targets[id]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("forward #%d not found", id)
|
|
}
|
|
|
|
t.mu.Lock()
|
|
wasEnabled := t.Enabled
|
|
t.Mode = mode
|
|
t.Address = address
|
|
t.mu.Unlock()
|
|
|
|
t.close()
|
|
|
|
if !wasEnabled {
|
|
m.notify("[forward] #%d updated (disabled)", id)
|
|
return nil
|
|
}
|
|
|
|
return m.Enable(id)
|
|
}
|
|
|
|
// Disable disconnects a target without removing it.
|
|
func (m *Manager) Disable(id int) error {
|
|
m.mu.RLock()
|
|
t, ok := m.targets[id]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("forward #%d not found", id)
|
|
}
|
|
|
|
t.mu.Lock()
|
|
t.Enabled = false
|
|
t.mu.Unlock()
|
|
t.close()
|
|
m.notify("[forward] #%d disabled", id)
|
|
return nil
|
|
}
|
|
|
|
// Broadcast sends data to all enabled, connected targets.
|
|
func (m *Manager) Broadcast(data []byte) {
|
|
if len(data) == 0 {
|
|
return
|
|
}
|
|
|
|
m.mu.RLock()
|
|
items := make([]*Target, 0, len(m.targets))
|
|
for _, t := range m.targets {
|
|
items = append(items, t)
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
for _, t := range items {
|
|
if !t.Enabled || !t.Connected {
|
|
continue
|
|
}
|
|
|
|
switch t.Mode {
|
|
case TCP, UDP:
|
|
if t.conn == nil {
|
|
continue
|
|
}
|
|
n, err := t.conn.Write(data)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
m.notify("[forward] #%d write error: %v", t.ID, err)
|
|
} else {
|
|
atomic.AddUint64(&t.stats.WrittenBytes, uint64(n))
|
|
}
|
|
|
|
case TCPServer:
|
|
t.connsMu.Lock()
|
|
conns := make([]net.Conn, 0, len(t.conns))
|
|
for c := range t.conns {
|
|
conns = append(conns, c)
|
|
}
|
|
t.connsMu.Unlock()
|
|
for _, c := range conns {
|
|
n, err := c.Write(data)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
} else {
|
|
atomic.AddUint64(&t.stats.WrittenBytes, uint64(n))
|
|
}
|
|
}
|
|
|
|
case UDPServer:
|
|
t.mu.Lock()
|
|
addrs := make([]net.Addr, 0, len(t.remoteAddrs))
|
|
for _, addr := range t.remoteAddrs {
|
|
addrs = append(addrs, addr)
|
|
}
|
|
t.mu.Unlock()
|
|
for _, addr := range addrs {
|
|
n, err := t.packetConn.WriteTo(data, addr)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
} else {
|
|
atomic.AddUint64(&t.stats.WrittenBytes, uint64(n))
|
|
}
|
|
}
|
|
|
|
case COMPort:
|
|
if t.serialPort == nil {
|
|
continue
|
|
}
|
|
n, err := t.serialPort.Write(data)
|
|
if err != nil {
|
|
t.stats.LastError = err.Error()
|
|
m.notify("[forward] #%d write error: %v", t.ID, err)
|
|
} else {
|
|
atomic.AddUint64(&t.stats.WrittenBytes, uint64(n))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// List returns a snapshot of all targets.
|
|
func (m *Manager) List() []Snapshot {
|
|
m.mu.RLock()
|
|
items := make([]Snapshot, 0, len(m.targets))
|
|
for _, t := range m.targets {
|
|
items = append(items, Snapshot{
|
|
ID: t.ID,
|
|
Mode: t.Mode.String(),
|
|
Address: t.Address,
|
|
Enabled: t.Enabled,
|
|
Connected: t.Connected,
|
|
ReadBytes: atomic.LoadUint64(&t.stats.ReadBytes),
|
|
WriteByte: atomic.LoadUint64(&t.stats.WrittenBytes),
|
|
LastError: t.stats.LastError,
|
|
Conns: t.acceptedConns(),
|
|
})
|
|
}
|
|
m.mu.RUnlock()
|
|
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].ID < items[j].ID
|
|
})
|
|
|
|
return items
|
|
}
|
|
|
|
// Close disconnects and removes all targets.
|
|
func (m *Manager) Close() {
|
|
m.mu.Lock()
|
|
items := make([]*Target, 0, len(m.targets))
|
|
for _, t := range m.targets {
|
|
items = append(items, t)
|
|
}
|
|
m.targets = map[int]*Target{}
|
|
m.mu.Unlock()
|
|
|
|
for _, t := range items {
|
|
t.close()
|
|
}
|
|
}
|
|
|
|
func (t *Target) close() {
|
|
t.mu.Lock()
|
|
if t.closed {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
t.closed = true
|
|
ch := t.closeCh
|
|
conn := t.conn
|
|
listener := t.listener
|
|
pc := t.packetConn
|
|
sp := t.serialPort
|
|
t.conn = nil
|
|
t.listener = nil
|
|
t.packetConn = nil
|
|
t.serialPort = nil
|
|
t.Connected = false
|
|
t.mu.Unlock()
|
|
|
|
if ch != nil {
|
|
close(ch)
|
|
}
|
|
if conn != nil {
|
|
_ = conn.Close()
|
|
}
|
|
if listener != nil {
|
|
_ = listener.Close()
|
|
}
|
|
if pc != nil {
|
|
_ = pc.Close()
|
|
}
|
|
if sp != nil {
|
|
_ = sp.Close()
|
|
}
|
|
}
|