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 errPluginFileWatchUnsupported

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Check p.context.Err() / plugin state before calling addStorageWatch; skip the watch if already stopped
  2. Serialize storage-watch setup against stop() using watcherMu so setup cannot interleave with teardown
  3. Drop or cancel the pending watch request instead of retrying after the plugin is stopped
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/5bf979dc55d68da5. Report an issue: GitHub.