siyuan-note/siyuan · warning
plugin stopped: %w
Error message
plugin stopped: %w
What it means
addStorageWatch checks the plugin's context before creating the fsnotify watcher; if the plugin has already been stopped (context canceled / deadline exceeded), it wraps the context error as `plugin stopped: %w` and refuses to set up a new watch. This prevents watch goroutines from leaking after shutdown.
Source
Thrown at kernel/plugin/plugin.go:775
if !ok {
return
}
logging.LogErrorf("[plugin:%s] storage watcher error: %s", p.Name, err)
}
}
}
// addStorageWatch adds a path to the fsnotify watcher to watch for storage file/directory changes.
func (p *KernelPlugin) addStorageWatch(path string) (err error) {
if !isPluginFileWatchSupported() {
return errPluginFileWatchUnsupported
}
p.watcherMu.Lock()
defer p.watcherMu.Unlock()
if contextErr := p.context.Err(); contextErr != nil {
return fmt.Errorf("plugin stopped: %w", contextErr)
}
if p.watcher == nil {
p.watcher, err = fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("initialize fsnotify watcher: %w", err)
}
p.watcherDone = make(chan struct{})
go p.startStorageWatch(p.watcher, p.watcherDone)
}
err = p.watcher.Add(path)
return
}
// removeStorageWatch removes a path from the fsnotify watcher to stop watching for storage file/directory changes.
func (p *KernelPlugin) removeStorageWatch(path string) (err error) {
if !isPluginFileWatchSupported() {
return errPluginFileWatchUnsupportedView on GitHub (pinned to 8641553a1f)
Solutions
- Check p.context.Err() / plugin state before calling addStorageWatch; skip the watch if already stopped
- Serialize storage-watch setup against stop() using watcherMu so setup cannot interleave with teardown
- Drop or cancel the pending watch request instead of retrying after the plugin is stopped
- If this is unexpected, verify the plugin is not being stopped by a supervisor (e.g. reload/disable) mid-operation
Example fix
// before
p.addStorageWatch(path) // errors if plugin already stopped
// after
if p.context.Err() == nil {
if err := p.addStorageWatch(path); err != nil {
logging.LogWarnf("skip storage watch: %v", err)
}
} Defensive patterns
Strategy: validation
Validate before calling
if err := p.context.Err(); err != nil {
return fmt.Errorf("skip storage watch, plugin stopped: %w", err)
} Try / catch
if err := p.addStorageWatch(path); err != nil {
if errors.Is(err, context.Canceled) || strings.HasPrefix(err.Error(), "plugin stopped") {
return nil // expected during shutdown, treat as no-op
}
return err
} Prevention
- Always check context.Err() before registering watchers in shutdown-adjacent code
- Serialize watcher setup/teardown behind watcherMu
- Treat 'plugin stopped' as an expected error during reload/disable flows
- Cancel pending watch requests when the plugin begins stopping
When it happens
Trigger: Calling addStorageWatch (directly or via storage-watch setup) after stop()/Close() has canceled p.context, or when the context deadline expired.
Common situations: A storage-write path firing concurrently with plugin shutdown; a late watcher.Add from a goroutine that outlived the plugin; stop() then a queued file event handler tries to re-register the watch.
Related errors
- subscribe plugin events: %v
- panic during plugin stop: %v
- fsnotify watcher not initialized
- panic during lifecycle hook invocation: %v
- globalThis.siyuan.plugin.lifecycle not found
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/5bf979dc55d68da5.
Report an issue: GitHub.