Commit Graph
12 Commits
Author SHA1 Message Date
Isaac ConnorandClaude Opus 5 ddb5c93211 fix: drive the database with DBD::MariaDB when it is installed fixes #5008
DBD::mysql 5 refuses to connect to a MariaDB server and upstream does not
consider that a bug, so a MariaDB system needs DBD::MariaDB. Prefer it when
present; it drives both servers. ZM_DB_TYPE cannot select it, which is why
setting it to MariaDB does not work: the same value builds the PHP PDO dsn,
where the only valid driver is mysql whichever server is in use.

Swapping the dsn scheme is not enough on its own. DBD::MariaDB spells
DBD::mysql's mysql_* parameters mariadb_* and ignores parameters it does not
recognise, so a dsn built with the wrong prefix does not fail: it drops the
socket path and the TLS settings and connects over plain TCP. Build the dsn in
one place from the chosen driver, renaming caller supplied options too, since
zmupdate.pl asks for mysql_multi_statements.

Read the id of an inserted row through DBI's last_insert_id rather than the
mysql_insertid handle attribute, which DBD::MariaDB does not have and would
answer undef for, leaving events and objects with no Id.

DBD::MariaDB is always utf8mb4, so it takes no mysql_enable_utf8mb4 attribute.

Let cmake accept either driver instead of requiring DBD::mysql, checking in the
same order, and warn when neither is installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019URmtYqza6Rzi6F7cmabSm
2026-08-20 08:12:25 -04:00
Isaac Connor 022410c299 fix: stop abandoned events matching every DateTime window, and index the query
A DateTime lower bound was written as

  COALESCE(E.EndDateTime, '9999-12-31 23:59:59') >= T1

reading "an event that has not ended yet never ends". EndDateTime is NULL for
an event that is still recording, but also for one zmc was killed part way
through, and that stays NULL forever - so every abandoned event matched every
window from then on. Wrapping the column in a function also meant no index
could range over it: with the only MonitorId key being MonitorId alone, every
montage review request read all of that monitor's events and filtered them in
memory, however narrow the window.

Length tells the two cases apart. It is flushed during recording, so a live
event's effective end keeps advancing while an abandoned one's is frozen at
whatever was recorded. Use the same CASE the SELECT list in ajax/events.php
already uses, and bound it below by StartDateTime.

The lower bound now emits three conjuncts:

  E.StartDateTime >= DATE_SUB(T1, INTERVAL 1 DAY)
  AND (E.EndDateTime IS NULL OR E.EndDateTime >= T1)
  AND <effective end> >= T1

The floor bounds the scan for a window in the past and drops events abandoned
more than a day earlier - events do not outlive the nightly logrotate SIGHUP,
which stops and restarts them, so a day is comfortably beyond any real event.
The middle conjunct is implied by the third and exists only to give the
optimiser a second indexable handle: it is narrow exactly when the floor is
wide, so between them a window at either end of the retention period has
something cheap to range over. Verified the optimiser picks correctly for both,
unprompted. The third is the residual that gets the semantics right.

Add the two composite keys those conjuncts need. Two range columns cannot both
narrow one B-tree, and these are mirror images - EndDateTime >= T1 is
open-ended upwards, StartDateTime <= T2 downwards:

  Events_MonitorId_StartDateTime_idx (MonitorId, StartDateTime)
  Events_EndDateTime_MonitorId_idx   (EndDateTime, MonitorId)

MonitorId leads the first because it is the equality; past a range column later
columns can no longer narrow the scan, and (StartDateTime, MonitorId) measured
3.5x slower for the same rows. EndDateTime leads the second so it also covers
zmaudit's hunt for events that were never closed, which has no monitor to scope
it - 9 rows with a key against a 22,948 row table scan without.

Both replacements are added before the keys they supersede are dropped:

  Events_MonitorId_idx (MonitorId) is now a leftmost prefix of the new key.
  Events_EndDateTime_DiskSpace (EndDateTime, DiskSpace) existed for the scan
  that hunted events with no DiskSpace set; DiskSpace is set when the event is
  finalised in C++, so nothing scans for it any more.

Net index count on Events is unchanged.

Measured on a 7,167 event monitor. Default one hour window: 7,055 rows
examined / 25.4ms -> 173 rows / 4ms. Window scrubbed a week back, which
persists in the zmFilter_StartDateTime cookie: 6,995 rows / 28ms -> 19 rows /
0.15ms.

Adds migration db/zm_update-1.39.22.sql. Extends t/filter_sql.t; the PHP and Perl were
checked to emit identical SQL, and the semantics checked against a table of
seven event shapes - abandoned long ago, abandoned recently, overlapping,
inside, before, still recording, and still recording for 17 hours.
2026-08-19 22:06:21 -05:00
Isaac Connor 831c4ce6b6 fix: lock filter events one at a time instead of locking the whole batch
A filter with LockRows set selected its entire result set FOR UPDATE inside one
transaction, so every lock the per-event work went on to take was held until
the run committed:

  Events[Id] -> Events_Hour/Day/Week/Month[EventId] -> Event_Summaries[MonitorId] -> Storage[Id]

Because the locks accumulated across events, two filters deadlocked: one held
Event_Summaries for a monitor while waiting on a Storage row, the other held
that Storage row while waiting on Event_Summaries for its next event. No
per-event lock ordering can fix that while both rows stay locked for the length
of the batch.

Holding Event_Summaries for the whole run also blocked zmc from opening a new
event on any monitor the filter had touched, since creating an event updates
that row. The transaction spanned ffmpeg encodes, uploads and executed commands
as well, so it could be held open for minutes.

zmfilter now claims one event at a time in Events_Lock and releases it when it
is done with that event, so no InnoDB lock is held across the work. The
per-event body moves into checkFilterEvent.

skip_locked now adds NOT EXISTS over Events_Lock to the filter query rather
than SKIP LOCKED. The exclusion has to happen in the query: a filter whose
whole result set was held elsewhere would otherwise fill its LIMIT with events
it could only skip, and make no progress. It no longer depends on MariaDB 10.6
/ MySQL 8.0.1, so the UI no longer disables the option on older servers.

Also drops the two dbh->commit() calls in the AutoCopy branch. With no
transaction open they would warn, and before this they were silently ending the
batch transaction mid-loop, so AutoCopy filters never had the guarantee
LockRows was supposed to give them.

filterdebug.php was appending a bare ' SKIP LOCKED' after the LIMIT, which is
not valid SQL; it now renders the real clause in the right position.

Adds t/event_lock.t and t/filter_sql.t.
2026-08-19 22:06:02 -05:00
Isaac Connor 3b130e4c94 fix: adjust Storage.DiskSpace with a relative UPDATE instead of read-modify-write
lock_and_load + save is a read-modify-write, and outside a transaction the
SELECT ... FOR UPDATE autocommits and drops its lock immediately. The absolute
value written afterwards then clobbers any adjustment zmc, zmaudit or another
filter made in between.

Replace both call sites with ZoneMinder::Storage::adjust_diskspace, a single
relative UPDATE that is atomic on its own and holds the row lock only for the
length of that statement. It reads the new total back into the object rather
than adding the delta locally, because ZoneMinder::Object caches objects for
the life of the process and its accessors never re-load, so the cached value
can be arbitrarily stale and adjusting it locally would preserve the error.

Keeping the adjustment to one statement also keeps Storage out of the lock
chain that deleting an event walks:

  Events[Id] -> Events_Hour/Day/Week/Month[EventId] -> Event_Summaries[MonitorId]

Adds t/storage_diskspace.t.
2026-08-19 22:04:37 -05:00
Isaac ConnorandClaude Opus 5 4bebdad44a refactor: move the systemd detection out of zmpkg.pl into ZoneMinder::Server and test it
The two subs decide whether zmpkg.pl hands start/stop/restart to systemd or
runs the daemons itself, which is what keeps them out of the web server's
cgroup and mount namespace. Living in a script they could not be reached by
require_ok, so the cgroup matching had no test.

Split the matching into cgroup_in_service, which takes the text of a cgroup
file rather than reading one, and cover it: cgroup v2's single line and v1's
several, a sub-cgroup of a delegated service, a unit whose name merely starts
with ours, and empty or undef input.

The test found that the delimiter in m|/\Q$unit\E\.service(?:/|$)| ended the
match at the alternation, so the module did not compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019URmtYqza6Rzi6F7cmabSm
2026-08-17 20:52:10 -04:00
Isaac ConnorandClaude Opus 5 ab08d37588 fix: merge OpenBSD top cpu parsing into Server::CpuUsage
The openbsd branch and master both changed the top fallback in CpuUsage
and diverged. Combine them:

- Keep master's /proc/stat diagnostics: report why /proc/stat could not
  be read (ProcSubset=pid mount namespace vs open failure), default the
  fields split out of top output so an empty result does not warn under
  -w, and only complain once per process.
- Keep the openbsd branch's parse_bsd_top_cpu(): OpenBSD top prints CPU
  states in a format neither the FreeBSD nor the Linux grep matches, so
  when the greps produce no numbers, parse raw top output. It handles
  both the aggregate "CPU states:" line and per-core "CPU0 states:"
  lines, averaging over the lines seen, and does not depend on padding.
- Carry over scripts/ZoneMinder/t/server_cpu.t covering it.

Tests: prove -Iscripts/ZoneMinder/lib scripts/ZoneMinder/t/server_cpu.t
-> 16/16 pass. perl -c on Server.pm is clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kfiom9RefM75GTZFkqpJo2
2026-08-15 11:16:03 -04:00
Isaac ConnorandClaude Opus 4.8 154786d31d fix: derive event start from mp4 end time in recover_timestamps
The mp4 is written as the event records, so its mtime is when recording
finished, not when it started. Set EndDateTime to the mtime and
StartDateTime to duration seconds before it, so Length agrees with
EndDateTime - StartDateTime.

The directory mtime fallback also ran unconditionally after both
branches, overwriting the times just computed and the Deep-scheme
path-derived start. Only use it when there are neither capture jpgs nor
an mp4.

Adds t/event_recover_timestamps.t, which builds a 3 second mp4 with
ffmpeg and checks the recovered times against its mtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV1gDc9T6D6H8gtTcxwLvk
2026-07-21 12:11:37 -04:00
Isaac ConnorandClaude Opus 4.8 e5bdb5f7a1 feat: add white-light (CanLight) control to HikVision/LTS module refs #4895
Add lightOn/lightOff/lightStatus to the HikVision control module, driving the
camera's ISAPI supplement light (ISAPI/Image/channels/<n>/supplementLight).
"On" selects the white light where the model has one (colorVuWhiteLight), else
IR; "off" restores the camera's smart/auto default (eventIntelligence) so night
IR keeps working. The methods GET the current document and rewrite only
<supplementLightMode>, preserving the std-cgi namespace and the sibling
brightness fields the firmware rejects PUTs without. Mode selection is driven by
the model's advertised supplementLight/capabilities. lightStatus returns
{ WhiteLight => 'On'|'Off'|undef } in the shape the existing web toggle consumes,
so no web changes are needed (the CanLight UI is already generic).

Add a model-specific Controls row for the LTS CMIP1342WE-28MDA (a fixed ColorVu
camera: white light, reboot, no PTZ/focus/iris), in zm_create.sql.in and
migration zm_update-1.39.16.sql. Bump version to 1.39.16.

The pure mode-selection/XML helpers are covered by t/hikvision_light.t. Verified
live against a CMIP1342WE-28MDA: the GET-modify-PUT round-trip turns the white
light on and restores the prior mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:59:10 -05:00
Isaac ConnorandClaude Opus 4.8 fd83672a11 feat: add status-aware Light control capability and control-command response path refs #4869
Add a CanLight control capability rendering a single status-aware Light toggle button. The ADC2W white light is driven via CoaxialControlIO.control (Type 1, numeric IO); the button queries live state and reflects it (amber when on).

To get device state to the browser, add an opt-in two-way response path to the control protocol: zmcontrol writes a JSON result back only when a request sets wants_response (fire-and-forget commands unchanged, SIGPIPE-safe); Monitor::sendControlCommandWithResponse and ajax/control.php return it.

Also adds get_config/set_config/probe to Dahua_RPC for characterising cameras, the CanLight column (migration zm_update-1.39.12.sql), edit-UI checkbox, and a config unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:00:00 -04:00
Isaac ConnorandClaude Opus 4.8 b180b1b736 feat: add Dahua/Amcrest RPC PTZ control module with per-model Controls refs #120
Add ZoneMinder::Control::Dahua_RPC, a PTZ control module driving Amcrest Smart Home (ASH21/ASH42/ADC2W) and Dahua cameras over the JSON-RPC /RPC2 interface, since their cgi-bin API is disabled and ONVIF exposes no PTZ service. Two-stage MD5-challenge login with session reuse and self-healing re-login; continuous pan/tilt + diagonals, stop, presets, zoom, focus, reboot.

Adds a generic 'Dahua/Amcrest RPC' Controls row plus model-specific rows for the ASH21-B (pan/tilt only) and ADC2W (reboot only), the ASH21-B/ADC2W models, migration zm_update-1.39.11.sql, and a login-hash unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:00:00 -04:00
Nic BoetandClaude Opus 4.6 c23869b6ac build: replace Perl ExtUtils::MakeMaker with pure CMake install
MakeMaker was only used to copy .pm files — no XS compilation, no binary
linking, no dependency resolution. Its hardcoded "Makefile" output name
conflicts with cmake's generated Makefile for in-source builds, and using
FIRST_MAKEFILE=MakefilePerl causes thousands of "uninitialized value"
warnings because MM.pm stats the wrong file.

Replace with native CMake install(DIRECTORY ... FILES_MATCHING PATTERN
"*.pm") directives. Perl module install path is auto-detected at configure
time via `perl -MConfig` (vendorlib on Linux, sitelib on FreeBSD),
overridable with -DZM_PERL_INSTALL_PATH=<path>.

What's removed:
- ExtUtils::MakeMaker as build dependency
- Three perl+make subprocesses at build time (zmperlmodules,
  zmonvifmodules, zmonvifproxy build targets)
- ~6000 auto-generated man3 pages from WSDL stubs
- MakeMaker scaffolding: Makefile.PL, MANIFEST, META.yml, Changes,
  README, and t/ZoneMinder.t test stub

What's preserved:
- configure_file() for .pm.in templates (same behavior)
- ZM_PERL_SEARCH_PATH (independent mechanism, unchanged)
- Section 8 man pages for .pl scripts (Pod2Man.cmake, unaffected)
- DESTDIR support (CMake install() handles natively)
- Installed file paths (perl -MConfig returns same paths MakeMaker used)

Verified: 3102 .pm files installed, 0 .pm.in files, 0 .3pm man pages,
no @VERSION@ markers in generated files, DESTDIR and user override work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 14:47:35 -06:00
stan 821763e2bd Bug 231 - Created ZoneMinder perl module to abstract out common code.
git-svn-id: http://svn.zoneminder.com/svn/zm/trunk@1673 e3e1d417-86f3-4887-817a-d78f3d33393f
2005-12-16 10:05:29 +00:00