fix: второй стабилизационный проход Lua plugin lifecycle
1. Enabled/Active state separation: - Enable() sets Enabled=true (persisted in config), does NOT create runtime - ActivatePlugin() checks Enabled && !Active, creates VM + scheduler - DeactivatePlugin() stops runtime, keeps Enabled=true - InitRuntimes() iterates Enabled plugins, sets Active=true after creation - SyncConfig() restores Enabled from config, does NOT touch Active 2. ActivatePlugin: добавлен vm.SetServices(m.Services) 3. Discover: атомарная замена списка (newPlugins slice), нет дублирования 4. CallPluginFunction: thread-safe через LuaVM.CallFunction (vm.mu + callWithTimeout) 5. Uninstall активного плагина: полная деактивация (StopScheduler → on_shutdown → CloseVM → Active=false) 6. GetPluginPanelHTML: валидация panel path (no absolute, no .., must be .html, must be within plugin dir) 7. PluginPage: убран hardcoded 'calendar-plugin', используется funcPrefix из pluginName Тесты: - security_test.go: +8 тестов (FullLifecycle, ActivatePlugin_Services, Discover_Idempotent, ReloadPlugins_NoDuplicates, CallPluginFunction_Timeout, Uninstall_ActivePlugin, GetPluginPanelHTML_PathTraversal, FullLifecycle_EndToEnd) - manager_test.go: обновлены тесты под новую семантику Enabled/Active
This commit is contained in:
@@ -105,22 +105,28 @@ func (a *App) ListPlugins() []PluginDTO {
|
||||
}
|
||||
|
||||
// SetPluginEnabled persists the enabled/disabled state and applies it to the runtime.
|
||||
// Returns error if the plugin is not installed but has install lifecycle.
|
||||
// Enable: marks plugin as enabled, then activates runtime (VM + scheduler).
|
||||
// Disable: deactivates runtime, then marks plugin as disabled.
|
||||
func (a *App) SetPluginEnabled(name string, enabled bool) error {
|
||||
if a.plugins == nil {
|
||||
return fmt.Errorf("plugin manager not ready")
|
||||
}
|
||||
|
||||
if enabled {
|
||||
// Enable first (sets Enabled=true), then activate runtime
|
||||
if err := a.plugins.Enable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
a.plugins.ActivatePlugin(name)
|
||||
} else {
|
||||
// Deactivate runtime first, then disable
|
||||
a.plugins.DeactivatePlugin(name)
|
||||
if err := a.plugins.Disable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Persist enabled state in config
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
@@ -143,16 +149,12 @@ func (a *App) SetPluginEnabled(name string, enabled bool) error {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
|
||||
if enabled {
|
||||
a.plugins.ActivatePlugin(name)
|
||||
} else {
|
||||
a.plugins.DeactivatePlugin(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPluginPanelHTML returns the HTML panel content for a plugin.
|
||||
// Validates that the panel path is safe: no absolute paths, no .. traversal,
|
||||
// must be within the plugin directory, and must end with .html.
|
||||
func (a *App) GetPluginPanelHTML(pluginName string) (string, error) {
|
||||
if a.plugins == nil {
|
||||
return "", fmt.Errorf("plugin manager not ready")
|
||||
@@ -164,10 +166,36 @@ func (a *App) GetPluginPanelHTML(pluginName string) (string, error) {
|
||||
if p.Meta.Panel == "" {
|
||||
return "", nil
|
||||
}
|
||||
panelPath := filepath.Join(p.Dir, p.Meta.Panel)
|
||||
data, err := os.ReadFile(panelPath)
|
||||
|
||||
// Validate panel path: must be relative, no .., within plugin dir, .html only
|
||||
panel := p.Meta.Panel
|
||||
if filepath.IsAbs(panel) {
|
||||
return "", fmt.Errorf("panel path %q must be relative", panel)
|
||||
}
|
||||
if strings.Contains(panel, "..") {
|
||||
return "", fmt.Errorf("panel path %q must not contain ..", panel)
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(panel), ".html") {
|
||||
return "", fmt.Errorf("panel path %q must end with .html", panel)
|
||||
}
|
||||
|
||||
// Resolve and verify the path is within the plugin directory
|
||||
panelPath := filepath.Join(p.Dir, panel)
|
||||
absPanel, err := filepath.Abs(panelPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read panel %s: %w", p.Meta.Panel, err)
|
||||
return "", fmt.Errorf("resolve panel path: %w", err)
|
||||
}
|
||||
absDir, err := filepath.Abs(p.Dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve plugin dir: %w", err)
|
||||
}
|
||||
if !strings.HasPrefix(absPanel, absDir+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("panel path %q escapes plugin directory", panel)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPanel)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read panel %s: %w", panel, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -241,52 +269,14 @@ func (a *App) CallPluginFunction(pluginName, funcName string, paramsJSON string)
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve the function via _G to avoid string-based code generation
|
||||
var fn lua.LValue
|
||||
if len(segments) == 1 {
|
||||
fn = vm.LState().GetGlobal(segments[0])
|
||||
} else {
|
||||
// Walk the dotted path: _G[seg1][seg2]...
|
||||
tbl := vm.LState().GetGlobal(segments[0])
|
||||
for i := 1; i < len(segments); i++ {
|
||||
if t, ok := tbl.(*lua.LTable); ok {
|
||||
tbl = t.RawGetString(segments[i])
|
||||
} else {
|
||||
tbl = lua.LNil
|
||||
break
|
||||
}
|
||||
}
|
||||
fn = tbl
|
||||
}
|
||||
|
||||
if fn == lua.LNil {
|
||||
return "", fmt.Errorf("function %q not found in plugin %q", funcName, pluginName)
|
||||
}
|
||||
if _, ok := fn.(*lua.LFunction); !ok {
|
||||
return "", fmt.Errorf("%q is not a function in plugin %q", funcName, pluginName)
|
||||
}
|
||||
|
||||
// Parse params into Lua value
|
||||
luaArg, err := parseParamsToLua(vm, paramsJSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse params: %w", err)
|
||||
}
|
||||
|
||||
// Call the function directly via PCall (no string-based code generation)
|
||||
vm.LState().Push(fn)
|
||||
if luaArg != nil {
|
||||
vm.LState().Push(luaArg)
|
||||
}
|
||||
nargs := 0
|
||||
if luaArg != nil {
|
||||
nargs = 1
|
||||
}
|
||||
if err := vm.LState().PCall(nargs, 1, nil); err != nil {
|
||||
return "", fmt.Errorf("call %s: %w", funcName, err)
|
||||
}
|
||||
ret := vm.LState().Get(-1)
|
||||
vm.LState().Pop(1)
|
||||
return ret.String(), nil
|
||||
// Call via thread-safe, timeout-safe LuaVM.CallFunction
|
||||
return vm.CallFunction(segments, luaArg)
|
||||
}
|
||||
return "", fmt.Errorf("plugin %q not active or not found", pluginName)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -19,7 +19,7 @@
|
||||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-9sLWbwBV.js"></script>
|
||||
<script type="module" crossorigin src="/assets/main-8qmy5tDO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-Cjkp2F09.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user