mirror of
https://github.com/jixishi/SerialTerminalForWindowsTerminal.git
synced 2026-06-15 16:42:46 +00:00
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:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user