Skip to content

fix: close events left open by a capture daemon that died - #5201

Open
nabbi wants to merge 2 commits into
ZoneMinder:masterfrom
nabbi:fix/b53-close-orphaned-events
Open

nabbi wants to merge 2 commits into
ZoneMinder:masterfrom
nabbi:fix/b53-close-orphaned-events

Conversation

@nabbi

@nabbi nabbi commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Automated PR, tested in a zm-holodeck sandbox. An AI agent (Claude Opus 5.5) reproduced the bug on a disposable ZoneMinder built from upstream master 821030e17, wrote this fix and ran the checks below on the same lab. Minimal human review before opening: please review it as you would any contributor's PR.

TL;DR

Details: change, behaviour changes, checks

Before

Upstream master 821030e17: kill -9 a Recording=Always zmc mid-event. The event keeps EndDateTime NULL indefinitely:

1  2026-10-10 19:19:05  NULL  19.93  300  incomplete.h264.mp4  New Event

Change

  • scripts/zmwatch.pl.in: when a monitor's heartbeat is fresh and its shared-memory startup_time differs from the last one seen, close its events with EndDateTime IS NULL, StartDateTime < FROM_UNIXTIME(startup_time) and Cause other than Re-analysis, through Event::Close(). A running zmc closes its event before reconnecting and sets startup_time after connecting, so it never owns such an event. Only this server's monitors.
  • scripts/ZoneMinder/lib/ZoneMinder/Event.pm Close(): Length, Frames, AlarmFrames, TotScore and MaxScore = the larger of the event row and the Frames rows (zmc writes both in batches, so either can be ahead); EndDateTime follows the Length used; works with no Frames rows; refuses an event without a start. Name and Notes as before ((r) and Recovered., zmaudit's existing tags).
  • Test: tests/perl/test_event_close_recovered.pl (17 cases; 11 fail on master's Event.pm).

Behaviour changes

Change Who sees it Intended / collateral Alternatives looked into Chosen, why Tested by
A dead zmc's open events are closed seconds after the next zmc is up, named <prefix><Id>(r), note Recovered. event lists, montage review, filters, API clients; zmeventnotificationNg's isEventEnded() now sees them end intended zmaudit's pass (off by default, one-day age); zmc in C++ (another copy of the close logic); zmdc before fork (misses a hand-started zmc) zmwatch: already watches each zmc's shared memory and has Event::Close() probe, events
zmwatch Info line Closed event N on monitor M … log readers intended Debug level visible once per recovery probe
zmeventtool.pl close handles events with no Frames rows admins intended — — Perl test
One indexed SELECT per monitor per zmc start DB collateral, negligible — — events

After

Branch at 993b0d80d (base 821030e17). kill -9 of monitor 2's zmc at 00:01:38; event 60 closed 13 s later, the new zmc's event 61 untouched:

60 | 00:01:15 | 00:01:35 | 20.00 | 300 | incomplete.h264.mp4 | Event-60(r) | Recovered.
Check Result
holodeck checks for the diff unit 249 cases; script tests PASS=30; smoke PASS=11 XFAIL=1 (stock zmonvif-probe.pl, known); events PASS=10; 0 open events after the run

Not run: clients (no API change), multi-server, release-1.38 (backport prepared, not run).

Possible follow-ups

Offered for discussion, not part of this change:

  • Length from the video. The row's Length can trail the video by the last unflushed batch (20.00 s in the row vs 22.0 s by ffprobe here). Closing with the video's duration would be more accurate. Benefit: recovered events show their real length. Risk: an ffprobe run per recovered event.
  • Finish the HLS playlist. A recovered index.m3u8 has no #EXT-X-ENDLIST, so some players treat it as live. Rewriting it as VOD would fix that. Risk: file rewrites in event directories.
  • Fill DiskSpace and keep a user-set name in zmeventtool close. Risk: a naming change for admins who rely on the (r) suffix.
  • Gaps left: monitors with Capturing=None keep any open event (zmwatch skips them), and zmaudit's unclosed-event pass keeps its own copy of the close logic.

A branch with the first three exists and was lab-tested; happy to open it separately if wanted.

refs #5200

🤖 Generated with Claude Code

nabbi and others added 2 commits October 10, 2026 18:36
Event::Close() (zmeventtool.pl close) took the end of an event only from
its Frames rows and refused events that had none. zmc writes Frames rows
in batches and updates the event row's Length, Frames and scores with
each batch, so an event whose zmc was killed in its first seconds has no
Frames rows, and either side can be ahead of the other.

Take Length, Frames, AlarmFrames, TotScore and MaxScore as the larger of
the event row and the Frames rows; EndDateTime follows the Length that
was used. The name and Notes are written as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An event open when its zmc is killed (lost stop, OOM, crash) keeps
EndDateTime NULL. Nothing closes it on a default install: zmaudit only
does with ZM_RUN_AUDIT on and after ZM_AUDIT_MIN_AGE, and the next zmc
does not look at earlier events.

A running zmc closes its event before it reconnects and sets
startup_time in shared memory after it connects, so an open event of
its monitor that started before that time belongs to a process that is
gone. Once per zmc start, when its heartbeat is fresh, zmwatch closes
those events through Event::Close(). Only this server's monitors are
checked, and re-analysis events (Cause 'Re-analysis', created by zma
with their original times) are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@connortechnology connortechnology left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Close() always renames the event to <prefix><Id>(r), so it overwrites a name the user gave the event while it was recording. That was fine while only zmeventtool.pl close and zmaudit called it, by hand or a day later, but now zmwatch runs it on its own every time a zmc restarts.

zmc's own close only renames an event whose name is still the placeholder (~Event() in src/zm_event.cpp):

Name = IF(Name='New Event', '<prefix><Id>', Name)

Could Close() do the same here: add the (r) name only when Name is still New Event, and otherwise keep the user's name? The Recovered. note still marks the event either way. I'd fold this into this PR rather than leave it as a follow-up, since this is the change that makes the rename automatic.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants