mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-06-30 07:46:02 +00:00
* feat(kernel-plugin): Prevent storage.put and storage.remove in read-only mode * feat(kernel-plugin): Reload petal plugin on install When installing a Bazaar package, ensure that if the corresponding petal exists and is enabled we call SetPetalEnabled to trigger a kernel plugin reload before scheduling the plugin reload. This ensures the petal's kernel plugin picks up the newly installed package immediately. * feat(kernel-plugin): Use gin-contrib/sse.Event for SSE handling Replace the custom sseEvent type with gin-contrib/sse.Event and update the SSE handling pipeline. The JS handler now accepts an event object (data, event, id, retry), which is validated and converted to sse.Event before being sent on the channel; the server renders events with c.Render(-1, e). Added import for github.com/gin-contrib/sse and bumped gin-contrib/sse to v1.1.1 in go.mod (go.sum updated accordingly). * feat(kernel-plugin): Add global kernel plugin start/stop hooks Introduce OnKernelPluginsStart and OnKernelPluginsStop hooks and replace the previous OnKernelPluginShutdown usage. Update Close to call OnKernelPluginsStop and wire the new hooks in the plugin manager (OnKernelPluginsStart -> GetManager().Start, OnKernelPluginsStop -> GetManager().Stop). Existing per-plugin OnKernelPluginStart/OnKernelPluginStop handlers remain unchanged. This adds global lifecycle hooks to start and stop all valid plugins via the manager. * feat(kernel-plugin): Add plugin manager state and bazaar handling Call kernel plugin start/stop callbacks when bazaar settings change and add lifecycle state to the PluginManager. setting.go now triggers model.OnKernelPluginsStop or model.OnKernelPluginsStart when Bazaar.PetalDisabled or Bazaar.Trust change. plugin/manager.go introduces a PluginManagerState enum, initializes the manager as stopped, adds a State() accessor, and changes lifecycleMu to an RWMutex. Start/Stop now check the current state to avoid redundant operations, skip starting if bazaar config disables plugins, and update the manager state after start/stop, with additional log messages. These changes improve safety around concurrent start/stop and ensure bazaar configuration is honored. * feat(kernel-plugin): Add plugin hot-reload watcher and per-plugin dirs Add fsnotify-based watcher and manager context to detect kernel.js changes and trigger plugin hot-reloads. Introduce pluginsDir, pluginDir and storageDir fields and use storageDir in storage API to prevent path traversal. Replace single plugin mutex map with per-plugin mutex map, serialize start/stop per plugin, and adjust StartPlugin/StopPlugin flow to stop first for hot-reload, honor Bazaar config flags, and propagate start context. Update KernelPlugin to accept a start context and set per-plugin directories. Add helper functions to add/remove plugin dirs from the watcher and simplify GetLoadedPlugin. (Note: go.sum changes are present but not described.) * feat(kernel-plugin): Add storage watcher and plugin reload improvements Introduce per-plugin fsnotify storage watcher and expose a JS API for it (siyuan.storage.watcher.add/remove). KernelPlugin now accepts a parent context, creates an fsnotify.Watcher, starts a storage watch goroutine that publishes standardized runtime events (createEventMessage) for fs changes, and provides addStorageWatch/removeStorageWatch helpers. Manager API renamed to addPluginSourceWatch/removePluginSourceWatch with safer watcher handling and clearer reload logging; StartPlugin now passes the manager context into NewKernelPlugin. Also refactor runtime start/stop event publishing to use createEventMessage and add minor logging/error handling for watcher creation and path watch operations. The JS watcher object is frozen and integrated into siyuan.storage.
432 lines
12 KiB
Go
432 lines
12 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 (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/samber/lo"
|
|
"github.com/siyuan-note/filelock"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
// injectStorage adds siyuan.storage.* methods for scoped file CRUD.
|
|
func injectStorage(p *KernelPlugin, rt *goja.Runtime, siyuan *goja.Object) (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("injectStorage: %v", r)
|
|
}
|
|
}()
|
|
|
|
resolvePath := func(relPath string) (abs string, err error) {
|
|
abs = filepath.Join(p.storageDir, filepath.Clean(relPath))
|
|
if !(abs == p.storageDir || strings.HasPrefix(abs, p.storageDir+string(filepath.Separator))) {
|
|
err = fmt.Errorf("siyuan.storage: path traversal not allowed")
|
|
}
|
|
return
|
|
}
|
|
|
|
watcher := rt.NewObject()
|
|
|
|
// siyuan.storage.watcher.add(path) -> Promise<void>
|
|
lo.Must0(watcher.Set("add", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
var path string
|
|
if len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {
|
|
path = call.Argument(0).String()
|
|
} else {
|
|
err = fmt.Errorf("path required")
|
|
return
|
|
}
|
|
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
|
|
addErr := p.addStorageWatch(abs)
|
|
if addErr != nil {
|
|
err = fmt.Errorf("failed to add storage path to watcher: %v", addErr)
|
|
return
|
|
}
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.add resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.add reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.add worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
// siyuan.storage.watcher.remove(path) -> void
|
|
lo.Must0(watcher.Set("remove", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
var path string
|
|
if len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {
|
|
path = call.Argument(0).String()
|
|
} else {
|
|
err = fmt.Errorf("path required")
|
|
return
|
|
}
|
|
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
|
|
addErr := p.removeStorageWatch(abs)
|
|
if addErr != nil {
|
|
err = fmt.Errorf("failed to remove storage path from watcher: %v", addErr)
|
|
return
|
|
}
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.watcher.remove worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
lo.Must0(ObjectFreeze(rt, watcher))
|
|
|
|
storage := rt.NewObject()
|
|
|
|
// siyuan.storage.get(path) -> Promise<{text, json, arrayBuffer}>
|
|
lo.Must0(storage.Set("get", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
var path string
|
|
if len(call.Arguments) >= 1 && goja.IsString(call.Argument(0)) {
|
|
path = call.Argument(0).String()
|
|
} else {
|
|
err = fmt.Errorf("path required")
|
|
return
|
|
}
|
|
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
|
|
go func() (result []byte, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic during siyuan.storage.get: %v", r)
|
|
}
|
|
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
content := rt.NewObject()
|
|
if err = ObjectSetDataMethods(p, rt, content, result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return content, nil
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.get resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.get reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
}()
|
|
|
|
data, readErr := filelock.ReadFile(abs)
|
|
if readErr != nil {
|
|
err = readErr
|
|
return
|
|
}
|
|
|
|
result = data
|
|
return
|
|
}()
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if !lo.IsNil(err) {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.get reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.get worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
// siyuan.storage.put(path, content) -> Promise<void>
|
|
lo.Must0(storage.Set("put", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
if util.ReadOnly {
|
|
err = fmt.Errorf("The current kernel is in read-only mode, storage.put is not allowed")
|
|
return
|
|
}
|
|
|
|
if len(call.Arguments) < 2 {
|
|
err = fmt.Errorf("path and content required")
|
|
return
|
|
}
|
|
path := call.Argument(0).String()
|
|
content := call.Argument(1).String()
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
|
|
go func() (result any, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic during siyuan.storage.put: %v", r)
|
|
}
|
|
|
|
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.put resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.put reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
return
|
|
}, nil)
|
|
}()
|
|
|
|
if mkdirErr := os.MkdirAll(filepath.Dir(abs), 0755); mkdirErr != nil {
|
|
err = fmt.Errorf("failed to make directory: %w", mkdirErr)
|
|
return
|
|
}
|
|
if writeErr := filelock.WriteFile(abs, []byte(content)); writeErr != nil {
|
|
err = fmt.Errorf("failed to write file: %w", writeErr)
|
|
return
|
|
}
|
|
return
|
|
}()
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if !lo.IsNil(err) {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.put reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.put worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
// siyuan.storage.remove(path) -> Promise<void>
|
|
lo.Must0(storage.Set("remove", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
if util.ReadOnly {
|
|
err = fmt.Errorf("The current kernel is in read-only mode, storage.remove is not allowed")
|
|
return
|
|
}
|
|
|
|
if len(call.Arguments) < 1 {
|
|
err = fmt.Errorf("path required")
|
|
return
|
|
}
|
|
path := call.Argument(0).String()
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
if abs == p.storageDir {
|
|
err = fmt.Errorf("cannot remove storage root")
|
|
return
|
|
}
|
|
|
|
go func() (result any, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic during siyuan.storage.remove: %v", r)
|
|
}
|
|
|
|
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.remove resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.remove reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
return
|
|
}, nil)
|
|
}()
|
|
|
|
if removeErr := os.RemoveAll(abs); removeErr != nil {
|
|
err = fmt.Errorf("failed to remove: %w", removeErr)
|
|
return
|
|
}
|
|
return
|
|
}()
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if !lo.IsNil(err) {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.remove reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.remove worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
// siyuan.storage.list(path) -> Promise<Entry[]>
|
|
lo.Must0(storage.Set("list", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
promise, resolve, reject := rt.NewPromise()
|
|
|
|
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
|
|
if len(call.Arguments) < 1 {
|
|
err = fmt.Errorf("path required")
|
|
return
|
|
}
|
|
path := call.Argument(0).String()
|
|
abs, resolveErr := resolvePath(path)
|
|
if resolveErr != nil {
|
|
err = resolveErr
|
|
return
|
|
}
|
|
|
|
go func() (result any, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic during siyuan.storage.list: %v", r)
|
|
}
|
|
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
|
|
if lo.IsNil(err) {
|
|
if resolveErr := resolve(result); resolveErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.list resolve: %v", p.Name, resolveErr)
|
|
}
|
|
} else {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.list reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
return
|
|
}, nil)
|
|
}()
|
|
|
|
entries, readErr := os.ReadDir(abs)
|
|
if readErr != nil {
|
|
err = fmt.Errorf("failed to read directory: %w", readErr)
|
|
return
|
|
}
|
|
|
|
results := make([]R, 0, len(entries))
|
|
for _, entry := range entries {
|
|
info, infoErr := entry.Info()
|
|
if infoErr != nil {
|
|
continue
|
|
}
|
|
results = append(results, R{
|
|
"name": entry.Name(),
|
|
"isDir": info.IsDir(),
|
|
"isSymlink": util.IsSymlink(entry),
|
|
"updated": info.ModTime().Unix(),
|
|
})
|
|
}
|
|
|
|
result = results
|
|
return
|
|
}()
|
|
|
|
return
|
|
}, func(rt *goja.Runtime, result any, err error) {
|
|
if !lo.IsNil(err) {
|
|
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.list reject: %v", p.Name, rejectErr)
|
|
}
|
|
}
|
|
})
|
|
if runErr != nil {
|
|
logging.LogErrorf("[plugin:%s] siyuan.storage.list worker run: %v", p.Name, runErr)
|
|
}
|
|
|
|
return rt.ToValue(promise)
|
|
})))
|
|
|
|
// siyuan.storage.watcher
|
|
lo.Must0(storage.Set("watcher", watcher))
|
|
|
|
lo.Must0(ObjectFreeze(rt, storage))
|
|
|
|
lo.Must0(siyuan.Set("storage", storage))
|
|
return
|
|
}
|