{"record":{"id":"ce64ebae13df81b5","repo":"siyuan-note/siyuan","slug":"panic-during-plugin-stop-v","errorCode":null,"errorMessage":"panic during plugin stop: %v","messagePattern":"panic during plugin stop: (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/plugin/plugin.go","lineNumber":297,"sourceCode":"\t}\n\n\tp.onLoad()\n\tp.updateState(PluginStateRunning)\n\tp.onRunning()\n\n\tp.bus.Publish(EventBusTopicRuntime, createEventMessage(\"start\", nil))\n\n\tlogging.LogDebugf(\"[plugin:%s] started\", p.Name)\n\treturn\n}\n\n// stop cleanly shuts down the plugin: closes sockets, frees goja runtime.\nfunc (p *KernelPlugin) stop() (ok bool, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tp.error()\n\t\t\tok = false\n\t\t\terr = fmt.Errorf(\"panic during plugin stop: %v\", r)\n\t\t}\n\t}()\n\n\tif p.State() != PluginStateRunning {\n\t\tok = false\n\t\treturn\n\t}\n\n\tp.bus.Publish(EventBusTopicRuntime, createEventMessage(\"stop\", nil))\n\n\tp.updateState(PluginStateStopping)\n\n\tp.onUnload()\n\n\tp.Clear()\n\n\tp.cancel()\n\tp.closeStorageWatcher()","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/plugin/plugin.go#L279-L315","documentation":"The plugin's `stop()` recovered from a Go panic raised while shutting down (closing sockets, freeing the goja runtime). The deferred recover captures the panic value, marks the stop as not ok, and returns this error instead of crashing the process; the plugin's state is left inconsistent because cleanup did not finish.","triggerScenarios":"Calling stop() on a running KernelPlugin whose teardown path panics - e.g. nil map/pointer dereference in a socket close, goja runtime freed twice, or a concurrent access to the runtime during stop.","commonSituations":"Disabling a plugin whose cleanup goroutine races with stop; double-stop of the same plugin; a goja runtime already closed by an earlier failure; nil handler registered for onUnload.","solutions":["Read the %v panic value in the log to locate the panicking teardown code","Ensure stop() is only called once per plugin instance and only from PluginStateRunning","Guard concurrent cleanup with the watcher/context mutexes so sockets and the goja runtime are closed exactly once","Fix the nil-dereference or double-free in the plugin's unload/socket-close code, then restart the plugin"],"exampleFix":"// before\nfunc (p *KernelPlugin) stop() { p.sock.Close(); p.rt.Free() } // panics if already closed\n// after\nfunc (p *KernelPlugin) stop() {\n  if p.sock != nil { _ = p.sock.Close(); p.sock = nil }\n  if p.rt != nil { p.rt.Free(); p.rt = nil }\n}","handlingStrategy":"try-catch","validationCode":"if p.State() != PluginStateRunning {\n  return // nothing to stop\n}","typeGuard":null,"tryCatchPattern":"ok, err := p.stop()\nif err != nil {\n  logging.LogErrorf(\"plugin %s stop failed: %v\", p.Name, err)\n  // force-reset state so a later start is possible\n  p.updateState(PluginStateStopped)\n}","preventionTips":["Make stop() idempotent: guard socket/runtime teardown with nil checks and sync.Once","Never share the goja runtime across goroutines without a mutex","Track stopped state before closing sockets to avoid double-free","Recover in teardown paths so one bad cleanup cannot wedge the supervisor"],"tags":["plugin","panic","lifecycle","concurrency"],"backgroundTag":"recovered-panic","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"}