{"record":{"id":"700a5446d18d2320","repo":"gastownhall/beads","slug":"procid-close-process-handle-w","errorCode":null,"errorMessage":"procid: close process handle: %w","messagePattern":"procid: close process handle: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/procid/procid_windows.go","lineNumber":101,"sourceCode":"\tif err := windows.TerminateProcess(h.process, 1); err != nil {\n\t\tif _, exitedErr := tokenForProcess(h.process); errors.Is(exitedErr, errProcessExited) {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"procid: terminate process: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (h *Handle) Kill() error { return h.Signal(os.Kill) }\n\nfunc (h *Handle) Close() error {\n\tif h.process == 0 {\n\t\treturn nil\n\t}\n\terr := windows.CloseHandle(h.process)\n\th.process = 0\n\tif err != nil {\n\t\treturn fmt.Errorf(\"procid: close process handle: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (h *Handle) verify() error {\n\tcurrent, err := tokenForProcess(h.process)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif current != h.token {\n\t\treturn fmt.Errorf(\"procid: process no longer matches token\")\n\t}\n\treturn nil\n}\n\nfunc openProcess(pid int, access uint32) (windows.Handle, error) {\n\treturn windows.OpenProcess(access, false, uint32(pid))\n}","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/procid/procid_windows.go#L83-L119","documentation":"Handle.Close calls windows.CloseHandle on the stored process handle and wraps any failure. This almost always means the handle value is invalid — typically a double-Close or use-after-Close, since Close sets h.process = 0 afterward, which normally makes repeat Close a no-op.","triggerScenarios":"Calling Close twice concurrently (race: both see h.process != 0); closing a Handle whose underlying handle was invalidated externally; passing a zero-value Handle built outside Open.","commonSituations":"Concurrent shutdown paths both calling Close/deferred Close; copying the Handle struct by value so two copies own the same raw handle.","solutions":["Guard Close with sync.Once or a mutex so it runs exactly once","Never copy the *Handle by value; always pass *procid.Handle by pointer","Ignore or downgrade INVALID_HANDLE_VALUE-class errors on idempotent shutdown paths","Always obtain Handle via procid.Open; don't construct one manually"],"exampleFix":"// before\nvar closeOnce sync.Once\ndefer h.Close()\ndefer h.Close() // second call races\n// after\nvar closeOnce sync.Once\ncloseFn := func() { _ = h.Close() }\ndefer closeOnce.Do(closeFn)\ndefer closeOnce.Do(closeFn)","handlingStrategy":"try-catch","validationCode":"// Guard against double close at call sites\nvar closed bool\nfunc shutdown(h *procid.Handle) error {\n    if closed { return nil }\n    closed = true\n    return h.Close()\n}","typeGuard":"func safeClose(h *procid.Handle) error {\n    if h == nil {\n        return nil\n    }\n    return h.Close() // Close itself is idempotent when h.process == 0\n}","tryCatchPattern":"if err := h.Close(); err != nil {\n    // invalid handle on close is rarely actionable; log and continue\n    log.Printf(\"close process handle: %v\", err)\n}","preventionTips":["Close each Handle exactly once via defer at the Open site","Use sync.Once for shutdown paths with multiple triggers","Never copy Handle structs by value","Treat Close errors as warnings, not fatal, since the goal is resource release"],"tags":["windows","handle","resource-leak","concurrency"],"backgroundTag":"invalid-handle-close","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}