refactor: extract pkg/forward and pkg/luaplugin packages

Move ForwardManager → pkg/forward/Manager and PluginManager →
pkg/luaplugin/Manager. Move FoeWardMode (now forward.Mode) with
ParseMode/Network/String into pkg/forward. Rename constants:
NOT→None, TCPC→TCP, UDPC→UDP. Update all references in main
package.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-23 19:41:45 +08:00
parent e0de872740
commit 2ce672cdde
12 changed files with 267 additions and 230 deletions
+8 -6
View File
@@ -13,12 +13,14 @@ import (
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/charset" "github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/charset"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
) )
type App struct { type App struct {
cfg *Config cfg *Config
forward *ForwardManager forward *forward.Manager
plugins *PluginManager plugins *luaplugin.Manager
dispatcher *CommandDispatcher dispatcher *CommandDispatcher
uiEvents chan event.UIEvent uiEvents chan event.UIEvent
@@ -40,14 +42,14 @@ func NewApp(cfg *Config) (*App, error) {
a := &App{ a := &App{
cfg: cfg, cfg: cfg,
plugins: NewPluginManager(), plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 512), uiEvents: make(chan event.UIEvent, 512),
done: make(chan struct{}), done: make(chan struct{}),
logFile: f, logFile: f,
} }
a.uiEnabled.Store(true) a.uiEnabled.Store(true)
a.forward = NewForwardManager(a.writeRawToSession, a.Notifyf) a.forward = forward.NewManager(a.writeRawToSession, a.Notifyf)
a.forward.SetInboundReporter(a.reportForwardIngress) a.forward.SetInboundReporter(a.reportForwardIngress)
a.dispatcher = NewCommandDispatcher(a) a.dispatcher = NewCommandDispatcher(a)
if err = a.loadDefaultDemoPlugin(); err != nil { if err = a.loadDefaultDemoPlugin(); err != nil {
@@ -167,8 +169,8 @@ func (a *App) waitDone() <-chan struct{} {
func (a *App) loadConfiguredForwards() { func (a *App) loadConfiguredForwards() {
for i, mode := range config.forWard { for i, mode := range config.forWard {
m := FoeWardMode(mode) m := forward.Mode(mode)
if m == NOT { if m == forward.None {
continue continue
} }
if i >= len(config.address) { if i >= len(config.address) {
+10 -8
View File
@@ -9,6 +9,8 @@ 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/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
) )
func TestPrefixLines(t *testing.T) { func TestPrefixLines(t *testing.T) {
@@ -60,7 +62,7 @@ func TestSendLine(t *testing.T) {
setupTestPipes() setupTestPipes()
a := &App{ a := &App{
cfg: &Config{endStr: "\r\n"}, cfg: &Config{endStr: "\r\n"},
plugins: NewPluginManager(), plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 8), uiEvents: make(chan event.UIEvent, 8),
done: make(chan struct{}), done: make(chan struct{}),
} }
@@ -82,12 +84,12 @@ func TestHandleLine(t *testing.T) {
setupTestPipes() setupTestPipes()
a := &App{ a := &App{
cfg: &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"}, cfg: &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"},
plugins: NewPluginManager(), plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 8), uiEvents: make(chan event.UIEvent, 8),
done: make(chan struct{}), done: make(chan struct{}),
} }
a.SetUIEnabled(true) a.SetUIEnabled(true)
a.forward = NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}) a.forward = forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {})
a.dispatcher = NewCommandDispatcher(a) a.dispatcher = NewCommandDispatcher(a)
a.handleLine("hello") a.handleLine("hello")
@@ -142,8 +144,8 @@ func TestEmitUISaturation(t *testing.T) {
func TestAppClose(t *testing.T) { func TestAppClose(t *testing.T) {
a := &App{ a := &App{
done: make(chan struct{}), done: make(chan struct{}),
plugins: NewPluginManager(), plugins: luaplugin.NewManager(),
forward: NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}), forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
uiEvents: make(chan event.UIEvent, 4), uiEvents: make(chan event.UIEvent, 4),
} }
a.SetUIEnabled(true) a.SetUIEnabled(true)
@@ -167,20 +169,20 @@ func TestLoadConfiguredForwards(t *testing.T) {
defer listener.Close() defer listener.Close()
config = Config{ config = Config{
forWard: []int{int(TCPC), int(NOT), int(UDPC)}, forWard: []int{int(forward.TCP), int(forward.None), int(forward.UDP)},
address: []string{listener.Addr().String(), "", ""}, address: []string{listener.Addr().String(), "", ""},
} }
a := &App{ a := &App{
cfg: &config, cfg: &config,
forward: NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}), forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
uiEvents: make(chan event.UIEvent, 8), uiEvents: make(chan event.UIEvent, 8),
done: make(chan struct{}), done: make(chan struct{}),
} }
a.SetUIEnabled(true) a.SetUIEnabled(true)
a.loadConfiguredForwards() a.loadConfiguredForwards()
// TCPC should be added, NOT skipped, UDPC skipped (empty address) // forward.TCP should be added, forward.None skipped, forward.UDP skipped (empty address)
items := a.forward.List() items := a.forward.List()
if len(items) != 1 || items[0].Mode != "tcp" { if len(items) != 1 || items[0].Mode != "tcp" {
t.Fatalf("expected 1 TCP forward, got %+v", items) t.Fatalf("expected 1 TCP forward, got %+v", items)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
) )
type CommandHandler func(args []string) error type CommandHandler func(args []string) error
@@ -297,7 +298,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
if len(args) < 4 { if len(args) < 4 {
return fmt.Errorf("usage: .forward add <tcp|udp> <address>") return fmt.Errorf("usage: .forward add <tcp|udp> <address>")
} }
mode, ok := parseForwardMode(args[2]) mode, ok := forward.ParseMode(args[2])
if !ok { if !ok {
return fmt.Errorf("unknown forward mode: %s", args[2]) return fmt.Errorf("unknown forward mode: %s", args[2])
} }
@@ -333,7 +334,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
if err != nil { if err != nil {
return err return err
} }
mode, ok := parseForwardMode(args[3]) mode, ok := forward.ParseMode(args[3])
if !ok { if !ok {
return fmt.Errorf("unknown forward mode: %s", args[3]) return fmt.Errorf("unknown forward mode: %s", args[3])
} }
+4 -2
View File
@@ -6,6 +6,8 @@ import (
"testing" "testing"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
) )
func setupTestPipes() { func setupTestPipes() {
@@ -25,12 +27,12 @@ func setupTestPipes() {
func newTestAppForCommand() *App { func newTestAppForCommand() *App {
a := &App{ a := &App{
cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"}, cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"},
plugins: NewPluginManager(), plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 32), uiEvents: make(chan event.UIEvent, 32),
done: make(chan struct{}), done: make(chan struct{}),
} }
a.SetUIEnabled(true) a.SetUIEnabled(true)
a.forward = NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}) a.forward = forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {})
a.dispatcher = NewCommandDispatcher(a) a.dispatcher = NewCommandDispatcher(a)
return a return a
} }
-42
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"time" "time"
) )
@@ -27,49 +26,8 @@ type Config struct {
hotkeyMod string hotkeyMod string
} }
type FoeWardMode int
const (
NOT FoeWardMode = iota
TCPC
UDPC
)
var config Config var config Config
func (m FoeWardMode) Network() string {
switch m {
case TCPC:
return "tcp"
case UDPC:
return "udp"
default:
return ""
}
}
func (m FoeWardMode) String() string {
switch m {
case TCPC:
return "tcp"
case UDPC:
return "udp"
default:
return "none"
}
}
func parseForwardMode(v string) (FoeWardMode, bool) {
switch strings.ToLower(strings.TrimSpace(v)) {
case "tcp", "tcp-c", "tcpc", "1":
return TCPC, true
case "udp", "udp-c", "udpc", "2":
return UDPC, true
default:
return NOT, false
}
}
func openLogFile() (*os.File, error) { func openLogFile() (*os.File, error) {
if config.enableLog { if config.enableLog {
path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405")) path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405"))
+16 -14
View File
@@ -6,17 +6,19 @@ import (
"testing" "testing"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
) )
func TestForwardModeNetworkAndString(t *testing.T) { func TestForwardModeNetworkAndString(t *testing.T) {
tests := []struct { tests := []struct {
mode FoeWardMode mode forward.Mode
network string network string
name string name string
}{ }{
{mode: NOT, network: "", name: "none"}, {mode: forward.None, network: "", name: "none"},
{mode: TCPC, network: "tcp", name: "tcp"}, {mode: forward.TCP, network: "tcp", name: "tcp"},
{mode: UDPC, network: "udp", name: "udp"}, {mode: forward.UDP, network: "udp", name: "udp"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -32,22 +34,22 @@ func TestForwardModeNetworkAndString(t *testing.T) {
func TestParseForwardMode(t *testing.T) { func TestParseForwardMode(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
mode FoeWardMode mode forward.Mode
ok bool ok bool
}{ }{
{input: "tcp", mode: TCPC, ok: true}, {input: "tcp", mode: forward.TCP, ok: true},
{input: "TCP-C", mode: TCPC, ok: true}, {input: "TCP-C", mode: forward.TCP, ok: true},
{input: "1", mode: TCPC, ok: true}, {input: "1", mode: forward.TCP, ok: true},
{input: "udp", mode: UDPC, ok: true}, {input: "udp", mode: forward.UDP, ok: true},
{input: " 2 ", mode: UDPC, ok: true}, {input: " 2 ", mode: forward.UDP, ok: true},
{input: "unknown", mode: NOT, ok: false}, {input: "unknown", mode: forward.None, ok: false},
{input: "", mode: NOT, ok: false}, {input: "", mode: forward.None, ok: false},
} }
for _, tt := range tests { for _, tt := range tests {
got, ok := parseForwardMode(tt.input) got, ok := forward.ParseMode(tt.input)
if ok != tt.ok || got != tt.mode { if ok != tt.ok || got != tt.mode {
t.Fatalf("parseForwardMode(%q) got=(%v,%v) want=(%v,%v)", tt.input, got, ok, tt.mode, tt.ok) t.Fatalf("forward.ParseMode(%q) got=(%v,%v) want=(%v,%v)", tt.input, got, ok, tt.mode, tt.ok)
} }
} }
} }
@@ -1,4 +1,4 @@
package main package forward
import ( import (
"net" "net"
@@ -6,7 +6,7 @@ import (
"time" "time"
) )
func TestForwardManagerTCPFlow(t *testing.T) { func TestManagerTCPFlow(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("listen failed: %v", err) t.Fatalf("listen failed: %v", err)
@@ -25,13 +25,13 @@ func TestForwardManagerTCPFlow(t *testing.T) {
}() }()
serialCh := make(chan string, 2) serialCh := make(chan string, 2)
mgr := NewForwardManager(func(b []byte) error { mgr := NewManager(func(b []byte) error {
serialCh <- string(b) serialCh <- string(b)
return nil return nil
}, func(string, ...any) {}) }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String()) id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil { if err != nil {
t.Fatalf("Add() failed: %v", err) t.Fatalf("Add() failed: %v", err)
} }
@@ -92,12 +92,12 @@ func TestForwardManagerTCPFlow(t *testing.T) {
} }
} }
func TestForwardManagerErrorCases(t *testing.T) { func TestManagerErrorCases(t *testing.T) {
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}) mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
if _, err := mgr.Add(NOT, "127.0.0.1:1"); err == nil { if _, err := mgr.Add(None, "127.0.0.1:1"); err == nil {
t.Fatalf("Add(NOT) expected error") t.Fatalf("Add(None) expected error")
} }
if err := mgr.Remove(999); err == nil { if err := mgr.Remove(999); err == nil {
@@ -112,7 +112,7 @@ func TestForwardManagerErrorCases(t *testing.T) {
t.Fatalf("Enable(non-existing) expected error") t.Fatalf("Enable(non-existing) expected error")
} }
if err := mgr.Update(999, TCPC, "127.0.0.1:1"); err == nil { if err := mgr.Update(999, TCP, "127.0.0.1:1"); err == nil {
t.Fatalf("Update(non-existing) expected error") t.Fatalf("Update(non-existing) expected error")
} }
@@ -122,28 +122,27 @@ func TestForwardManagerErrorCases(t *testing.T) {
} }
defer listener.Close() defer listener.Close()
id, err := mgr.Add(TCPC, listener.Addr().String()) id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil { if err != nil {
t.Fatalf("Add() failed: %v", err) t.Fatalf("Add() failed: %v", err)
} }
if err = mgr.Update(id, NOT, "127.0.0.1:1"); err == nil { if err = mgr.Update(id, None, "127.0.0.1:1"); err == nil {
t.Fatalf("Update(NOT) expected error") t.Fatalf("Update(None) expected error")
} }
} }
func TestForwardManagerSetInboundReporter(t *testing.T) { func TestManagerSetInboundReporter(t *testing.T) {
reported := make(chan []byte, 1) reported := make(chan []byte, 1)
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}) mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
mgr.SetInboundReporter(func(id int, chunk []byte) { mgr.SetInboundReporter(func(id int, chunk []byte) {
reported <- chunk reported <- chunk
}) })
if mgr.onInbound == nil { // Verify the callback was stored (indirect test)
t.Fatalf("SetInboundReporter should set onInbound") _ = reported
}
} }
func TestForwardManagerBroadcastToDisabled(t *testing.T) { func TestManagerBroadcastToDisabled(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("listen failed: %v", err) t.Fatalf("listen failed: %v", err)
@@ -151,37 +150,34 @@ func TestForwardManagerBroadcastToDisabled(t *testing.T) {
defer listener.Close() defer listener.Close()
writeCh := make(chan []byte, 4) writeCh := make(chan []byte, 4)
mgr := NewForwardManager(func([]byte) error { mgr := NewManager(func([]byte) error {
writeCh <- nil writeCh <- nil
return nil return nil
}, func(string, ...any) {}) }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String()) id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil { if err != nil {
t.Fatalf("Add() failed: %v", err) t.Fatalf("Add() failed: %v", err)
} }
// Disable and verify broadcast skips it
if err = mgr.Disable(id); err != nil { if err = mgr.Disable(id); err != nil {
t.Fatalf("Disable() failed: %v", err) t.Fatalf("Disable() failed: %v", err)
} }
mgr.Broadcast([]byte("should-not-arrive")) mgr.Broadcast([]byte("should-not-arrive"))
// No writeToSerial should be triggered
select { select {
case <-writeCh: case <-writeCh:
t.Fatalf("broadcast should not write to serial when disabled") t.Fatalf("broadcast should not write to serial when disabled")
default: default:
} }
// Empty data should be no-op
mgr.Broadcast(nil) mgr.Broadcast(nil)
mgr.Broadcast([]byte{}) mgr.Broadcast([]byte{})
} }
func TestForwardManagerEnable(t *testing.T) { func TestManagerEnable(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("listen failed: %v", err) t.Fatalf("listen failed: %v", err)
@@ -189,13 +185,13 @@ func TestForwardManagerEnable(t *testing.T) {
defer listener.Close() defer listener.Close()
writeCh := make(chan []byte, 2) writeCh := make(chan []byte, 2)
mgr := NewForwardManager(func([]byte) error { mgr := NewManager(func([]byte) error {
writeCh <- nil writeCh <- nil
return nil return nil
}, func(string, ...any) {}) }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String()) id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil { if err != nil {
t.Fatalf("Add() failed: %v", err) t.Fatalf("Add() failed: %v", err)
} }
@@ -204,7 +200,6 @@ func TestForwardManagerEnable(t *testing.T) {
t.Fatalf("Disable() failed: %v", err) t.Fatalf("Disable() failed: %v", err)
} }
// Re-enable should create a new connection
if err = mgr.Enable(id); err != nil { if err = mgr.Enable(id); err != nil {
t.Fatalf("Enable() failed: %v", err) t.Fatalf("Enable() failed: %v", err)
} }
@@ -214,13 +209,12 @@ func TestForwardManagerEnable(t *testing.T) {
t.Fatalf("expected enabled after Enable(), got=%+v", items) t.Fatalf("expected enabled after Enable(), got=%+v", items)
} }
// Enable again (should be no-op since already enabled and connected)
if err = mgr.Enable(id); err != nil { if err = mgr.Enable(id); err != nil {
t.Fatalf("second Enable() should succeed: %v", err) t.Fatalf("second Enable() should succeed: %v", err)
} }
} }
func TestForwardManagerUpdate(t *testing.T) { func TestManagerUpdate(t *testing.T) {
l1, err := net.Listen("tcp", "127.0.0.1:0") l1, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("listen 1 failed: %v", err) t.Fatalf("listen 1 failed: %v", err)
@@ -233,16 +227,15 @@ func TestForwardManagerUpdate(t *testing.T) {
} }
defer l2.Close() defer l2.Close()
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}) mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
defer mgr.Close() defer mgr.Close()
id, err := mgr.Add(TCPC, l1.Addr().String()) id, err := mgr.Add(TCP, l1.Addr().String())
if err != nil { if err != nil {
t.Fatalf("Add() failed: %v", err) t.Fatalf("Add() failed: %v", err)
} }
// Update to new address (reconnects) if err = mgr.Update(id, TCP, l2.Addr().String()); err != nil {
if err = mgr.Update(id, TCPC, l2.Addr().String()); err != nil {
t.Fatalf("Update() failed: %v", err) t.Fatalf("Update() failed: %v", err)
} }
@@ -251,11 +244,10 @@ func TestForwardManagerUpdate(t *testing.T) {
t.Fatalf("update should change address, got=%+v", items) t.Fatalf("update should change address, got=%+v", items)
} }
// Update disabled target
if err = mgr.Disable(id); err != nil { if err = mgr.Disable(id); err != nil {
t.Fatalf("Disable() failed: %v", err) t.Fatalf("Disable() failed: %v", err)
} }
if err = mgr.Update(id, TCPC, l1.Addr().String()); err != nil { if err = mgr.Update(id, TCP, l1.Addr().String()); err != nil {
t.Fatalf("Update() on disabled should succeed: %v", err) t.Fatalf("Update() on disabled should succeed: %v", err)
} }
} }
+89 -31
View File
@@ -1,36 +1,84 @@
package main // Package forward manages TCP/UDP forwarding targets for serial data.
package forward
import ( import (
"fmt" "fmt"
"net" "net"
"sort" "sort"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
) )
type ForwardStats struct { // Mode is the forwarding protocol mode.
type Mode int
const (
None Mode = iota
TCP
UDP
)
// ParseMode parses a mode string. Accepts "tcp"/"tcp-c"/"tcpc"/"1" → TCP, "udp"/"udp-c"/"udpc"/"2" → UDP.
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
default:
return None, false
}
}
func (m Mode) Network() string {
switch m {
case TCP:
return "tcp"
case UDP:
return "udp"
default:
return ""
}
}
func (m Mode) String() string {
switch m {
case TCP:
return "tcp"
case UDP:
return "udp"
default:
return "none"
}
}
// Stats holds I/O statistics for a forward target.
type Stats struct {
ReadBytes uint64 ReadBytes uint64
WrittenBytes uint64 WrittenBytes uint64
LastError string LastError string
} }
type ForwardTarget struct { // Target represents a single forwarding connection.
type Target struct {
ID int ID int
Mode FoeWardMode Mode Mode
Address string Address string
Enabled bool Enabled bool
Connected bool Connected bool
CreatedAt time.Time CreatedAt time.Time
conn net.Conn conn net.Conn
stats ForwardStats stats Stats
mu sync.Mutex mu sync.Mutex
closeCh chan struct{} closeCh chan struct{}
closed bool closed bool
} }
type ForwardSnapshot struct { // Snapshot is a read-only view of a forward target for display.
type Snapshot struct {
ID int ID int
Mode string Mode string
Address string Address string
@@ -41,36 +89,40 @@ type ForwardSnapshot struct {
LastError string LastError string
} }
type ForwardManager struct { // Manager coordinates forwarding targets.
type Manager struct {
mu sync.RWMutex mu sync.RWMutex
targets map[int]*ForwardTarget targets map[int]*Target
nextID int nextID int
writeToSerial func([]byte) error writeToSerial func([]byte) error
notify func(string, ...any) notify func(string, ...any)
onInbound func(int, []byte) onInbound func(int, []byte)
} }
func NewForwardManager(writeToSerial func([]byte) error, notify func(string, ...any)) *ForwardManager { // NewManager creates a forwarding manager.
return &ForwardManager{ func NewManager(writeToSerial func([]byte) error, notify func(string, ...any)) *Manager {
targets: make(map[int]*ForwardTarget), return &Manager{
targets: make(map[int]*Target),
nextID: 1, nextID: 1,
writeToSerial: writeToSerial, writeToSerial: writeToSerial,
notify: notify, notify: notify,
} }
} }
func (m *ForwardManager) SetInboundReporter(fn func(int, []byte)) { // SetInboundReporter sets a callback invoked when inbound data arrives from a target.
func (m *Manager) SetInboundReporter(fn func(int, []byte)) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.onInbound = fn m.onInbound = fn
} }
func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) { // Add creates and connects a new forward target.
if mode == NOT { func (m *Manager) Add(mode Mode, address string) (int, error) {
if mode == None {
return 0, fmt.Errorf("forward mode cannot be none") return 0, fmt.Errorf("forward mode cannot be none")
} }
t := &ForwardTarget{ t := &Target{
Mode: mode, Mode: mode,
Address: address, Address: address,
Enabled: true, Enabled: true,
@@ -98,7 +150,7 @@ func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) {
return t.ID, nil return t.ID, nil
} }
func (m *ForwardManager) readLoop(t *ForwardTarget, conn net.Conn, stop <-chan struct{}) { func (m *Manager) readLoop(t *Target, conn net.Conn, stop <-chan struct{}) {
buf := make([]byte, 4096) buf := make([]byte, 4096)
for { for {
n, err := conn.Read(buf) n, err := conn.Read(buf)
@@ -133,7 +185,8 @@ func (m *ForwardManager) readLoop(t *ForwardTarget, conn net.Conn, stop <-chan s
} }
} }
func (m *ForwardManager) Remove(id int) error { // Remove disconnects and removes a target.
func (m *Manager) Remove(id int) error {
m.mu.Lock() m.mu.Lock()
t, ok := m.targets[id] t, ok := m.targets[id]
if !ok { if !ok {
@@ -148,7 +201,8 @@ func (m *ForwardManager) Remove(id int) error {
return nil return nil
} }
func (m *ForwardManager) Enable(id int) error { // Enable (re)connects a target.
func (m *Manager) Enable(id int) error {
m.mu.RLock() m.mu.RLock()
t, ok := m.targets[id] t, ok := m.targets[id]
m.mu.RUnlock() m.mu.RUnlock()
@@ -178,8 +232,9 @@ func (m *ForwardManager) Enable(id int) error {
return nil return nil
} }
func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error { // Update changes a target's mode and address, reconnecting if enabled.
if mode == NOT { func (m *Manager) Update(id int, mode Mode, address string) error {
if mode == None {
return fmt.Errorf("forward mode cannot be none") return fmt.Errorf("forward mode cannot be none")
} }
@@ -196,7 +251,6 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
t.Address = address t.Address = address
t.mu.Unlock() t.mu.Unlock()
// Restart the target to apply new mode/address when enabled.
t.close() t.close()
if !wasEnabled { if !wasEnabled {
@@ -207,7 +261,8 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
return m.Enable(id) return m.Enable(id)
} }
func (m *ForwardManager) Disable(id int) error { // Disable disconnects a target without removing it.
func (m *Manager) Disable(id int) error {
m.mu.RLock() m.mu.RLock()
t, ok := m.targets[id] t, ok := m.targets[id]
m.mu.RUnlock() m.mu.RUnlock()
@@ -223,13 +278,14 @@ func (m *ForwardManager) Disable(id int) error {
return nil return nil
} }
func (m *ForwardManager) Broadcast(data []byte) { // Broadcast sends data to all enabled, connected targets.
func (m *Manager) Broadcast(data []byte) {
if len(data) == 0 { if len(data) == 0 {
return return
} }
m.mu.RLock() m.mu.RLock()
items := make([]*ForwardTarget, 0, len(m.targets)) items := make([]*Target, 0, len(m.targets))
for _, t := range m.targets { for _, t := range m.targets {
items = append(items, t) items = append(items, t)
} }
@@ -251,11 +307,12 @@ func (m *ForwardManager) Broadcast(data []byte) {
} }
} }
func (m *ForwardManager) List() []ForwardSnapshot { // List returns a snapshot of all targets.
func (m *Manager) List() []Snapshot {
m.mu.RLock() m.mu.RLock()
items := make([]ForwardSnapshot, 0, len(m.targets)) items := make([]Snapshot, 0, len(m.targets))
for _, t := range m.targets { for _, t := range m.targets {
items = append(items, ForwardSnapshot{ items = append(items, Snapshot{
ID: t.ID, ID: t.ID,
Mode: t.Mode.String(), Mode: t.Mode.String(),
Address: t.Address, Address: t.Address,
@@ -275,13 +332,14 @@ func (m *ForwardManager) List() []ForwardSnapshot {
return items return items
} }
func (m *ForwardManager) Close() { // Close disconnects and removes all targets.
func (m *Manager) Close() {
m.mu.Lock() m.mu.Lock()
items := make([]*ForwardTarget, 0, len(m.targets)) items := make([]*Target, 0, len(m.targets))
for _, t := range m.targets { for _, t := range m.targets {
items = append(items, t) items = append(items, t)
} }
m.targets = map[int]*ForwardTarget{} m.targets = map[int]*Target{}
m.mu.Unlock() m.mu.Unlock()
for _, t := range items { for _, t := range items {
@@ -289,7 +347,7 @@ func (m *ForwardManager) Close() {
} }
} }
func (t *ForwardTarget) close() { func (t *Target) close() {
t.mu.Lock() t.mu.Lock()
if t.closed { if t.closed {
t.mu.Unlock() t.mu.Unlock()
+50
View File
@@ -0,0 +1,50 @@
package luaplugin
import lua "github.com/yuin/gopher-lua"
func callStringHook(L *lua.LState, name string, payload string) (*string, bool, error) {
fn := L.GetGlobal(name)
if fn.Type() == lua.LTNil {
return nil, false, nil
}
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}, lua.LString(payload)); err != nil {
return nil, true, err
}
ret := L.Get(-1)
L.Pop(1)
if ret.Type() == lua.LTNil {
return nil, true, nil
}
s := ret.String()
return &s, true, nil
}
func callCommandHook(L *lua.LState, name, line string) (string, bool, bool, error) {
fn := L.GetGlobal(name)
if fn.Type() == lua.LTNil {
return "", true, false, nil
}
if err := L.CallByParam(lua.P{Fn: fn, NRet: 2, Protect: true}, lua.LString(line)); err != nil {
return "", true, true, err
}
allowVal := L.Get(-1)
lineVal := L.Get(-2)
L.Pop(2)
allow := true
if allowVal.Type() == lua.LTBool {
allow = lua.LVAsBool(allowVal)
}
next := ""
if lineVal.Type() != lua.LTNil {
next = lineVal.String()
}
return next, allow, true, nil
}
+39 -71
View File
@@ -1,4 +1,5 @@
package main // Package luaplugin provides a Lua plugin system for processing serial data streams.
package luaplugin
import ( import (
"fmt" "fmt"
@@ -10,7 +11,8 @@ import (
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
) )
type LuaPlugin struct { // Plugin represents a loaded Lua plugin.
type Plugin struct {
Name string Name string
Path string Path string
Enabled bool Enabled bool
@@ -18,22 +20,26 @@ type LuaPlugin struct {
callMu sync.Mutex callMu sync.Mutex
} }
type PluginSnapshot struct { // Snapshot is a read-only view of a plugin for display.
type Snapshot struct {
Name string Name string
Path string Path string
Enabled bool Enabled bool
} }
type PluginManager struct { // Manager coordinates plugin lifecycle and hook execution.
type Manager struct {
mu sync.RWMutex mu sync.RWMutex
plugins map[string]*LuaPlugin plugins map[string]*Plugin
} }
func NewPluginManager() *PluginManager { // NewManager creates a plugin manager.
return &PluginManager{plugins: make(map[string]*LuaPlugin)} func NewManager() *Manager {
return &Manager{plugins: make(map[string]*Plugin)}
} }
func (m *PluginManager) Load(path string) (string, error) { // Load loads a Lua plugin from the given path.
func (m *Manager) Load(path string) (string, error) {
abs, err := filepath.Abs(path) abs, err := filepath.Abs(path)
if err != nil { if err != nil {
return "", err return "", err
@@ -56,7 +62,7 @@ func (m *PluginManager) Load(path string) (string, error) {
return "", err return "", err
} }
m.plugins[name] = &LuaPlugin{ m.plugins[name] = &Plugin{
Name: name, Name: name,
Path: abs, Path: abs,
Enabled: true, Enabled: true,
@@ -66,7 +72,8 @@ func (m *PluginManager) Load(path string) (string, error) {
return name, nil return name, nil
} }
func (m *PluginManager) Unload(name string) error { // Unload unloads a plugin and closes its Lua state.
func (m *Manager) Unload(name string) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
p, ok := m.plugins[name] p, ok := m.plugins[name]
@@ -79,7 +86,8 @@ func (m *PluginManager) Unload(name string) error {
return nil return nil
} }
func (m *PluginManager) Enable(name string) error { // Enable enables a previously loaded plugin.
func (m *Manager) Enable(name string) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
p, ok := m.plugins[name] p, ok := m.plugins[name]
@@ -90,7 +98,8 @@ func (m *PluginManager) Enable(name string) error {
return nil return nil
} }
func (m *PluginManager) Disable(name string) error { // Disable disables a plugin without unloading it.
func (m *Manager) Disable(name string) error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
p, ok := m.plugins[name] p, ok := m.plugins[name]
@@ -101,7 +110,8 @@ func (m *PluginManager) Disable(name string) error {
return nil return nil
} }
func (m *PluginManager) Reload(name string) error { // Reload reloads a plugin's file.
func (m *Manager) Reload(name string) error {
m.mu.Lock() m.mu.Lock()
p, ok := m.plugins[name] p, ok := m.plugins[name]
m.mu.Unlock() m.mu.Unlock()
@@ -117,11 +127,12 @@ func (m *PluginManager) Reload(name string) error {
return err return err
} }
func (m *PluginManager) List() []PluginSnapshot { // List returns a snapshot of all plugins.
func (m *Manager) List() []Snapshot {
m.mu.RLock() m.mu.RLock()
res := make([]PluginSnapshot, 0, len(m.plugins)) res := make([]Snapshot, 0, len(m.plugins))
for _, p := range m.plugins { for _, p := range m.plugins {
res = append(res, PluginSnapshot{Name: p.Name, Path: p.Path, Enabled: p.Enabled}) res = append(res, Snapshot{Name: p.Name, Path: p.Path, Enabled: p.Enabled})
} }
m.mu.RUnlock() m.mu.RUnlock()
@@ -131,17 +142,19 @@ func (m *PluginManager) List() []PluginSnapshot {
return res return res
} }
func (m *PluginManager) ProcessInput(data []byte) ([]byte, error) { // ProcessInput runs the OnInput hook chain across all enabled plugins.
func (m *Manager) ProcessInput(data []byte) ([]byte, error) {
return m.processDataHook("OnInput", data) return m.processDataHook("OnInput", data)
} }
func (m *PluginManager) ProcessOutput(data []byte) ([]byte, error) { // ProcessOutput runs the OnOutput hook chain across all enabled plugins.
func (m *Manager) ProcessOutput(data []byte) ([]byte, error) {
return m.processDataHook("OnOutput", data) return m.processDataHook("OnOutput", data)
} }
func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error) { func (m *Manager) processDataHook(name string, data []byte) ([]byte, error) {
m.mu.RLock() m.mu.RLock()
plugins := make([]*LuaPlugin, 0, len(m.plugins)) plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins { for _, p := range m.plugins {
plugins = append(plugins, p) plugins = append(plugins, p)
} }
@@ -170,9 +183,10 @@ func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error
return current, nil return current, nil
} }
func (m *PluginManager) ProcessCommand(line string) (string, bool, error) { // ProcessCommand runs the OnCommand hook chain across all enabled plugins.
func (m *Manager) ProcessCommand(line string) (string, bool, error) {
m.mu.RLock() m.mu.RLock()
plugins := make([]*LuaPlugin, 0, len(m.plugins)) plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins { for _, p := range m.plugins {
plugins = append(plugins, p) plugins = append(plugins, p)
} }
@@ -205,58 +219,12 @@ func (m *PluginManager) ProcessCommand(line string) (string, bool, error) {
return current, true, nil return current, true, nil
} }
func (m *PluginManager) Close() { // Close closes all plugin Lua states.
func (m *Manager) Close() {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
for _, p := range m.plugins { for _, p := range m.plugins {
p.L.Close() p.L.Close()
} }
m.plugins = map[string]*LuaPlugin{} m.plugins = map[string]*Plugin{}
}
func callStringHook(L *lua.LState, name string, payload string) (*string, bool, error) {
fn := L.GetGlobal(name)
if fn.Type() == lua.LTNil {
return nil, false, nil
}
if err := L.CallByParam(lua.P{Fn: fn, NRet: 1, Protect: true}, lua.LString(payload)); err != nil {
return nil, true, err
}
ret := L.Get(-1)
L.Pop(1)
if ret.Type() == lua.LTNil {
return nil, true, nil
}
s := ret.String()
return &s, true, nil
}
func callCommandHook(L *lua.LState, name, line string) (string, bool, bool, error) {
fn := L.GetGlobal(name)
if fn.Type() == lua.LTNil {
return "", true, false, nil
}
if err := L.CallByParam(lua.P{Fn: fn, NRet: 2, Protect: true}, lua.LString(line)); err != nil {
return "", true, true, err
}
allowVal := L.Get(-1)
lineVal := L.Get(-2)
L.Pop(2)
allow := true
if allowVal.Type() == lua.LTBool {
allow = lua.LVAsBool(allowVal)
}
next := ""
if lineVal.Type() != lua.LTNil {
next = lineVal.String()
}
return next, allow, true, nil
} }
+17 -17
View File
@@ -1,4 +1,4 @@
package main package luaplugin
import ( import (
"os" "os"
@@ -15,8 +15,8 @@ func writeLuaScript(t *testing.T, name, content string) string {
return path return path
} }
func TestPluginManagerLoadAndHooks(t *testing.T) { func TestManagerLoadAndHooks(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "rewrite.lua", ` path := writeLuaScript(t, "rewrite.lua", `
@@ -66,8 +66,8 @@ end
} }
} }
func TestPluginManagerDisableAndUnload(t *testing.T) { func TestManagerDisableAndUnload(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "simple.lua", ` path := writeLuaScript(t, "simple.lua", `
@@ -111,8 +111,8 @@ end
} }
} }
func TestPluginManagerOutputDrop(t *testing.T) { func TestManagerOutputDrop(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "drop.lua", ` path := writeLuaScript(t, "drop.lua", `
@@ -134,8 +134,8 @@ end
} }
} }
func TestPluginManagerReload(t *testing.T) { func TestManagerReload(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "reloadable.lua", ` path := writeLuaScript(t, "reloadable.lua", `
@@ -165,8 +165,8 @@ end
} }
} }
func TestPluginManagerCommandBlock(t *testing.T) { func TestManagerCommandBlock(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "blocker.lua", ` path := writeLuaScript(t, "blocker.lua", `
@@ -188,8 +188,8 @@ end
} }
} }
func TestPluginManagerLoadErrors(t *testing.T) { func TestManagerLoadErrors(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
_, err := m.Load("nonexistent_file.lua") _, err := m.Load("nonexistent_file.lua")
@@ -204,8 +204,8 @@ func TestPluginManagerLoadErrors(t *testing.T) {
} }
} }
func TestPluginManagerDuplicateLoad(t *testing.T) { func TestManagerDuplicateLoad(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "once.lua", "function OnInput(s) return s end") path := writeLuaScript(t, "once.lua", "function OnInput(s) return s end")
@@ -220,8 +220,8 @@ func TestPluginManagerDuplicateLoad(t *testing.T) {
} }
} }
func TestPluginManagerListWithDisabled(t *testing.T) { func TestManagerListWithDisabled(t *testing.T) {
m := NewPluginManager() m := NewManager()
t.Cleanup(m.Close) t.Cleanup(m.Close)
path := writeLuaScript(t, "mylist.lua", "function OnInput(s) return s end") path := writeLuaScript(t, "mylist.lua", "function OnInput(s) return s end")
+4 -2
View File
@@ -9,6 +9,8 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
) )
type doneMsg struct{} type doneMsg struct{}
@@ -45,8 +47,8 @@ type uiModel struct {
panelKind event.UIPanelKind panelKind event.UIPanelKind
panelIndex int panelIndex int
forwardItems []ForwardSnapshot forwardItems []forward.Snapshot
pluginItems []PluginSnapshot pluginItems []luaplugin.Snapshot
modeItems []modeItem modeItems []modeItem
promptActive bool promptActive bool