Killing a process tree the launcher wouldn't forward signals to
Stopping the vehicle's software stack cleanly looked solved twice, on real hardware, and was wrong both times. A real test — not another hardware run — is what finally found the actual bug.
Killing ros2 launch orphaned every node it had started. Those orphaned nodes kept holding the Hailo-8, which surfaced an hour later — often in a completely different tool — as HAILO_OUT_OF_PHYSICAL_DEVICES, with no obvious connection back to the stop that had supposedly already happened.
Two fixes, both measured, both wrong
The first attempt reset the inherited SIGINT-ignore state and signalled the launcher directly. It didn't exit within a 20-second window, so the escalation path sent SIGTERM — which ros2 launch does not forward to its children. They orphaned anyway. The second attempt tried setsid plus a process-group signal; os.setsid() failed in that execution context, the wrapper died immediately, and the stored PID pointed at a process that had never existed. The stop function returned success, having signalled nothing — silently. It printed neither "stopped" nor a failure. It just lied by omission.
Both attempts shared the same broken assumption: that some other process in the chain would forward the signal onward. The actual fix drops that assumption entirely — kill_tree walks every descendant process and signals each one directly (INT, then TERM, then KILL as escalation), because ros2 launch starts each node as its own child, so the live process tree is the accurate node list, and nothing needs to relay anything.
Verified on hardware and correct is not the same claim as correct.
A real test — not another hardware run — found a fourth defect the hardware genuinely could not have exposed: kill_tree re-walked the process list at every escalation step, which looks equivalent to walking once and isn't. The instant a parent process exits, its children are reparented to init, so a second walk can never find them again — any child that ignored SIGINT simply outlived its parent and was never signalled a second time. On real hardware this stayed invisible because ROS 2 nodes happen to die on the first SIGINT, so the underlying defect had zero opportunities to matter — until a test deliberately spawned children that ignore SIGINT on purpose, which is exactly the condition the tool exists for. The fix snapshots the whole tree before anything is asked to die.