fix: финальный cleanup Lua plugin lifecycle
1. ActivatePlugin → error return:
- Возвращает ошибки при создании VM, загрузке main.lua, scheduler setup
- on_init failure = non-fatal (logged, activation continues)
- SetPluginEnabled сохраняет EnabledPlugins в config ТОЛЬКО после успешной активации
- При ошибке активации — rollback (deactivate + не сохраняем в config)
2. CallPluginFunction fully thread-safe:
- Новый метод LuaVM.CallFunctionJSON(segments, paramsJSON)
- JSON→Lua conversion происходит под vm.mu (внутри lock)
- Убраны parseParamsToLua/goToLua из bindings_plugins.go
- goToLua перенесён в runtime.go (под lock)
3. PluginPage → CalendarPluginPage:
- Компонент явно календарный (get-events/create-event/update-event/delete-event)
- Переименован для ясности
- Console log префиксы обновлены
4. Тесты:
- TestSetPluginEnabled_ActivateFails_NoConfigSave: проверяет что при ошибке
активации плагин НЕ сохраняется в EnabledPlugins
- TestActivatePlugin_ErrorReturn: проверяет все режимы ошибок
- TestCallFunctionJSON_ThreadSafe: JSON object/array/empty params
- TestDeactivatePlugin_Idempotent: двойная деактивация = no-op
- TestInitRuntimes_SkipsDisabled: только Enabled плагины активируются
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -10,9 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"verstak/internal/core/config"
|
||||
"verstak/internal/core/plugins"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// PluginDTO represents a discovered plugin with its current state.
|
||||
@@ -105,51 +101,74 @@ func (a *App) ListPlugins() []PluginDTO {
|
||||
}
|
||||
|
||||
// SetPluginEnabled persists the enabled/disabled state and applies it to the runtime.
|
||||
// Enable: marks plugin as enabled, then activates runtime (VM + scheduler).
|
||||
// Disable: deactivates runtime, then marks plugin as disabled.
|
||||
// Enable: marks plugin as enabled, activates runtime, THEN persists to config (only on success).
|
||||
// Disable: deactivates runtime, then marks plugin as disabled in config.
|
||||
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
|
||||
// Enable first (sets Enabled=true on the plugin struct)
|
||||
if err := a.plugins.Enable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
a.plugins.ActivatePlugin(name)
|
||||
// Activate runtime — if this fails, do NOT persist to config
|
||||
if err := a.plugins.ActivatePlugin(name); err != nil {
|
||||
// Rollback: deactivate and un-enable
|
||||
a.plugins.DeactivatePlugin(name)
|
||||
return fmt.Errorf("activate %q: %w", name, err)
|
||||
}
|
||||
// Only persist to config after successful activation
|
||||
if err := a.saveEnabledPlugin(name); err != nil {
|
||||
// Config save failed — rollback runtime too
|
||||
a.plugins.DeactivatePlugin(name)
|
||||
return fmt.Errorf("save config for %q: %w", name, err)
|
||||
}
|
||||
} else {
|
||||
// Deactivate runtime first, then disable
|
||||
a.plugins.DeactivatePlugin(name)
|
||||
if err := a.plugins.Disable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove from config
|
||||
if err := a.removeEnabledPlugin(name); err != nil {
|
||||
return fmt.Errorf("save config for %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Persist enabled state in config
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveEnabledPlugin adds name to EnabledPlugins in config and saves.
|
||||
func (a *App) saveEnabledPlugin(name string) error {
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
}
|
||||
|
||||
existing := make(map[string]bool)
|
||||
for _, n := range appCfg.EnabledPlugins {
|
||||
existing[n] = true
|
||||
}
|
||||
if enabled {
|
||||
existing[name] = true
|
||||
} else {
|
||||
delete(existing, name)
|
||||
}
|
||||
appCfg.EnabledPlugins = make([]string, 0, len(existing))
|
||||
for n := range existing {
|
||||
appCfg.EnabledPlugins = append(appCfg.EnabledPlugins, n)
|
||||
}
|
||||
if err := config.SaveAppConfig(appCfg); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
if n == name {
|
||||
return nil // already present
|
||||
}
|
||||
}
|
||||
appCfg.EnabledPlugins = append(appCfg.EnabledPlugins, name)
|
||||
return config.SaveAppConfig(appCfg)
|
||||
}
|
||||
|
||||
return nil
|
||||
// removeEnabledPlugin removes name from EnabledPlugins in config and saves.
|
||||
func (a *App) removeEnabledPlugin(name string) error {
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
return nil // nothing to remove
|
||||
}
|
||||
var updated []string
|
||||
for _, n := range appCfg.EnabledPlugins {
|
||||
if n != name {
|
||||
updated = append(updated, n)
|
||||
}
|
||||
}
|
||||
appCfg.EnabledPlugins = updated
|
||||
return config.SaveAppConfig(appCfg)
|
||||
}
|
||||
|
||||
// GetPluginPanelHTML returns the HTML panel content for a plugin.
|
||||
@@ -269,59 +288,13 @@ func (a *App) CallPluginFunction(pluginName, funcName string, paramsJSON string)
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse params into Lua value
|
||||
luaArg, err := parseParamsToLua(vm, paramsJSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse params: %w", err)
|
||||
}
|
||||
|
||||
// Call via thread-safe, timeout-safe LuaVM.CallFunction
|
||||
return vm.CallFunction(segments, luaArg)
|
||||
// Call via fully thread-safe LuaVM.CallFunctionJSON
|
||||
// (JSON→Lua conversion happens under vm.mu)
|
||||
return vm.CallFunctionJSON(segments, paramsJSON)
|
||||
}
|
||||
return "", fmt.Errorf("plugin %q not active or not found", pluginName)
|
||||
}
|
||||
|
||||
// parseParamsToLua converts a JSON params string to a lua.LValue.
|
||||
// Empty or "{}" → nil (no argument).
|
||||
func parseParamsToLua(vm *plugins.LuaVM, paramsJSON string) (lua.LValue, error) {
|
||||
if paramsJSON == "" || paramsJSON == "{}" {
|
||||
return nil, nil
|
||||
}
|
||||
var params interface{}
|
||||
if err := json.Unmarshal([]byte(paramsJSON), ¶ms); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON params: %w", err)
|
||||
}
|
||||
return goToLua(vm.LState(), params), nil
|
||||
}
|
||||
|
||||
// goToLua converts a Go interface{} to a lua.LValue.
|
||||
func goToLua(L *lua.LState, v interface{}) lua.LValue {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
return lua.LNil
|
||||
case string:
|
||||
return lua.LString(val)
|
||||
case float64:
|
||||
return lua.LNumber(val)
|
||||
case bool:
|
||||
return lua.LBool(val)
|
||||
case map[string]interface{}:
|
||||
tbl := L.NewTable()
|
||||
for k, v := range val {
|
||||
tbl.RawSetString(k, goToLua(L, v))
|
||||
}
|
||||
return tbl
|
||||
case []interface{}:
|
||||
tbl := L.NewTable()
|
||||
for i, v := range val {
|
||||
tbl.RawSetInt(i+1, goToLua(L, v))
|
||||
}
|
||||
return tbl
|
||||
default:
|
||||
return lua.LString(fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadPlugins re-scans the plugins directory and re-initializes runtimes.
|
||||
func (a *App) ReloadPlugins() error {
|
||||
if a.plugins == nil {
|
||||
|
||||
+1
-1
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-8qmy5tDO.js"></script>
|
||||
<script type="module" crossorigin src="/assets/main-C2sdkP-s.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-Cjkp2F09.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user