镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Treat an unreaped zombie instance as exited on Linux [patch] - #223

Open
matt-edmondson wants to merge 3 commits into
mainfrom
fix/zombie-instance-219
Open

matt-edmondson wants to merge 3 commits into
mainfrom
fix/zombie-instance-219

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #219

What was wrong

On Linux, an instance that has exited but whose parent hasn't reaped it yet (a launcher, wrapper or supervisor that doesn't wait()) keeps its /proc/<pid> entry in state Z. For that process:

  • HasExited is false
  • the name still reads back
  • the memory map is empty, so there is no main module

Since #215, a null main module falls back to the name check. The zombie passes that check, so every later launch reported "already running" until the parent went away. The legacy plain-PID path had the same blind spot.

Change

  • New HasExited(Process) returns process.HasExited || IsLinuxZombie(pid). It's used in IsStoredProcessRunning, IsStoredProcessRunningByName and IsLegacyProcessRunning.
  • IsLinuxZombie reads /proc/<pid>/stat and treats state Z or X as exited. It does nothing on other platforms. When the state can't be read, it doesn't change the result.
  • ParseLinuxProcessState takes the state field from after the last ), because the command name in field 2 can itself contain spaces and parentheses.
  • The On Linux, a second instance launches when the running one belongs to another user: MainModule is null there, not a Win32Exception, so the name-only fallback never runs #214 cross-user fallback is unchanged. A live process in another user's session is in state S/R, so it still falls back to the name and start-time check. The existing IsStoredProcessRunning_WhenMainModuleIsUnreadable_* tests still pass.

Test helpers and the CI runner image

The first CI run failed in an existing test, IsStoredProcessRunning_WhenMainModuleIsUnreadable_ShouldFallBackToNameAndStartTime, with "process has exited".

ubuntu-latest now ships uutils coreutils, a multi-call binary that picks the utility from its executable name. HelperProcess copied /bin/sleep under a test name, so the copy printed coreutils: unknown program 'StartTimeHelper' and exited at once. Every helper-based test then raced an exited process.

  • On Linux the helper is now a copy of /bin/sh that runs sleep 120 as a child. The copy keeps the requested name and main module. Windows and macOS are unchanged.
  • Disposing a helper kills its whole process tree, so no sleep outlives a test.

Tests

  • IsAlreadyRunning_WhenStoredProcessIsAnUnreapedZombie_ShouldReturnFalse and IsAlreadyRunning_WithLegacyPidFile_WhenProcessIsAnUnreapedZombie_ShouldReturnFalse create a real zombie. A shell backgrounds a copied sh -c 'exit 0' that exits at once, then execs into sleep, which never reaps it. Disposing kills that parent, so init reaps the zombie. Each test writes the zombie into a JSON or legacy PID file and asserts IsAlreadyRunning() is false. On other platforms they are inconclusive.
  • ParseLinuxProcessState data-row tests cover a command name like a) b (c) and input that can't be parsed.
  • With IsLinuxZombie taken out of HasExited, both zombie tests fail and the other 65 pass.
  • Full suite on Linux / .NET 10: 67 passed, 1 skipped. The skip was already there: the read-only-folder test is inconclusive when run as root. CI passes on Ubuntu, macOS and Windows.
  • This branch merges cleanly with the open PR Recognise a running instance after the wall clock is stepped on Linux [patch] #216, which edits the same process-identity checks.

🤖 Generated with Claude Code

https://claude.ai/code/session_017vfJW3W1DsZmnUsfmWXu2k

matt-edmondson and others added 3 commits November 12, 2026 18:25
An instance that has exited but whose parent has not reaped it keeps its
/proc entry: HasExited is false, its name still reads back, and its empty
memory map leaves no main module. Since #215 that null module falls back
to the name check, which the zombie passes, so every later launch saw
"already running". The legacy plain-PID path had the same blind spot.

Read the state field of /proc/<pid>/stat and count Z or X as exited in the
stored, name-fallback and legacy checks. The #214 cross-user fallback is
unchanged: a live process in another user's session is in state S or R.

Fixes #219

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW3W1DsZmnUsfmWXu2k
CI's ubuntu-latest image now ships uutils coreutils, a multi-call binary
that picks the utility from its executable name. HelperProcess copied
/bin/sleep under a test name, so the copy printed "coreutils: unknown
program 'StartTimeHelper'" and exited at once. Each helper test then raced
an exited process: one failed with "process has exited" and the rest could
pass without checking anything.

On Linux the helper is now a copy of /bin/sh that runs `sleep 120` as a
child. The copy keeps the requested name and main module and stays alive.
ZombieProcess's short-lived child is a copied sh running `exit 0` for the
same reason. Windows and macOS are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW3W1DsZmnUsfmWXu2k
The helper is now a shell waiting on a sleep child, and Kill() stopped only
the shell, leaving its sleep running for up to two minutes after each test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW3W1DsZmnUsfmWXu2k
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant