{"record":{"id":"22d503d8d54648d8","repo":"File-New-Project/EarTrumpet","slug":"process-is-a-zombie-processid-22d503","errorCode":null,"errorMessage":"Process is a zombie: {processId}","messagePattern":"Process is a zombie: (.+?)","errorType":"exception","errorClass":"ZombieProcessException","httpStatus":null,"severity":"error","filePath":"EarTrumpet/DataModel/AppInformation/Internal/ZombieProcessException.cs","lineNumber":14,"sourceCode":"﻿using EarTrumpet.Interop;\r\nusing System;\r\n\r\nnamespace EarTrumpet.DataModel.AppInformation.Internal\r\n{\r\n    public class ZombieProcessException : Exception\r\n    {\r\n        public ZombieProcessException(int processId) : base($\"Process is a zombie: {processId}\") { }\r\n\r\n        public static void ThrowIfZombie(int processId, IntPtr handle)\r\n        {\r\n            if (Kernel32.WaitForSingleObject(handle, 0) != Kernel32.WAIT_TIMEOUT)\r\n            {\r\n                throw new ZombieProcessException(processId);\r\n            }\r\n        }\r\n    }\r\n}\r\n","sourceCodeStart":1,"sourceCodeEnd":19,"githubUrl":"https://github.com/File-New-Project/EarTrumpet/blob/aa894e51c22f5f9a939b31b224c4d2d3e163416e/EarTrumpet/DataModel/AppInformation/Internal/ZombieProcessException.cs#L1-L19","documentation":"ZombieProcessException is both the exception type and the detector. ThrowIfZombie opens a process handle and calls Kernel32.WaitForSingleObject(handle, 0): a non-WAIT_TIMEOUT return means the handle is already signaled, i.e. the process has terminated. It then throws. Both DesktopAppInfo and ModernAppInfo call this right after a successful OpenProcess to catch the race where the process died between session creation and info construction.","triggerScenarios":"A process handle was opened successfully, but by the time ThrowIfZombie runs the process has exited, so WaitForSingleObject returns WAIT_OBJECT_0 (signaled) instead of WAIT_TIMEOUT, and the exception is thrown.","commonSituations":"Process exits during the window between OpenProcess and the first info query; teardown race when a device/app is closing; rapid create/destroy of audio processes (notifications, system sounds).","solutions":["Catch ZombieProcessException wherever you construct DesktopAppInfo / ModernAppInfo and treat the session as expired.","If you call ThrowIfZombie yourself, ensure the caller can tolerate the throw and clean up the handle (it is in a try/finally that closes the handle).","Do not widen the WaitForSingleObject timeout to mask the issue; instead discard the dead session.","Log the PID for diagnostics, then suppress the session from the UI."],"exampleFix":"// before\nusing (var h = Kernel32.OpenProcess(flags, false, pid))\n{\n    ZombieProcessException.ThrowIfZombie(pid, h);\n    ReadInfo(h);\n}\n\n// after\ntry { using (var h = Kernel32.OpenProcess(flags, false, pid)) { ZombieProcessException.ThrowIfZombie(pid, h); ReadInfo(h); } }\ncatch (ZombieProcessException) { /* process died mid-read */ }","handlingStrategy":"try-catch","validationCode":"// if you call ThrowIfZombie yourself, check first to avoid the throw\nbool IsSignaled(IntPtr handle) => Kernel32.WaitForSingleObject(handle, 0) != Kernel32.WAIT_TIMEOUT;","typeGuard":null,"tryCatchPattern":"try { ZombieProcessException.ThrowIfZombie(pid, handle); /* read info */ } catch (ZombieProcessException) { /* process already terminated; abort info read */ }","preventionTips":["Always pair OpenProcess success with a ThrowIfZombie-or-handle-signaled check.","Keep the handle close in a try/finally so it is not leaked on the throw.","Discard the session, do not retry — a signaled handle means the process is gone."],"tags":["process","win32","zombie","interop","race-condition","appinfo"],"backgroundTag":null,"analyzedSha":"aa894e51c22f5f9a939b31b224c4d2d3e163416e","analyzedAt":"2026-08-13T18:51:30.564Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}