diff --git a/ops/devlane/dispatch/tests/launch_support.py b/ops/devlane/dispatch/tests/launch_support.py index 14a2df2..8042bdb 100644 --- a/ops/devlane/dispatch/tests/launch_support.py +++ b/ops/devlane/dispatch/tests/launch_support.py @@ -663,6 +663,18 @@ def sha256_file(path) -> str: def pid_is_alive(pid: int) -> bool: + # A group SIGKILL can leave a descendant as a zombie until the host's + # init process reaps it. ``kill(pid, 0)`` still succeeds for zombies, + # even though they cannot execute and therefore are not survivors of the + # isolation boundary. Check procfs first so the process-group assertion + # measures live workers rather than the reaping behaviour of PID 1 (which + # is notably delayed in some CI containers). + stat = Path(f"/proc/{pid}/stat") + try: + if stat.read_text(encoding="utf-8").split()[2] == "Z": + return False + except (FileNotFoundError, IndexError, OSError): + pass try: os.kill(pid, 0) except ProcessLookupError: diff --git a/ops/devlane/task/tests/test_run.py b/ops/devlane/task/tests/test_run.py index f3f48d6..c2925be 100644 --- a/ops/devlane/task/tests/test_run.py +++ b/ops/devlane/task/tests/test_run.py @@ -1307,6 +1307,14 @@ def _orphan_maker(self): @staticmethod def _alive(pid): + # kill(0) also reports zombies as present. A killed orphan can stay + # zombied until PID 1 reaps it in a container, but it is no longer a + # runnable descendant and must not make this isolation check flaky. + try: + if Path(f"/proc/{pid}/stat").read_text().split()[2] == "Z": + return False + except (FileNotFoundError, IndexError, OSError): + pass try: os.kill(pid, 0) return True