{"record":{"id":"5bf979dc55d68da5","repo":"siyuan-note/siyuan","slug":"plugin-stopped-w","errorCode":null,"errorMessage":"plugin stopped: %w","messagePattern":"plugin stopped: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/plugin/plugin.go","lineNumber":775,"sourceCode":"\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlogging.LogErrorf(\"[plugin:%s] storage watcher error: %s\", p.Name, err)\n\t\t}\n\t}\n}\n\n// addStorageWatch adds a path to the fsnotify watcher to watch for storage file/directory changes.\nfunc (p *KernelPlugin) addStorageWatch(path string) (err error) {\n\tif !isPluginFileWatchSupported() {\n\t\treturn errPluginFileWatchUnsupported\n\t}\n\n\tp.watcherMu.Lock()\n\tdefer p.watcherMu.Unlock()\n\n\tif contextErr := p.context.Err(); contextErr != nil {\n\t\treturn fmt.Errorf(\"plugin stopped: %w\", contextErr)\n\t}\n\tif p.watcher == nil {\n\t\tp.watcher, err = fsnotify.NewWatcher()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"initialize fsnotify watcher: %w\", err)\n\t\t}\n\t\tp.watcherDone = make(chan struct{})\n\t\tgo p.startStorageWatch(p.watcher, p.watcherDone)\n\t}\n\n\terr = p.watcher.Add(path)\n\treturn\n}\n\n// removeStorageWatch removes a path from the fsnotify watcher to stop watching for storage file/directory changes.\nfunc (p *KernelPlugin) removeStorageWatch(path string) (err error) {\n\tif !isPluginFileWatchSupported() {\n\t\treturn errPluginFileWatchUnsupported","sourceCodeStart":757,"sourceCodeEnd":793,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/plugin/plugin.go#L757-L793","documentation":"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.","triggerScenarios":"Calling addStorageWatch (directly or via storage-watch setup) after stop()/Close() has canceled p.context, or when the context deadline expired.","commonSituations":"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.","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"],"exampleFix":"// before\np.addStorageWatch(path) // errors if plugin already stopped\n// after\nif p.context.Err() == nil {\n  if err := p.addStorageWatch(path); err != nil {\n    logging.LogWarnf(\"skip storage watch: %v\", err)\n  }\n}","handlingStrategy":"validation","validationCode":"if err := p.context.Err(); err != nil {\n  return fmt.Errorf(\"skip storage watch, plugin stopped: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err := p.addStorageWatch(path); err != nil {\n  if errors.Is(err, context.Canceled) || strings.HasPrefix(err.Error(), \"plugin stopped\") {\n    return nil // expected during shutdown, treat as no-op\n  }\n  return err\n}","preventionTips":["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"],"tags":["plugin","context-canceled","fsnotify","lifecycle"],"backgroundTag":"context-canceled","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}