From f04119123b7f13d4af0fac744f998a880f634ff2 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Mon, 15 Jun 2026 21:23:34 +0300 Subject: [PATCH 1/4] Replace "%" with "%25" in the message line (log.js) Otherwise, decodeURIComponent() will fail, as "%" without subsequent characters is not allowed in a URL. --- web/skins/classic/views/js/log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/log.js b/web/skins/classic/views/js/log.js index a054a1124..45e7e0d59 100644 --- a/web/skins/classic/views/js/log.js +++ b/web/skins/classic/views/js/log.js @@ -76,7 +76,7 @@ function ajaxRequest(params) { function processRows(rows) { $j.each(rows, function(ndx, row) { try { - row.Message = decodeURIComponent(row.Message) + row.Message = decodeURIComponent(row.Message.replace(/%/g, '%25')) .replace(//g, ">") // Replace link tags .replace(/event (\d+)/g, "event $1"); } catch (e) { From 58a328a2049c264e3f9a12c45d85df068953ce32 Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Mon, 15 Jun 2026 21:58:06 +0300 Subject: [PATCH 2/4] Of course: "% " => '%25%20' (log.js) --- web/skins/classic/views/js/log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/log.js b/web/skins/classic/views/js/log.js index 45e7e0d59..006e3b481 100644 --- a/web/skins/classic/views/js/log.js +++ b/web/skins/classic/views/js/log.js @@ -76,7 +76,7 @@ function ajaxRequest(params) { function processRows(rows) { $j.each(rows, function(ndx, row) { try { - row.Message = decodeURIComponent(row.Message.replace(/%/g, '%25')) + row.Message = decodeURIComponent(row.Message.replace(/% /g, '%25%20')) .replace(//g, ">") // Replace link tags .replace(/event (\d+)/g, "event $1"); } catch (e) { From f51f82671a4f9b7f978379926dd0cc851dcbf79e Mon Sep 17 00:00:00 2001 From: IgorA100 Date: Tue, 16 Jun 2026 09:37:34 +0300 Subject: [PATCH 3/4] Update web/skins/classic/views/js/log.js Co-authored-by: Isaac Connor --- web/skins/classic/views/js/log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/skins/classic/views/js/log.js b/web/skins/classic/views/js/log.js index 006e3b481..3f9f8abd9 100644 --- a/web/skins/classic/views/js/log.js +++ b/web/skins/classic/views/js/log.js @@ -76,7 +76,7 @@ function ajaxRequest(params) { function processRows(rows) { $j.each(rows, function(ndx, row) { try { - row.Message = decodeURIComponent(row.Message.replace(/% /g, '%25%20')) + row.Message = decodeURIComponent(row.Message.replace(/%(?![0-9A-Fa-f]{2})/g, '%25')) .replace(//g, ">") // Replace link tags .replace(/event (\d+)/g, "event $1"); } catch (e) { From 19b55adb9c0501bc71b7812f241b3614ee63d0d3 Mon Sep 17 00:00:00 2001 From: Isaac Connor Date: Tue, 16 Jun 2026 18:30:02 -0400 Subject: [PATCH 4/4] fix: ping camera before reboot in zmwatch and resolve host in Control::ping zmwatch only gated the camera reboot attempt on CanReboot(), so it called $control->open() even when the camera was unreachable - the common reason a monitor has no image since startup - and blocked until the connection timed out, logging an error each pass. Add a ping check before open(). Move the host resolution into Control so callers don't have to dig the ip out of the Path: add Control::host(), which returns the cached host or derives it from the monitor's ControlAddress/Path via the shared guess_credentials() (parsing only, no network i/o), and have ping() fall back to it. ping() still accepts an explicit ip. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/ZoneMinder/lib/ZoneMinder/Control.pm | 20 +++++++++++++++++++- scripts/zmwatch.pl.in | 12 ++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm index 6e29ea992..834fde4ea 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control.pm @@ -593,9 +593,27 @@ sub get_realm { return undef; } # end sub get_realm +# Resolve the camera host/ip from the monitor without opening a connection. +# Returns the cached $self->{host} if open()/guess_credentials() already set it, +# otherwise parses it out of the monitor's ControlAddress/Path using the shared +# guess_credentials() (which only needs a UserAgent, no network I/O). This lets +# callers such as ping() work before open() has been called. +sub host { + my $self = shift; + $$self{host} = shift if @_; + return $$self{host} if $$self{host}; + + if (!$self->{ua}) { + require LWP::UserAgent; + $self->{ua} = LWP::UserAgent->new(); + } + $self->guess_credentials(); + return $$self{host}; +} + sub ping { my $self = shift; - my $ip = @_ ? shift : $$self{host}; + my $ip = @_ ? shift : $self->host(); if (!$ip) { Warning("No ip to ping. Please either pass ip or populate self{host}"); return undef; diff --git a/scripts/zmwatch.pl.in b/scripts/zmwatch.pl.in index 2d37ff475..ff796cb7d 100644 --- a/scripts/zmwatch.pl.in +++ b/scripts/zmwatch.pl.in @@ -132,8 +132,16 @@ while (!$zm_terminate) { Debug("Monitor $monitor->{Id} $monitor->{Name}, startup time $now - $startup_time $startup_elapsed ControlId()) { my $control = $monitor->Control(); - if ($control and $control->CanReboot() and $control->open()) { - $control->reboot(); + # Only try to reboot the camera if it actually answers. Otherwise + # open() blocks until it times out on a camera that is down (the + # common reason there is no image since startup). ping() resolves the + # host from the monitor itself, so no need to dig the ip out here. + if ($control and $control->CanReboot()) { + if (!$control->ping()) { + Debug("Not rebooting $monitor->{Id} $monitor->{Name}: camera is not reachable"); + } elsif ($control->open()) { + $control->reboot(); + } } } $log->logPrint(ZoneMinder::Logger::WARNING+$monitor->ImportanceNumber(),