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/pkg/charset"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
)
type App struct {
cfg *Config
forward *ForwardManager
plugins *PluginManager
forward *forward.Manager
plugins *luaplugin.Manager
dispatcher *CommandDispatcher
uiEvents chan event.UIEvent
@@ -40,14 +42,14 @@ func NewApp(cfg *Config) (*App, error) {
a := &App{
cfg: cfg,
plugins: NewPluginManager(),
plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 512),
done: make(chan struct{}),
logFile: f,
}
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.dispatcher = NewCommandDispatcher(a)
if err = a.loadDefaultDemoPlugin(); err != nil {
@@ -167,8 +169,8 @@ func (a *App) waitDone() <-chan struct{} {
func (a *App) loadConfiguredForwards() {
for i, mode := range config.forWard {
m := FoeWardMode(mode)
if m == NOT {
m := forward.Mode(mode)
if m == forward.None {
continue
}
if i >= len(config.address) {
+10 -8
View File
@@ -9,6 +9,8 @@ import (
"go.bug.st/serial"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
)
func TestPrefixLines(t *testing.T) {
@@ -60,7 +62,7 @@ func TestSendLine(t *testing.T) {
setupTestPipes()
a := &App{
cfg: &Config{endStr: "\r\n"},
plugins: NewPluginManager(),
plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 8),
done: make(chan struct{}),
}
@@ -82,12 +84,12 @@ func TestHandleLine(t *testing.T) {
setupTestPipes()
a := &App{
cfg: &Config{endStr: "\n", inputCode: "UTF-8", outputCode: "UTF-8"},
plugins: NewPluginManager(),
plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 8),
done: make(chan struct{}),
}
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.handleLine("hello")
@@ -142,8 +144,8 @@ func TestEmitUISaturation(t *testing.T) {
func TestAppClose(t *testing.T) {
a := &App{
done: make(chan struct{}),
plugins: NewPluginManager(),
forward: NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {}),
plugins: luaplugin.NewManager(),
forward: forward.NewManager(func([]byte) error { return nil }, func(string, ...any) {}),
uiEvents: make(chan event.UIEvent, 4),
}
a.SetUIEnabled(true)
@@ -167,20 +169,20 @@ func TestLoadConfiguredForwards(t *testing.T) {
defer listener.Close()
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(), "", ""},
}
a := &App{
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),
done: make(chan struct{}),
}
a.SetUIEnabled(true)
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()
if len(items) != 1 || items[0].Mode != "tcp" {
t.Fatalf("expected 1 TCP forward, got %+v", items)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
)
type CommandHandler func(args []string) error
@@ -297,7 +298,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
if len(args) < 4 {
return fmt.Errorf("usage: .forward add <tcp|udp> <address>")
}
mode, ok := parseForwardMode(args[2])
mode, ok := forward.ParseMode(args[2])
if !ok {
return fmt.Errorf("unknown forward mode: %s", args[2])
}
@@ -333,7 +334,7 @@ func (d *CommandDispatcher) handleForwardCommand(args []string) error {
if err != nil {
return err
}
mode, ok := parseForwardMode(args[3])
mode, ok := forward.ParseMode(args[3])
if !ok {
return fmt.Errorf("unknown forward mode: %s", args[3])
}
+4 -2
View File
@@ -6,6 +6,8 @@ import (
"testing"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
)
func setupTestPipes() {
@@ -25,12 +27,12 @@ func setupTestPipes() {
func newTestAppForCommand() *App {
a := &App{
cfg: &Config{inputCode: "UTF-8", outputCode: "UTF-8", endStr: "\n"},
plugins: NewPluginManager(),
plugins: luaplugin.NewManager(),
uiEvents: make(chan event.UIEvent, 32),
done: make(chan struct{}),
}
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)
return a
}
-42
View File
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"os"
"strings"
"time"
)
@@ -27,49 +26,8 @@ type Config struct {
hotkeyMod string
}
type FoeWardMode int
const (
NOT FoeWardMode = iota
TCPC
UDPC
)
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) {
if config.enableLog {
path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405"))
+16 -14
View File
@@ -6,17 +6,19 @@ import (
"testing"
"github.com/spf13/pflag"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
)
func TestForwardModeNetworkAndString(t *testing.T) {
tests := []struct {
mode FoeWardMode
mode forward.Mode
network string
name string
}{
{mode: NOT, network: "", name: "none"},
{mode: TCPC, network: "tcp", name: "tcp"},
{mode: UDPC, network: "udp", name: "udp"},
{mode: forward.None, network: "", name: "none"},
{mode: forward.TCP, network: "tcp", name: "tcp"},
{mode: forward.UDP, network: "udp", name: "udp"},
}
for _, tt := range tests {
@@ -32,22 +34,22 @@ func TestForwardModeNetworkAndString(t *testing.T) {
func TestParseForwardMode(t *testing.T) {
tests := []struct {
input string
mode FoeWardMode
mode forward.Mode
ok bool
}{
{input: "tcp", mode: TCPC, ok: true},
{input: "TCP-C", mode: TCPC, ok: true},
{input: "1", mode: TCPC, ok: true},
{input: "udp", mode: UDPC, ok: true},
{input: " 2 ", mode: UDPC, ok: true},
{input: "unknown", mode: NOT, ok: false},
{input: "", mode: NOT, ok: false},
{input: "tcp", mode: forward.TCP, ok: true},
{input: "TCP-C", mode: forward.TCP, ok: true},
{input: "1", mode: forward.TCP, ok: true},
{input: "udp", mode: forward.UDP, ok: true},
{input: " 2 ", mode: forward.UDP, ok: true},
{input: "unknown", mode: forward.None, ok: false},
{input: "", mode: forward.None, ok: false},
}
for _, tt := range tests {
got, ok := parseForwardMode(tt.input)
got, ok := forward.ParseMode(tt.input)
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 (
"net"
@@ -6,7 +6,7 @@ import (
"time"
)
func TestForwardManagerTCPFlow(t *testing.T) {
func TestManagerTCPFlow(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen failed: %v", err)
@@ -25,13 +25,13 @@ func TestForwardManagerTCPFlow(t *testing.T) {
}()
serialCh := make(chan string, 2)
mgr := NewForwardManager(func(b []byte) error {
mgr := NewManager(func(b []byte) error {
serialCh <- string(b)
return nil
}, func(string, ...any) {})
defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String())
id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil {
t.Fatalf("Add() failed: %v", err)
}
@@ -92,12 +92,12 @@ func TestForwardManagerTCPFlow(t *testing.T) {
}
}
func TestForwardManagerErrorCases(t *testing.T) {
mgr := NewForwardManager(func([]byte) error { return nil }, func(string, ...any) {})
func TestManagerErrorCases(t *testing.T) {
mgr := NewManager(func([]byte) error { return nil }, func(string, ...any) {})
defer mgr.Close()
if _, err := mgr.Add(NOT, "127.0.0.1:1"); err == nil {
t.Fatalf("Add(NOT) expected error")
if _, err := mgr.Add(None, "127.0.0.1:1"); err == nil {
t.Fatalf("Add(None) expected error")
}
if err := mgr.Remove(999); err == nil {
@@ -112,7 +112,7 @@ func TestForwardManagerErrorCases(t *testing.T) {
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")
}
@@ -122,28 +122,27 @@ func TestForwardManagerErrorCases(t *testing.T) {
}
defer listener.Close()
id, err := mgr.Add(TCPC, listener.Addr().String())
id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil {
t.Fatalf("Add() failed: %v", err)
}
if err = mgr.Update(id, NOT, "127.0.0.1:1"); err == nil {
t.Fatalf("Update(NOT) expected error")
if err = mgr.Update(id, None, "127.0.0.1:1"); err == nil {
t.Fatalf("Update(None) expected error")
}
}
func TestForwardManagerSetInboundReporter(t *testing.T) {
func TestManagerSetInboundReporter(t *testing.T) {
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()
mgr.SetInboundReporter(func(id int, chunk []byte) {
reported <- chunk
})
if mgr.onInbound == nil {
t.Fatalf("SetInboundReporter should set onInbound")
}
// Verify the callback was stored (indirect test)
_ = reported
}
func TestForwardManagerBroadcastToDisabled(t *testing.T) {
func TestManagerBroadcastToDisabled(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen failed: %v", err)
@@ -151,37 +150,34 @@ func TestForwardManagerBroadcastToDisabled(t *testing.T) {
defer listener.Close()
writeCh := make(chan []byte, 4)
mgr := NewForwardManager(func([]byte) error {
mgr := NewManager(func([]byte) error {
writeCh <- nil
return nil
}, func(string, ...any) {})
defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String())
id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil {
t.Fatalf("Add() failed: %v", err)
}
// Disable and verify broadcast skips it
if err = mgr.Disable(id); err != nil {
t.Fatalf("Disable() failed: %v", err)
}
mgr.Broadcast([]byte("should-not-arrive"))
// No writeToSerial should be triggered
select {
case <-writeCh:
t.Fatalf("broadcast should not write to serial when disabled")
default:
}
// Empty data should be no-op
mgr.Broadcast(nil)
mgr.Broadcast([]byte{})
}
func TestForwardManagerEnable(t *testing.T) {
func TestManagerEnable(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen failed: %v", err)
@@ -189,13 +185,13 @@ func TestForwardManagerEnable(t *testing.T) {
defer listener.Close()
writeCh := make(chan []byte, 2)
mgr := NewForwardManager(func([]byte) error {
mgr := NewManager(func([]byte) error {
writeCh <- nil
return nil
}, func(string, ...any) {})
defer mgr.Close()
id, err := mgr.Add(TCPC, listener.Addr().String())
id, err := mgr.Add(TCP, listener.Addr().String())
if err != nil {
t.Fatalf("Add() failed: %v", err)
}
@@ -204,7 +200,6 @@ func TestForwardManagerEnable(t *testing.T) {
t.Fatalf("Disable() failed: %v", err)
}
// Re-enable should create a new connection
if err = mgr.Enable(id); err != nil {
t.Fatalf("Enable() failed: %v", err)
}
@@ -214,13 +209,12 @@ func TestForwardManagerEnable(t *testing.T) {
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 {
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")
if err != nil {
t.Fatalf("listen 1 failed: %v", err)
@@ -233,16 +227,15 @@ func TestForwardManagerUpdate(t *testing.T) {
}
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()
id, err := mgr.Add(TCPC, l1.Addr().String())
id, err := mgr.Add(TCP, l1.Addr().String())
if err != nil {
t.Fatalf("Add() failed: %v", err)
}
// Update to new address (reconnects)
if err = mgr.Update(id, TCPC, l2.Addr().String()); err != nil {
if err = mgr.Update(id, TCP, l2.Addr().String()); err != nil {
t.Fatalf("Update() failed: %v", err)
}
@@ -251,11 +244,10 @@ func TestForwardManagerUpdate(t *testing.T) {
t.Fatalf("update should change address, got=%+v", items)
}
// Update disabled target
if err = mgr.Disable(id); err != nil {
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)
}
}
+89 -31
View File
@@ -1,36 +1,84 @@
package main
// Package forward manages TCP/UDP forwarding targets for serial data.
package forward
import (
"fmt"
"net"
"sort"
"strings"
"sync"
"sync/atomic"
"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
WrittenBytes uint64
LastError string
}
type ForwardTarget struct {
// Target represents a single forwarding connection.
type Target struct {
ID int
Mode FoeWardMode
Mode Mode
Address string
Enabled bool
Connected bool
CreatedAt time.Time
conn net.Conn
stats ForwardStats
stats Stats
mu sync.Mutex
closeCh chan struct{}
closed bool
}
type ForwardSnapshot struct {
// Snapshot is a read-only view of a forward target for display.
type Snapshot struct {
ID int
Mode string
Address string
@@ -41,36 +89,40 @@ type ForwardSnapshot struct {
LastError string
}
type ForwardManager struct {
// Manager coordinates forwarding targets.
type Manager struct {
mu sync.RWMutex
targets map[int]*ForwardTarget
targets map[int]*Target
nextID int
writeToSerial func([]byte) error
notify func(string, ...any)
onInbound func(int, []byte)
}
func NewForwardManager(writeToSerial func([]byte) error, notify func(string, ...any)) *ForwardManager {
return &ForwardManager{
targets: make(map[int]*ForwardTarget),
// 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,
}
}
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()
defer m.mu.Unlock()
m.onInbound = fn
}
func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) {
if mode == NOT {
// 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 := &ForwardTarget{
t := &Target{
Mode: mode,
Address: address,
Enabled: true,
@@ -98,7 +150,7 @@ func (m *ForwardManager) Add(mode FoeWardMode, address string) (int, error) {
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)
for {
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()
t, ok := m.targets[id]
if !ok {
@@ -148,7 +201,8 @@ func (m *ForwardManager) Remove(id int) error {
return nil
}
func (m *ForwardManager) Enable(id int) error {
// Enable (re)connects a target.
func (m *Manager) Enable(id int) error {
m.mu.RLock()
t, ok := m.targets[id]
m.mu.RUnlock()
@@ -178,8 +232,9 @@ func (m *ForwardManager) Enable(id int) error {
return nil
}
func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error {
if mode == NOT {
// 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")
}
@@ -196,7 +251,6 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
t.Address = address
t.mu.Unlock()
// Restart the target to apply new mode/address when enabled.
t.close()
if !wasEnabled {
@@ -207,7 +261,8 @@ func (m *ForwardManager) Update(id int, mode FoeWardMode, address string) error
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()
t, ok := m.targets[id]
m.mu.RUnlock()
@@ -223,13 +278,14 @@ func (m *ForwardManager) Disable(id int) error {
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 {
return
}
m.mu.RLock()
items := make([]*ForwardTarget, 0, len(m.targets))
items := make([]*Target, 0, len(m.targets))
for _, t := range m.targets {
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()
items := make([]ForwardSnapshot, 0, len(m.targets))
items := make([]Snapshot, 0, len(m.targets))
for _, t := range m.targets {
items = append(items, ForwardSnapshot{
items = append(items, Snapshot{
ID: t.ID,
Mode: t.Mode.String(),
Address: t.Address,
@@ -275,13 +332,14 @@ func (m *ForwardManager) List() []ForwardSnapshot {
return items
}
func (m *ForwardManager) Close() {
// Close disconnects and removes all targets.
func (m *Manager) Close() {
m.mu.Lock()
items := make([]*ForwardTarget, 0, len(m.targets))
items := make([]*Target, 0, len(m.targets))
for _, t := range m.targets {
items = append(items, t)
}
m.targets = map[int]*ForwardTarget{}
m.targets = map[int]*Target{}
m.mu.Unlock()
for _, t := range items {
@@ -289,7 +347,7 @@ func (m *ForwardManager) Close() {
}
}
func (t *ForwardTarget) close() {
func (t *Target) close() {
t.mu.Lock()
if t.closed {
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 (
"fmt"
@@ -10,7 +11,8 @@ import (
lua "github.com/yuin/gopher-lua"
)
type LuaPlugin struct {
// Plugin represents a loaded Lua plugin.
type Plugin struct {
Name string
Path string
Enabled bool
@@ -18,22 +20,26 @@ type LuaPlugin struct {
callMu sync.Mutex
}
type PluginSnapshot struct {
// Snapshot is a read-only view of a plugin for display.
type Snapshot struct {
Name string
Path string
Enabled bool
}
type PluginManager struct {
// Manager coordinates plugin lifecycle and hook execution.
type Manager struct {
mu sync.RWMutex
plugins map[string]*LuaPlugin
plugins map[string]*Plugin
}
func NewPluginManager() *PluginManager {
return &PluginManager{plugins: make(map[string]*LuaPlugin)}
// NewManager creates a plugin manager.
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)
if err != nil {
return "", err
@@ -56,7 +62,7 @@ func (m *PluginManager) Load(path string) (string, error) {
return "", err
}
m.plugins[name] = &LuaPlugin{
m.plugins[name] = &Plugin{
Name: name,
Path: abs,
Enabled: true,
@@ -66,7 +72,8 @@ func (m *PluginManager) Load(path string) (string, error) {
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()
defer m.mu.Unlock()
p, ok := m.plugins[name]
@@ -79,7 +86,8 @@ func (m *PluginManager) Unload(name string) error {
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()
defer m.mu.Unlock()
p, ok := m.plugins[name]
@@ -90,7 +98,8 @@ func (m *PluginManager) Enable(name string) error {
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()
defer m.mu.Unlock()
p, ok := m.plugins[name]
@@ -101,7 +110,8 @@ func (m *PluginManager) Disable(name string) error {
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()
p, ok := m.plugins[name]
m.mu.Unlock()
@@ -117,11 +127,12 @@ func (m *PluginManager) Reload(name string) error {
return err
}
func (m *PluginManager) List() []PluginSnapshot {
// List returns a snapshot of all plugins.
func (m *Manager) List() []Snapshot {
m.mu.RLock()
res := make([]PluginSnapshot, 0, len(m.plugins))
res := make([]Snapshot, 0, len(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()
@@ -131,17 +142,19 @@ func (m *PluginManager) List() []PluginSnapshot {
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)
}
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)
}
func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error) {
func (m *Manager) processDataHook(name string, data []byte) ([]byte, error) {
m.mu.RLock()
plugins := make([]*LuaPlugin, 0, len(m.plugins))
plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins {
plugins = append(plugins, p)
}
@@ -170,9 +183,10 @@ func (m *PluginManager) processDataHook(name string, data []byte) ([]byte, error
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()
plugins := make([]*LuaPlugin, 0, len(m.plugins))
plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins {
plugins = append(plugins, p)
}
@@ -205,58 +219,12 @@ func (m *PluginManager) ProcessCommand(line string) (string, bool, error) {
return current, true, nil
}
func (m *PluginManager) Close() {
// Close closes all plugin Lua states.
func (m *Manager) Close() {
m.mu.Lock()
defer m.mu.Unlock()
for _, p := range m.plugins {
p.L.Close()
}
m.plugins = map[string]*LuaPlugin{}
}
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
m.plugins = map[string]*Plugin{}
}
+17 -17
View File
@@ -1,4 +1,4 @@
package main
package luaplugin
import (
"os"
@@ -15,8 +15,8 @@ func writeLuaScript(t *testing.T, name, content string) string {
return path
}
func TestPluginManagerLoadAndHooks(t *testing.T) {
m := NewPluginManager()
func TestManagerLoadAndHooks(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "rewrite.lua", `
@@ -66,8 +66,8 @@ end
}
}
func TestPluginManagerDisableAndUnload(t *testing.T) {
m := NewPluginManager()
func TestManagerDisableAndUnload(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "simple.lua", `
@@ -111,8 +111,8 @@ end
}
}
func TestPluginManagerOutputDrop(t *testing.T) {
m := NewPluginManager()
func TestManagerOutputDrop(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "drop.lua", `
@@ -134,8 +134,8 @@ end
}
}
func TestPluginManagerReload(t *testing.T) {
m := NewPluginManager()
func TestManagerReload(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "reloadable.lua", `
@@ -165,8 +165,8 @@ end
}
}
func TestPluginManagerCommandBlock(t *testing.T) {
m := NewPluginManager()
func TestManagerCommandBlock(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "blocker.lua", `
@@ -188,8 +188,8 @@ end
}
}
func TestPluginManagerLoadErrors(t *testing.T) {
m := NewPluginManager()
func TestManagerLoadErrors(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
_, err := m.Load("nonexistent_file.lua")
@@ -204,8 +204,8 @@ func TestPluginManagerLoadErrors(t *testing.T) {
}
}
func TestPluginManagerDuplicateLoad(t *testing.T) {
m := NewPluginManager()
func TestManagerDuplicateLoad(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
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) {
m := NewPluginManager()
func TestManagerListWithDisabled(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
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"
"github.com/charmbracelet/lipgloss"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/forward"
"github.com/jixishi/SerialTerminalForWindowsTerminal/pkg/luaplugin"
)
type doneMsg struct{}
@@ -45,8 +47,8 @@ type uiModel struct {
panelKind event.UIPanelKind
panelIndex int
forwardItems []ForwardSnapshot
pluginItems []PluginSnapshot
forwardItems []forward.Snapshot
pluginItems []luaplugin.Snapshot
modeItems []modeItem
promptActive bool