Skip to content

Fix endless motion restart loop when motion is not a direct child (ECHILD) - #3346

Open
muminkoykiran wants to merge 1 commit into
motioneye-project:devfrom
muminkoykiran:fix/running-echild-restart-loop
Open

Fix endless motion restart loop when motion is not a direct child (ECHILD)#3346
muminkoykiran wants to merge 1 commit into
motioneye-project:devfrom
muminkoykiran:fix/running-echild-restart-loop

Conversation

@muminkoykiran

Copy link
Copy Markdown

Problem

motionctl.running() reports a healthy motion process as not running whenever motion is not a direct child of the motionEye process. motionEye's watchdog then kills and restarts motion on every check interval, producing an endless restart loop that makes motionEye unusable (cameras never come up, logs spam motion 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

try:
    os.waitpid(pid, os.WNOHANG)
    os.kill(pid, 0)
    return True
except OSError as e:
    if e.errno not in (errno.ESRCH, errno.ECHILD):
        raise
return False

os.waitpid() and os.kill() share one try block that swallows both ESRCH and ECHILD. When motion is not a child of the calling process, os.waitpid() raises OSError(ECHILD) before os.kill() ever executes. ECHILD is in the ignore list, so the function falls through to return False — even though os.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) and ESRCH are ignored — reaping is only meaningful when motion is our child.
  • Liveness is decided solely by os.kill(pid, 0), where only ESRCH means 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

  • Verified on a Home Assistant add-on install (aarch64): before the patch, motion restarted 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.

…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.
Copilot AI review requested due to automatic review settings June 6, 2026 08:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ESRCH from kill(pid, 0) as “not running” (no longer treating ECHILD as “not running”).

Comment thread motioneye/motionctl.py
Comment on lines 202 to 210
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)
@MichaIng

MichaIng commented Jun 6, 2026

Copy link
Copy Markdown
Member

This will cause issues if e.g. the motion.service is erroneously enabled. motionEye starts its own motion processes directly, passing specific command arguments, hence only those direct child processes will work.

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, /usr/local/bin/motion #/bin/sh => s6 => /usr/bin/motion or something like that? If so, for which reason?

EDIT:

a process supervisor sits between motionEye and motion

Basically this should never be possible without manipulating motionEye Python code or creating some weird motion executable wrapper. motionEye starts motion internally, directly, and is not supposed to deal with some separately started motion process is does not (and usually cannot) control.

A supervisor can start motionEye, but then it must not start motion. Or if so, that motion instance must not conflict with motionEye, i.e. must not use the same ports and/or video device nodes. If there are separate motion instances, motionEye correctly ignores them in its running() check, which is why ECHILD is ignored/treated as False.

So far, my impression is that there is a conflicting separate motion instance started, so that motionEye cannot start its own instance(s) internally. So the restart loop is basically expected. Disable the dedicated motion service, wherever it is coming from. If that is intended part of the HA addon, I'd consider this a bug in that addon. But so far, no one else has reported it, so it is probably a misconfiguration on your end instead, or another conflicting HA addon/integration.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants