Child exit, result finalization, notice publication and output consumption are
separate steps, but several callers treated an earlier one as the last:
- A duplicate _move_to_finished set _completion_event while the owning call was
still before its notice, so a one-shot parent could exit without the follow-up.
Only the call that moved the session out of _running releases waiters now, and
it always does, even when its publish raises.
- _finish_exited ran mark_exited without the session lock, so a kill committed
between its `killed` check and its writes was overwritten with exited/0.
- After a reconcile flips `exited` ahead of a reader still on its final drain,
wait/read_log/kill no longer consume the partial output as the completion,
and poll no longer marks it observed. poll/read_log/kill decide that under
the same lock hold as their snapshot, so a reader finishing in between cannot
make an earlier partial snapshot count as the whole output.
- kill_process ignored a failed systemd scope stop. It now returns an error
with scope_stop_failed whenever the stop fails, including when the root
exited meanwhile (that proves nothing about detached descendants); the
session keeps its unit so a later kill retries the stop. An already-exited
session reports scope_stop_failed too.
Process-tree termination and post-kill survivor checks move into
tools/process_registry_termination.py (ProcessTerminationMixin) to keep
process_registry.py under its size cap. The only change in the move:
_live_descendants logs the traceback of the failure it swallows.
Refs #132358. The races were found by @andrexibiza and @jonpol01 in that thread.