mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-06-28 14:56:12 +00:00
* 🎨 Refactor plugin API routes for improved organization and clarity * 🎨 Improve plugin hot-reload handling and streamline stop logic in PluginManager
364 lines
9.8 KiB
Go
364 lines
9.8 KiB
Go
// SiYuan - Refactor your thinking
|
|
// Copyright (c) 2020-present, b3log.org
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/model"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
type PluginManagerState int64
|
|
|
|
const (
|
|
PluginManagerStateStopped PluginManagerState = iota
|
|
PluginManagerStateRunning
|
|
)
|
|
|
|
// PluginManager discovers, loads, starts, and stops kernel plugins.
|
|
type PluginManager struct {
|
|
state PluginManagerState // protected by lifecycleMu
|
|
lifecycleMu sync.RWMutex // protects the lifecycle state of the manager (starting/stopping), allowing concurrent start/stop of different plugins while preventing concurrent start/stop of the entire manager
|
|
|
|
pluginsDir string // base directory for plugins (e.g. /path/to/workspace/data/plugins)
|
|
|
|
context context.Context // Context for managing plugin manager lifecycle
|
|
watcher *fsnotify.Watcher // watcher for kernel plugin source file changes, to trigger hot reload
|
|
|
|
plugins sync.Map // map[string]*KernelPlugin
|
|
pluginsMu sync.Map // map[string]*sync.Mutex, one per plugin name, to serialize start/stop of the same plugin while allowing concurrent start/stop of different plugins
|
|
|
|
}
|
|
|
|
type PluginInfo struct {
|
|
Name string `json:"name"`
|
|
State string `json:"state"`
|
|
StateCode int `json:"stateCode"`
|
|
Methods []*RpcMethodInfo `json:"methods"`
|
|
}
|
|
|
|
var (
|
|
manager *PluginManager
|
|
managerOnce sync.Once
|
|
)
|
|
|
|
// InitManager initializes the global PluginManager singleton and starts it.
|
|
func InitManager() {
|
|
m := GetManager()
|
|
|
|
model.OnKernelPluginStart = func(petal *model.Petal) { GetManager().StartPlugin(petal) }
|
|
model.OnKernelPluginStop = func(petal *model.Petal) { GetManager().StopPlugin(petal) }
|
|
|
|
model.OnKernelPluginsStart = func() { GetManager().Start() }
|
|
model.OnKernelPluginsStop = func() { GetManager().Stop() }
|
|
|
|
m.Start()
|
|
}
|
|
|
|
// GetManager returns the singleton PluginManager.
|
|
func GetManager() *PluginManager {
|
|
managerOnce.Do(func() {
|
|
context := context.Background()
|
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
logging.LogErrorf("failed to create kernel plugin source file watcher: %s", err)
|
|
} else if watcher != nil {
|
|
go func() {
|
|
defer watcher.Close()
|
|
|
|
for {
|
|
select {
|
|
case <-context.Done():
|
|
return
|
|
case event, ok := <-watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
pluginDir, fileName := filepath.Split(event.Name)
|
|
pluginName := filepath.Base(pluginDir)
|
|
if fileName == "kernel.js" {
|
|
switch event.Op {
|
|
case fsnotify.Create, fsnotify.Write:
|
|
petal := model.GetPetalByName(pluginName)
|
|
if petal != nil && petal.Enabled {
|
|
logging.LogInfof("[plugin:%s] source file kernel.js changed, reloading plugin", petal.Name)
|
|
go model.SetPetalEnabled(petal.Name, petal.Enabled)
|
|
}
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
logging.LogErrorf("kernel plugin source file watcher error: %s", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
manager = &PluginManager{
|
|
state: PluginManagerStateStopped,
|
|
|
|
pluginsDir: filepath.Join(util.DataDir, "plugins"),
|
|
|
|
context: context,
|
|
watcher: watcher,
|
|
}
|
|
})
|
|
return manager
|
|
}
|
|
|
|
func (m *PluginManager) State() PluginManagerState {
|
|
m.lifecycleMu.RLock()
|
|
defer m.lifecycleMu.RUnlock()
|
|
return m.state
|
|
}
|
|
|
|
// Start loads and starts all kernel-eligible plugins.
|
|
// Called from main.go after model initialization.
|
|
func (m *PluginManager) Start() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logging.LogErrorf("kernel plugin manager failed to start: %v", r)
|
|
}
|
|
}()
|
|
|
|
if m.State() == PluginManagerStateRunning {
|
|
logging.LogInfof("kernel plugin manager is already running, skipping start")
|
|
return
|
|
}
|
|
|
|
if model.Conf.Bazaar.PetalDisabled || !model.Conf.Bazaar.Trust {
|
|
logging.LogInfof("kernel plugins are disabled by configuration, skipping start")
|
|
return
|
|
}
|
|
|
|
m.lifecycleMu.Lock()
|
|
defer m.lifecycleMu.Unlock()
|
|
|
|
logging.LogInfof("kernel plugin manager starting")
|
|
|
|
petals := model.LoadKernelPetals()
|
|
|
|
all := len(petals)
|
|
counter := int64(0)
|
|
|
|
wg := sync.WaitGroup{}
|
|
for _, petal := range petals {
|
|
wg.Go(func() {
|
|
if ok := m.StartPlugin(petal); ok {
|
|
atomic.AddInt64(&counter, 1)
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
m.state = PluginManagerStateRunning
|
|
logging.LogInfof("kernel plugin manager started, %d/%d plugin(s) loaded", counter, all)
|
|
}
|
|
|
|
// Stop shuts down all running kernel plugins.
|
|
// Called from model.Close() before process exit.
|
|
func (m *PluginManager) Stop() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logging.LogErrorf("kernel plugin manager failed to stop: %v", r)
|
|
}
|
|
}()
|
|
|
|
if m.State() == PluginManagerStateStopped {
|
|
logging.LogInfof("kernel plugin manager is already stopped, skipping stop")
|
|
return
|
|
}
|
|
|
|
m.lifecycleMu.Lock()
|
|
defer m.lifecycleMu.Unlock()
|
|
|
|
logging.LogInfof("kernel plugin manager stopping")
|
|
|
|
all := int64(0)
|
|
counter := int64(0)
|
|
|
|
wg := sync.WaitGroup{}
|
|
m.plugins.Range(func(key, value any) bool {
|
|
atomic.AddInt64(&all, 1)
|
|
if p, ok := value.(*KernelPlugin); ok {
|
|
wg.Go(func() {
|
|
if ok := m.StopPlugin(p.Petal); ok {
|
|
atomic.AddInt64(&counter, 1)
|
|
}
|
|
})
|
|
}
|
|
return true
|
|
})
|
|
wg.Wait()
|
|
|
|
m.state = PluginManagerStateStopped
|
|
logging.LogInfof("kernel plugin manager stopped, %d/%d plugin(s) unloaded", counter, all)
|
|
}
|
|
|
|
// StartPlugin starts a single kernel plugin.
|
|
// Called when a petal is enabled via SetPetalEnabled.
|
|
func (m *PluginManager) StartPlugin(petal *model.Petal) (ok bool) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logging.LogErrorf("[plugin:%s] panic during start: %v", petal.Name, r)
|
|
ok = false
|
|
}
|
|
}()
|
|
|
|
if model.Conf.Bazaar.PetalDisabled || !model.Conf.Bazaar.Trust {
|
|
ok = false
|
|
return
|
|
}
|
|
|
|
if petal.Kernel.Incompatible || !petal.Kernel.Existed {
|
|
ok = false
|
|
return
|
|
}
|
|
|
|
pluginMu := m.getPluginMu(petal.Name)
|
|
pluginMu.Lock()
|
|
defer pluginMu.Unlock()
|
|
|
|
// Stop any running instance inside the same lock so that concurrent hot-reload goroutines queue here and each one sees the instance started by the previous.
|
|
m.stopLocked(petal.Name)
|
|
|
|
m.addPluginSourceWatch(petal.Name)
|
|
|
|
p := NewKernelPlugin(m.context, petal)
|
|
|
|
m.plugins.Store(p.Name, p)
|
|
|
|
if err := p.start(); err != nil {
|
|
logging.LogErrorf("[plugin:%s] start failed: %s", p.Name, err)
|
|
ok = false
|
|
return
|
|
}
|
|
|
|
ok = true
|
|
return
|
|
}
|
|
|
|
// StopPlugin stops a single kernel plugin.
|
|
// Called when a petal is disabled via SetPetalEnabled.
|
|
func (m *PluginManager) StopPlugin(petal *model.Petal) (ok bool) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logging.LogErrorf("[plugin:%s] panic during stop: %v", petal.Name, r)
|
|
ok = false
|
|
}
|
|
}()
|
|
|
|
pluginMu := m.getPluginMu(petal.Name)
|
|
pluginMu.Lock()
|
|
defer pluginMu.Unlock()
|
|
|
|
return m.stopLocked(petal.Name)
|
|
}
|
|
|
|
// stopLocked stops a running plugin by name. The caller must hold the per-plugin mutex.
|
|
func (m *PluginManager) stopLocked(name string) (ok bool) {
|
|
m.removePluginSourceWatch(name)
|
|
|
|
value, loaded := m.plugins.LoadAndDelete(name)
|
|
if !loaded {
|
|
return false
|
|
}
|
|
|
|
p := value.(*KernelPlugin)
|
|
success, err := p.stop()
|
|
if err != nil {
|
|
logging.LogErrorf("[plugin:%s] stop failed: %s", name, err)
|
|
return false
|
|
}
|
|
return success
|
|
}
|
|
|
|
// getPluginMu returns the per-plugin mutex for the given name, creating it if needed.
|
|
func (m *PluginManager) getPluginMu(name string) *sync.Mutex {
|
|
v, _ := m.pluginsMu.LoadOrStore(name, &sync.Mutex{})
|
|
return v.(*sync.Mutex)
|
|
}
|
|
|
|
// GetPlugin returns a loaded KernelPlugin by name, or nil.
|
|
func (m *PluginManager) GetPlugin(name string) *KernelPlugin {
|
|
value, loaded := m.plugins.Load(name)
|
|
if loaded {
|
|
return value.(*KernelPlugin)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetLoadedPlugin returns the plugin info for a loaded KernelPlugin by name, or nil.
|
|
func (m *PluginManager) GetLoadedPlugin(name string) (plugin *PluginInfo, found bool) {
|
|
p := m.GetPlugin(name)
|
|
if p != nil {
|
|
return &PluginInfo{
|
|
Name: p.Name,
|
|
State: p.State().String(),
|
|
StateCode: int(p.State()),
|
|
Methods: p.GetRpcMethodsInfo(),
|
|
}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// GetLoadedPluginInfo returns a list of all loaded plugins with their RPC method info.
|
|
func (m *PluginManager) GetLoadedPluginsInfo() (plugins []*PluginInfo) {
|
|
m.plugins.Range(func(key, value any) bool {
|
|
p := value.(*KernelPlugin)
|
|
plugins = append(plugins, &PluginInfo{
|
|
Name: p.Name,
|
|
State: p.State().String(),
|
|
StateCode: int(p.State()),
|
|
Methods: p.GetRpcMethodsInfo(),
|
|
})
|
|
return true
|
|
})
|
|
return plugins
|
|
}
|
|
|
|
// addPluginSourceWatch adds the plugin's base directory to the fsnotify watcher to watch for source file changes for hot reload.
|
|
func (m *PluginManager) addPluginSourceWatch(name string) {
|
|
if m.watcher == nil {
|
|
return
|
|
}
|
|
path := filepath.Join(m.pluginsDir, name)
|
|
if err := m.watcher.Add(path); err != nil {
|
|
logging.LogErrorf("failed to add kernel plugin source path [%s] to watcher: %s", path, err)
|
|
}
|
|
}
|
|
|
|
// removePluginSourceWatch removes the plugin's base directory from the fsnotify watcher when the plugin is stopped.
|
|
func (m *PluginManager) removePluginSourceWatch(name string) (err error) {
|
|
if m.watcher == nil {
|
|
return
|
|
}
|
|
path := filepath.Join(m.pluginsDir, name)
|
|
err = m.watcher.Remove(path)
|
|
return
|
|
}
|