Fix endless motion restart loop when motion is not a direct child (ECHILD) - #3346
Fix endless motion restart loop when motion is not a direct child (ECHILD)#3346muminkoykiran wants to merge 1 commit into
Conversation
…HILD) running() called os.waitpid(pid, WNOHANG) and os.kill(pid, 0) inside a single try block that swallowed both ESRCH and ECHILD. When the motion process is not a direct child of the motionEye process -- e.g. when it is launched by a process supervisor such as s6 in the Home Assistant add-on -- os.waitpid() raises OSError(ECHILD) *before* os.kill() ever runs. The ECHILD was caught and the function returned False, reporting a perfectly healthy motion process as 'not running'. The watchdog then killed and restarted motion every check interval, leaving motionEye unusable. Separate the best-effort zombie reap from the liveness check: ECHILD from waitpid() is ignored (it only means motion is not our child), and the running state is decided solely by os.kill(pid, 0), where only ESRCH means the process is gone.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adjusts motion process liveness detection to avoid treating zombies / non-child processes as “not running”, preventing restart loops under supervisors.
Changes:
- Adds a best-effort
waitpid(..., WNOHANG)reap step before the liveness check. - Treats only
ESRCHfromkill(pid, 0)as “not running” (no longer treatingECHILDas “not running”).
| try: | ||
| os.waitpid(pid, os.WNOHANG) | ||
| except OSError as e: | ||
| if e.errno not in (errno.ESRCH, errno.ECHILD): | ||
| raise | ||
|
|
||
| # The actual liveness check. Only ESRCH means the process is gone. | ||
| try: | ||
| os.kill(pid, 0) |
|
This will cause issues if e.g. the Can you go into details about how the HA addon starts motion processes which are no direct children of motionEye? s6 is a service manager like systemd, hence I do not see how motionEye could start (and stop) motion via s6, passing required arguments etc, without significant code changes? Or does it use some sort of wrapper script, EDIT:
Basically this should never be possible without manipulating motionEye Python code or creating some weird A supervisor can start motionEye, but then it must not start So far, my impression is that there is a conflicting separate |
Problem
motionctl.running()reports a healthymotionprocess as not running whenevermotionis not a direct child of the motionEye process. motionEye's watchdog then kills and restartsmotionon every check interval, producing an endless restart loop that makes motionEye unusable (cameras never come up, logs spammotion not running, starting it).This happens in any setup where a process supervisor sits between motionEye and
motion, most notably the official Home Assistant add-on (s6-overlay), and more generally any container/supervisor launch context.Root cause
os.waitpid()andos.kill()share onetryblock that swallows bothESRCHandECHILD. Whenmotionis not a child of the calling process,os.waitpid()raisesOSError(ECHILD)beforeos.kill()ever executes.ECHILDis in the ignore list, so the function falls through toreturn False— even thoughos.kill(pid, 0)would have confirmed the process is alive.Fix
Separate the best-effort zombie reap from the liveness check:
os.waitpid()is now a standalone best-effort call.ECHILD(motion is not our child) andESRCHare ignored — reaping is only meaningful when motion is our child.os.kill(pid, 0), where onlyESRCHmeans the process is gone.This keeps zombie-reaping behaviour intact when motion is a direct child, while no longer misreporting it as dead when it is supervised by something else.
Testing
motionrestarted every check interval; after, it stays up and cameras work.os.kill(pid, 0)remains the authoritative liveness probe in both the child and non-child cases.