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
+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
}
+230
View File
@@ -0,0 +1,230 @@
// Package luaplugin provides a Lua plugin system for processing serial data streams.
package luaplugin
import (
"fmt"
"path/filepath"
"sort"
"strings"
"sync"
lua "github.com/yuin/gopher-lua"
)
// Plugin represents a loaded Lua plugin.
type Plugin struct {
Name string
Path string
Enabled bool
L *lua.LState
callMu sync.Mutex
}
// Snapshot is a read-only view of a plugin for display.
type Snapshot struct {
Name string
Path string
Enabled bool
}
// Manager coordinates plugin lifecycle and hook execution.
type Manager struct {
mu sync.RWMutex
plugins map[string]*Plugin
}
// NewManager creates a plugin manager.
func NewManager() *Manager {
return &Manager{plugins: make(map[string]*Plugin)}
}
// 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
}
name := strings.TrimSuffix(filepath.Base(abs), filepath.Ext(abs))
if name == "" {
return "", fmt.Errorf("invalid plugin name")
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.plugins[name]; ok {
return "", fmt.Errorf("plugin %s already loaded", name)
}
state := lua.NewState()
if err = state.DoFile(abs); err != nil {
state.Close()
return "", err
}
m.plugins[name] = &Plugin{
Name: name,
Path: abs,
Enabled: true,
L: state,
}
return name, nil
}
// 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]
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
p.L.Close()
delete(m.plugins, name)
return nil
}
// 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]
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
p.Enabled = true
return nil
}
// 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]
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
p.Enabled = false
return nil
}
// Reload reloads a plugin's file.
func (m *Manager) Reload(name string) error {
m.mu.Lock()
p, ok := m.plugins[name]
m.mu.Unlock()
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
path := p.Path
if err := m.Unload(name); err != nil {
return err
}
_, err := m.Load(path)
return err
}
// List returns a snapshot of all plugins.
func (m *Manager) List() []Snapshot {
m.mu.RLock()
res := make([]Snapshot, 0, len(m.plugins))
for _, p := range m.plugins {
res = append(res, Snapshot{Name: p.Name, Path: p.Path, Enabled: p.Enabled})
}
m.mu.RUnlock()
sort.Slice(res, func(i, j int) bool {
return res[i].Name < res[j].Name
})
return res
}
// ProcessInput runs the OnInput hook chain across all enabled plugins.
func (m *Manager) ProcessInput(data []byte) ([]byte, error) {
return m.processDataHook("OnInput", data)
}
// 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 *Manager) processDataHook(name string, data []byte) ([]byte, error) {
m.mu.RLock()
plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins {
plugins = append(plugins, p)
}
m.mu.RUnlock()
current := data
for _, p := range plugins {
if !p.Enabled {
continue
}
p.callMu.Lock()
ret, called, err := callStringHook(p.L, name, string(current))
p.callMu.Unlock()
if err != nil {
return nil, fmt.Errorf("plugin %s %s: %w", p.Name, name, err)
}
if !called {
continue
}
if ret == nil {
return nil, nil
}
current = []byte(*ret)
}
return current, nil
}
// ProcessCommand runs the OnCommand hook chain across all enabled plugins.
func (m *Manager) ProcessCommand(line string) (string, bool, error) {
m.mu.RLock()
plugins := make([]*Plugin, 0, len(m.plugins))
for _, p := range m.plugins {
plugins = append(plugins, p)
}
m.mu.RUnlock()
current := line
allow := true
for _, p := range plugins {
if !p.Enabled {
continue
}
p.callMu.Lock()
next, nextAllow, called, err := callCommandHook(p.L, "OnCommand", current)
p.callMu.Unlock()
if err != nil {
return "", false, fmt.Errorf("plugin %s OnCommand: %w", p.Name, err)
}
if !called {
continue
}
allow = allow && nextAllow
if !allow {
return "", false, nil
}
if next != "" {
current = next
}
}
return current, true, nil
}
// 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]*Plugin{}
}
+241
View File
@@ -0,0 +1,241 @@
package luaplugin
import (
"os"
"path/filepath"
"testing"
)
func writeLuaScript(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("write lua script failed: %v", err)
}
return path
}
func TestManagerLoadAndHooks(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "rewrite.lua", `
function OnInput(s)
return s .. "-in"
end
function OnOutput(s)
return s .. "-out"
end
function OnCommand(line)
return line .. " --lua", true
end
`)
name, err := m.Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if name != "rewrite" {
t.Fatalf("unexpected plugin name: %q", name)
}
in, err := m.ProcessInput([]byte("abc"))
if err != nil {
t.Fatalf("ProcessInput() failed: %v", err)
}
if string(in) != "abc-in" {
t.Fatalf("ProcessInput() got=%q want=%q", in, "abc-in")
}
out, err := m.ProcessOutput([]byte("xyz"))
if err != nil {
t.Fatalf("ProcessOutput() failed: %v", err)
}
if string(out) != "xyz-out" {
t.Fatalf("ProcessOutput() got=%q want=%q", out, "xyz-out")
}
line, allow, err := m.ProcessCommand(".help")
if err != nil {
t.Fatalf("ProcessCommand() failed: %v", err)
}
if !allow || line != ".help --lua" {
t.Fatalf("ProcessCommand() got=(%q,%v) want=(%q,true)", line, allow, ".help --lua")
}
}
func TestManagerDisableAndUnload(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "simple.lua", `
function OnInput(s)
return s .. "-x"
end
`)
name, err := m.Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if err = m.Disable(name); err != nil {
t.Fatalf("Disable() failed: %v", err)
}
got, err := m.ProcessInput([]byte("abc"))
if err != nil {
t.Fatalf("ProcessInput() with disabled plugin failed: %v", err)
}
if string(got) != "abc" {
t.Fatalf("disabled plugin should not modify input, got=%q", got)
}
if err = m.Enable(name); err != nil {
t.Fatalf("Enable() failed: %v", err)
}
got, err = m.ProcessInput([]byte("abc"))
if err != nil {
t.Fatalf("ProcessInput() after enable failed: %v", err)
}
if string(got) != "abc-x" {
t.Fatalf("enabled plugin should modify input, got=%q", got)
}
if err = m.Unload(name); err != nil {
t.Fatalf("Unload() failed: %v", err)
}
if len(m.List()) != 0 {
t.Fatalf("Unload() should remove plugin from list")
}
}
func TestManagerOutputDrop(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "drop.lua", `
function OnOutput(s)
return nil
end
`)
if _, err := m.Load(path); err != nil {
t.Fatalf("Load() failed: %v", err)
}
out, err := m.ProcessOutput([]byte("abc"))
if err != nil {
t.Fatalf("ProcessOutput() failed: %v", err)
}
if out != nil {
t.Fatalf("expected nil output when plugin returns nil")
}
}
func TestManagerReload(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "reloadable.lua", `
function OnInput(s)
return s .. "-v1"
end
`)
name, err := m.Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if err = m.Reload(name); err != nil {
t.Fatalf("Reload() failed: %v", err)
}
out, err := m.ProcessInput([]byte("test"))
if err != nil {
t.Fatalf("ProcessInput() after reload failed: %v", err)
}
if string(out) != "test-v1" {
t.Fatalf("reloaded plugin should still work, got=%q", out)
}
if err = m.Reload("nonexistent"); err == nil {
t.Fatalf("Reload() non-existent should error")
}
}
func TestManagerCommandBlock(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "blocker.lua", `
function OnCommand(line)
return line, false
end
`)
if _, err := m.Load(path); err != nil {
t.Fatalf("Load() failed: %v", err)
}
line, allow, err := m.ProcessCommand(".exit")
if err != nil {
t.Fatalf("ProcessCommand() failed: %v", err)
}
if allow {
t.Fatalf("command should be blocked, got allow=%v line=%q", allow, line)
}
}
func TestManagerLoadErrors(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
_, err := m.Load("nonexistent_file.lua")
if err == nil {
t.Fatalf("Load() non-existent file should error")
}
path := writeLuaScript(t, "bad.lua", "this is not valid lua {{{")
_, err = m.Load(path)
if err == nil {
t.Fatalf("Load() invalid lua should error")
}
}
func TestManagerDuplicateLoad(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "once.lua", "function OnInput(s) return s end")
_, err := m.Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
_, err = m.Load(path)
if err == nil {
t.Fatalf("Load() duplicate should error")
}
}
func TestManagerListWithDisabled(t *testing.T) {
m := NewManager()
t.Cleanup(m.Close)
path := writeLuaScript(t, "mylist.lua", "function OnInput(s) return s end")
name, err := m.Load(path)
if err != nil {
t.Fatalf("Load() failed: %v", err)
}
if err = m.Disable(name); err != nil {
t.Fatalf("Disable() failed: %v", err)
}
items := m.List()
if len(items) != 1 || items[0].Enabled {
t.Fatalf("expected disabled in list, got %+v", items)
}
}