Files
zoneminder/scripts/zmdc.pl.in
T
Isaac ConnorandClaude Opus 5.5 a3c9d5cf70 fix: start zmdc's daemons with zmdc's signal mask and log their signals correctly
start() blocks SIGCHLD (and since #5199 TERM, INT and ABRT) across the fork.
The child unblocks the stop signals, but SIGCHLD stayed blocked through exec,
so every daemon zmdc started ran with it blocked (SigBlk 0x10000 in
/proc/<pid>/status for zmc, zmfilter, zmstats and zmwatch). Nothing depends
on it today, but a daemon that waits for SIGCHLD would never get it. Restore
the pre-fork mask just before exec.

The reaper took the signal from a wait status as $status & 0xfe, dropping its
low bit, so TERM was logged as signal 14, KILL as 8 and SEGV as 10, and the
core dump flag as $status & 0x01. Use 0x7f and 0x80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:39:00 -04:00

980 lines
31 KiB
Plaintext

#!@PERL_EXECUTABLE@ -wT
#
# ==========================================================================
#
# ZoneMinder Daemon Control Script, $Date$, $Revision$
# Copyright (C) 2001-2008 Philip Coombes
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ==========================================================================
=head1 NAME
zmdc.pl - ZoneMinder Daemon Control script
=head1 SYNOPSIS
zmdc.pl {command} [daemon [options]]
=head1 DESCRIPTION
This script is the gateway for controlling the various ZoneMinder
daemons. All starting, stopping and restarting goes through here.
On the first invocation it starts up a server which subsequently
records what's running and what's not. Other invocations just
connect to the server and pass instructions to it.
=head1 OPTIONS
{command} - One of 'startup|shutdown|status|check|logrot' or
'start|stop|restart|reload|version'.
[daemon [options]] - Daemon name and options, required for second group of commands
=cut
use strict;
use warnings;
use bytes;
# ==========================================================================
#
# User config
#
# ==========================================================================
# in useconds, not seconds.
use constant MAX_CONNECT_DELAY => 40;
# ==========================================================================
#
# Don't change anything from here on down
#
# ==========================================================================
@EXTRA_PERL_LIB@
use ZoneMinder;
use ZoneMinder::Server;
use POSIX;
use Socket;
use IO::Handle;
use Time::HiRes qw(usleep);
use autouse 'Pod::Usage'=>qw(pod2usage);
#use Data::Dumper;
use constant SOCK_FILE => $Config{ZM_PATH_SOCKS}.'/zmdc'.($Config{ZM_SERVER_ID}?$Config{ZM_SERVER_ID}:'').'.sock';
$| = 1;
$ENV{PATH} = '@ZM_SCRIPT_PATH@';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
if ( $Config{ZM_LD_PRELOAD} ) {
Debug("Adding ENV{LD_PRELOAD} = $Config{ZM_LD_PRELOAD}");
$ENV{LD_PRELOAD} = $Config{ZM_LD_PRELOAD};
foreach my $lib ( split(/\s+/, $ENV{LD_PRELOAD} ) ) {
if ( ! -e $lib ) {
Warning("LD_PRELOAD lib $lib does not exist from LD_PRELOAD $ENV{LD_PRELOAD}.");
}
}
}
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
my @daemons = (
'zmc',
'zmfilter.pl',
'zmaudit.pl',
'zmtrigger.pl',
'zmx10.pl',
'zmwatch.pl',
'zmupdate.pl',
'zmstats.pl',
'zmtrack.pl',
'zmcontrol.pl',
'zm_rtsp_server',
'zmtelemetry.pl',
'zmalarm-server.py'
);
if ( $Config{ZM_OPT_USE_EVENTNOTIFICATION} ) {
push @daemons, 'zmeventnotification.pl';
}
my $command = shift @ARGV;
if ( !$command ) {
print(STDERR "No command given\n");
pod2usage(-exitstatus => -1);
}
if ( $command eq 'version' ) {
print ZoneMinder::Base::ZM_VERSION."\n";
exit(0);
}
my $needs_daemon = $command !~ /(?:startup|shutdown|status|check|logrot|version)/;
my $daemon = shift @ARGV;
if ( $needs_daemon && !$daemon ) {
print(STDERR "No daemon given\n");
pod2usage(-exitstatus => -1);
}
my @args;
my $daemon_patt = '('.join('|', @daemons).')';
if ( $needs_daemon ) {
if ( $daemon =~ /^${daemon_patt}$/ ) {
$daemon = $1;
} else {
print(STDERR "Invalid daemon '$daemon' specified");
pod2usage(-exitstatus => -1);
}
}
foreach my $arg ( @ARGV ) {
# Detaint arguments, if they look ok
#if ( $arg =~ /^(-{0,2}[\w]+)/ )
if ( $arg =~ /^(-{0,2}[\w\/?&=.-]+)$/ ) {
push @args, $1;
} else {
print(STDERR "Bogus argument '$arg' found");
exit(-1);
}
}
my $dbh = zmDbConnect();
socket(CLIENT, PF_UNIX, SOCK_STREAM, 0) or Fatal("Can't open socket: $!");
my $saddr = sockaddr_un(SOCK_FILE);
my $server_up = connect(CLIENT, $saddr);
if (!$server_up) {
my $server = new ZoneMinder::Server($Config{ZM_SERVER_ID});
if ($server->Id()) {
use Sys::MemInfo qw(totalmem freemem totalswap freeswap);
my ($user_percent, $nice_percent, $sys_percent, $idle_percent, $usage_percent) = $server->CpuUsage();
my @cpuload = $server->CpuLoad();
my $in_transaction = ZoneMinder::Database::start_transaction($dbh);
$server->lock_and_load(); # get fresh other values
if ($_=$server->save({Status=>'NotRunning',
CpuLoad=>$cpuload[0],
TotalMem=>&totalmem, FreeMem=>&freemem, TotalSwap=>&totalswap, FreeSwap=>&freeswap,
CpuUserPercent=>$user_percent, CpuNicePercent=>$nice_percent, CpuSystemPercent=>$sys_percent, CpuIdlePercent=>$idle_percent, CpuUsagePercent=>$usage_percent,
})) {
Error('Failed Updating status of Server record to Not Running for Id='.$Config{ZM_SERVER_ID}.': '.$dbh->errstr());
}
ZoneMinder::Database::end_transaction($dbh, $in_transaction);
}
# Server is not up. Some commands can still be handled
if ( $command eq 'logrot' ) {
# If server is not running, then logrotate doesn't need to do anything.
Debug('Server is not running, logrotate doesn\'t need to do anything');
exit();
}
if ( $command eq 'check' ) {
print("stopped\n");
exit();
} elsif ( $command ne 'startup' ) {
print('Unable to connect to server using socket at '.SOCK_FILE."\n");
exit(-1);
}
# The server isn't there
print("Starting server\n");
close(CLIENT);
if ( my $cpid = fork() ) {
# Parent process just sleep and fall through
# I'm still not sure why we need to re-init the logs
logInit();
socket(CLIENT, PF_UNIX, SOCK_STREAM, 0) or Fatal("Can't open socket: $!");
my $attempts = 0;
while ( !connect(CLIENT, $saddr) ) {
$attempts++;
Debug('Waiting for zmdc.pl server process at '.SOCK_FILE.', attempt '.$attempts);
Fatal('Can\'t connect to zmdc.pl server process at '.SOCK_FILE.': '.$!) if $attempts > MAX_CONNECT_DELAY;
usleep(200000);
} # end while
} elsif ( defined($cpid) ) {
ZMServer::run();
} else {
Fatal("Can't fork: $!");
}
} # end if ! server is up
if ( ($command eq 'check') && !$daemon ) {
print("running\n");
exit();
} elsif ( $command eq 'startup' ) {
# Our work here is done
exit() if !$server_up;
}
# The server is there, connect to it
CLIENT->autoflush();
my $message = join(';', $command, ( $daemon ? $daemon : () ), @args);
print(CLIENT $message);
shutdown(CLIENT, 1);
# Read response with timeout to avoid hanging indefinitely
my $client_rin = '';
vec($client_rin, fileno(CLIENT), 1) = 1;
while ( select(my $rout = $client_rin, undef, undef, 30) > 0 ) {
my $line = <CLIENT>;
last if !defined($line);
chomp($line);
print("$line\n");
}
close(CLIENT);
exit;
package ZMServer;
use strict;
use warnings;
use bytes;
@EXTRA_PERL_LIB@
use ZoneMinder;
use POSIX;
use Socket;
use IO::Handle;
use Time::HiRes qw(usleep);
use Sys::MemInfo qw(totalmem freemem totalswap freeswap);
use ZoneMinder::Server;
#use Data::Dumper;
use constant KILL_DELAY => 30; # seconds to wait between sending TERM and sending KILL
# ffmpeg connect can take up to 30 seconds at least and doesn't seem to ack signals
# or get back to zm code so we can exit gracefully
our %cmd_hash;
our %pid_hash;
our %terminating_processes;
our %pids_to_reap;
our $zm_terminate = 0;
# Re-assert the pid file so systemd (PIDFile=) keeps tracking us even if it
# was removed out-of-band (e.g. tmpfiles cleanup of /run, or a partial/aborted
# stop that unlinked the pid but left us running). Cheap: only writes when the
# file is missing or its contents don't match our pid. Shutdown-safe: the
# shutdown unlink runs only after the main loop exits, so this can never
# recreate the pid file after termination has begun.
my $pid_reassert_failed = 0;
sub ensure_pid {
my $current;
if ( open(my $fh, '<', ZM_PID) ) {
$current = <$fh>;
close($fh);
chomp $current if defined $current;
}
return if defined($current) and $current eq "$$";
if ( open(my $PID, '>', ZM_PID) ) {
print($PID $$);
close($PID);
Warning('Re-asserted pid file '.ZM_PID.' (was missing or stale)');
$pid_reassert_failed = 0;
} elsif ( !$pid_reassert_failed ) {
# Warn only on the first failure while the condition persists, so a
# permanently unwritable ZM_PID (e.g. /run/zm removed) does not flood the
# log every loop tick. Reset above once a write succeeds again.
Warning('Unable to (re)write pid file '.ZM_PID.": $!");
$pid_reassert_failed = 1;
}
}
sub run {
# Call this first otherwise stdout/stderror redirects to the pidfile = bad
if ( open(my $PID, '>', ZM_PID) ) {
print($PID $$);
close($PID);
} else {
# Log not initialized at this point so use die instead
die 'Can\'t open pid file at '.ZM_PID."\n";
}
# Redirect stdin/stdout/stderr to /dev/null rather than closing them.
# Closing them causes the FDs to be reused, which led to memory corruption
# when libx264 wrote to a reused stderr FD (66f11435b). Redirecting to
# /dev/null keeps valid FDs that children inherit safely.
open(STDIN, '<', '/dev/null') or die "Can't redirect STDIN: $!";
open(STDOUT, '>', '/dev/null') or die "Can't redirect STDOUT: $!";
open(STDERR, '>', '/dev/null') or die "Can't redirect STDERR: $!";
# Close all remaining FDs (dbh, CLIENT, SERVER, etc.)
my $fd = 3;
while ( $fd < POSIX::sysconf(&POSIX::_SC_OPEN_MAX) ) {
POSIX::close($fd++);
}
# Sets a process group, so that signals to go this and it's children I think
setpgrp();
# dbh got closed with the rest of the fd's above, so need to reconnect.
my $dbh = zmDbConnect(1);
logInit();
dPrint(ZoneMinder::Logger::INFO, 'Server starting at '
.strftime('%y/%m/%d %H:%M:%S', localtime())
."\n"
);
# We don't want to leave killall zombies, so ignore SIGCHLD
$SIG{CHLD} = 'IGNORE';
# Tell any existing processes to die, wait 1 second between TERM and KILL
killAll(1);
dPrint(ZoneMinder::Logger::INFO, 'Socket should be open at ' .main::SOCK_FILE);
socket(SERVER, PF_UNIX, SOCK_STREAM, 0) or Fatal("Can't open socket: $!");
unlink(main::SOCK_FILE) or Error('Unable to unlink ' . main::SOCK_FILE .". Error message was: $!") if -e main::SOCK_FILE;
bind(SERVER, sockaddr_un(main::SOCK_FILE)) or Fatal('Can\'t bind to ' . main::SOCK_FILE . ": $!");
listen(SERVER, SOMAXCONN) or Fatal("Can't listen: $!");
$SIG{CHLD} = \&chld_sig_handler;
$SIG{INT} = \&shutdown_sig_handler;
$SIG{TERM} = \&shutdown_sig_handler;
$SIG{ABRT} = \&shutdown_sig_handler;
$SIG{HUP} = \&logrot;
my $rin = '';
vec($rin, fileno(SERVER), 1) = 1;
my $timeout = 1;
my $secs_count = 0;
my $server = new ZoneMinder::Server($Config{ZM_SERVER_ID});
while ( !$zm_terminate ) {
if ($server->Id()) {
if ( ! ( $secs_count % 60 ) ) {
while ( (!$zm_terminate) and !($dbh and $dbh->ping()) ) {
Warning("Not connected to db ($dbh)".($dbh?' ping('.$dbh->ping().')':''). ($DBI::errstr?" errstr($DBI::errstr)":'').' Reconnecting');
$dbh = zmDbConnect();
sleep 10 if !$dbh;
}
last if $zm_terminate;
my $in_transaction = ZoneMinder::Database::start_transaction($dbh);
$server->lock_and_load(); # get fresh other values
my @cpuload = $server->CpuLoad();
my ($user_percent, $nice_percent, $sys_percent, $idle_percent, $usage_percent) = $server->CpuUsage();
if ($_=$server->save({Status=>'Running',
TotalMem=>&totalmem, FreeMem=>&freemem, TotalSwap=>&totalswap, FreeSwap=>&freeswap, CpuLoad=>$cpuload[0],
CpuUserPercent=>$user_percent, CpuNicePercent=>$nice_percent, CpuSystemPercent=>$sys_percent, CpuIdlePercent=>$idle_percent, CpuUsagePercent=>$usage_percent
})) {
Error('Failed Updating status of Server record to Running for Id='.$Config{ZM_SERVER_ID}.': '.$dbh->errstr());
}
ZoneMinder::Database::end_transaction($dbh, $in_transaction);
}
$secs_count += 1;
}
my $nfound = select(my $rout = $rin, undef, undef, $timeout);
if ( $nfound > 0 ) {
if ( vec($rout, fileno(SERVER), 1) ) {
my $paddr = accept(CLIENT, SERVER);
my $message = <CLIENT>;
next if !$message;
my ( $command, $daemon, @args ) = split(';', $message);
if ( $command eq 'start' ) {
start($daemon, @args);
} elsif ( $command eq 'stop' ) {
stop($daemon, @args);
} elsif ( $command eq 'restart' ) {
restart($daemon, @args);
} elsif ( $command eq 'reload' ) {
reload($daemon, @args);
} elsif ( $command eq 'startup' ) {
# Do nothing, this is all we're here for
dPrint(ZoneMinder::Logger::WARNING, "Already running, ignoring command '$command'\n");
} elsif ( $command eq 'shutdown' ) {
# Break out of while loop
last;
} elsif ( $command eq 'check' ) {
check($daemon, @args);
} elsif ( $command eq 'status' ) {
if ( $daemon ) {
status($daemon, @args);
} else {
status();
}
} elsif ( $command eq 'logrot' ) {
logrot();
} else {
dPrint(ZoneMinder::Logger::ERROR, "Invalid command '$command'\n");
}
close(CLIENT);
} else {
Error('Bogus descriptor');
}
} elsif ( $nfound < 0 ) {
if ( $! == EINTR ) {
# Dead child, will be reaped
#print( "Probable dead child\n" );
# See if it needs to start up again
} elsif ( $! == EPIPE ) {
Error("Can't select: $!");
} else {
Fatal("Can't select: $!");
}
} else {
#print( "Select timed out\n" );
}
restartPending();
check_for_processes_to_kill() if %terminating_processes;
reaper() if %pids_to_reap;
ensure_pid();
} # end while
dPrint(ZoneMinder::Logger::INFO, 'Server exiting at '.strftime('%y/%m/%d %H:%M:%S', localtime())."\n");
if ($Config{ZM_SERVER_ID}) {
$dbh = zmDbConnect() if ! ($dbh and $dbh->ping());
my $server = new ZoneMinder::Server($Config{ZM_SERVER_ID});
my $in_transaction = ZoneMinder::Database::start_transaction($dbh);
$server->lock_and_load(); # get fresh other values
if ($_=$server->save({Status=>'NotRunning'})) {
Error('Failed Updating status of Server record to Not Running for Id='.$Config{ZM_SERVER_ID}.': '.$dbh->errstr());
}
ZoneMinder::Database::end_transaction($dbh, $in_transaction);
}
shutdownAll();
}
sub cPrint {
# One thought here, if no client exists to read these... does it block?
if ( fileno(CLIENT) ) {
print CLIENT @_
}
}
# I think the purpose of this is to echo the logs to the client process so it can then display them.
sub dPrint {
my $logLevel = shift;
cPrint(@_);
if ( $logLevel == ZoneMinder::Logger::DEBUG ) {
Debug(@_);
} elsif ( $logLevel == ZoneMinder::Logger::INFO ) {
Info(@_);
} elsif ( $logLevel == ZoneMinder::Logger::WARNING ) {
Warning(@_);
} elsif ( $logLevel == ZoneMinder::Logger::ERROR ) {
Error(@_);
} elsif ( $logLevel == ZoneMinder::Logger::FATAL ) {
Fatal(@_);
}
}
sub start {
my $daemon = shift;
if ( $daemon !~ /^$daemon_patt$/ ) {
Error("Invalid daemon '$daemon' specified");
return;
}
my @args = @_;
my $command = join(' ', $daemon, @args);
my $process = $cmd_hash{$command};
if ( !$process ) {
# It's not running, or at least it's not been started by us
$process = { daemon=>$daemon, args=>\@args, command=>$command, keepalive=>1 };
} elsif ( $process->{pid} && $pid_hash{$process->{pid}} ) {
if ($process->{term_sent_at}) {
dPrint(ZoneMinder::Logger::INFO, "'$process->{command}' was told to term at "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{term_sent_at}))
.", pid = $process->{pid}\n"
);
$process->{keepalive} = !undef;
$process->{delay} = 0;
delete $terminating_processes{$command};
} else {
dPrint(ZoneMinder::Logger::INFO, "'$process->{command}' already running at "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{started}))
.", pid = $process->{pid}\n"
);
}
return;
}
# We have to block SIGCHLD during fork to prevent races while we setup our records for it.
# TERM, INT and ABRT are blocked too: the child starts with our handlers, which
# only set $zm_terminate, so a stop sent to it before it resets them would be
# lost and the daemon it execs would keep running until KILL_DELAY.
my $sigset = POSIX::SigSet->new;
my $blockset = POSIX::SigSet->new(SIGCHLD, SIGTERM, SIGINT, SIGABRT);
sigprocmask(SIG_BLOCK, $blockset, $sigset) or Fatal("Can't block SIGCHLD: $!");
# Apparently the child closing the db connection can affect the parent.
zmDbDisconnect();
if ( my $child_pid = fork() ) {
$dbh = zmDbConnect(1);
# This logReinit is required. Not sure why.
logReinit();
$process->{pid} = $child_pid;
$process->{started} = time();
delete $process->{pending};
dPrint(ZoneMinder::Logger::INFO, "'$command' starting at "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{started}))
.", pid = $process->{pid}\n"
);
$cmd_hash{$process->{command}} = $pid_hash{$child_pid} = $process;
sigprocmask(SIG_SETMASK, $sigset) or Fatal("Can't restore SIGCHLD: $!");
} elsif ( defined($child_pid) ) {
# Child process
# Take the stop signals back to their defaults first, then unblock them, so
# a TERM sent while we are still getting ready to exec (connecting to the
# db, closing descriptors) ends this child and zmdc's reaper sees it go.
$SIG{INT} = 'DEFAULT';
$SIG{TERM} = 'DEFAULT';
$SIG{ABRT} = 'DEFAULT';
sigprocmask(SIG_UNBLOCK, POSIX::SigSet->new(SIGTERM, SIGINT, SIGABRT))
or Fatal("Can't unblock stop signals: $!");
# Force reconnection to the db. $dbh got copied, but isn't really valid anymore.
$dbh = zmDbConnect(1);
logReinit();
dPrint(ZoneMinder::Logger::INFO, "'$command' started at "
.strftime('%y/%m/%d %H:%M:%S', localtime())
."\n"
);
if ( $daemon =~ /^$daemon_patt$/ ) {
$daemon = $Config{ZM_PATH_BIN}.'/'.$1;
} else {
Fatal("Invalid daemon '$daemon' specified");
}
my @good_args;
foreach my $arg ( @args ) {
# Detaint arguments, if they look ok
if ( $arg =~ /^(-{0,2}[\w\/?&=.-]+)$/ ) {
push @good_args, $1;
} else {
Fatal("Bogus argument '$arg' found");
}
}
logTerm();
zmDbDisconnect();
# leave stdin,stdout,stderr open. Closing them causes problems with libx264
# Close only the descriptors that are open where /proc lists them: walking
# every number up to _SC_OPEN_MAX takes seconds with a high open-file limit.
if ( opendir(my $fd_dir, '/proc/self/fd') ) {
my @fds = map { /^(\d+)$/ ? $1 : () } readdir($fd_dir);
closedir($fd_dir);
foreach my $fd ( @fds ) {
POSIX::close($fd) if $fd > 2;
}
} else {
my $fd = 3;
my $max = POSIX::sysconf(&POSIX::_SC_OPEN_MAX);
while ( $fd < $max ) {
POSIX::close($fd++);
}
}
$SIG{CHLD} = 'DEFAULT';
$SIG{HUP} = 'DEFAULT';
# SIGCHLD is still blocked from the fork above, and a blocked signal stays
# blocked across exec, so the daemon would start with it blocked. Hand it the
# mask zmdc had before the fork.
sigprocmask(SIG_SETMASK, $sigset) or Fatal("Can't restore the signal mask: $!");
exec($daemon, @good_args) or Fatal("Can't exec: $!");
} else {
Fatal("Can't fork: $!");
}
} # end sub start
# Sends the stop signal, without waiting around to see if the process died.
sub send_stop {
my ( $final, $process ) = @_;
my $sigset = POSIX::SigSet->new;
my $blockset = POSIX::SigSet->new(SIGCHLD);
sigprocmask(SIG_BLOCK, $blockset, $sigset) or Fatal("Can't block SIGCHLD: $!");
my $command = $process->{command};
if ( $process->{pending} ) {
delete $cmd_hash{$command};
dPrint(ZoneMinder::Logger::INFO, "Command '$command' removed from pending list at "
.strftime('%y/%m/%d %H:%M:%S', localtime())
."\n"
);
sigprocmask(SIG_UNBLOCK, $blockset) or Fatal("Can't unblock SIGCHLD: $!");
return ();
}
my $pid = $process->{pid};
if ( !$pid ) {
dPrint(ZoneMinder::Logger::ERROR, "No process with command of '$command' is running\n");
sigprocmask(SIG_UNBLOCK, $blockset) or Fatal("Can't unblock SIGCHLD: $!");
return();
}
if ( !$pid_hash{$pid} ) {
dPrint(ZoneMinder::Logger::ERROR, "No process with command of '$command' pid $pid is running\n");
sigprocmask(SIG_UNBLOCK, $blockset) or Fatal("Can't unblock SIGCHLD: $!");
return();
}
dPrint(ZoneMinder::Logger::INFO, "'$command' sending stop to pid $pid at "
.strftime('%y/%m/%d %H:%M:%S', localtime())
."\n"
);
$process->{keepalive} = !$final;
$process->{term_sent_at} = time; # Always update to reset the KILL_DELAY countdown
$process->{pending} = 0;
$terminating_processes{$command} = $process;
kill('TERM', $pid);
sigprocmask(SIG_UNBLOCK, $blockset) or Fatal("Can't unblock SIGCHLD: $!");
return $pid;
} # end sub send_stop
sub check_for_processes_to_kill {
# Turn off SIGCHLD
my $sigset = POSIX::SigSet->new;
my $blockset = POSIX::SigSet->new(SIGCHLD);
sigprocmask(SIG_BLOCK, $blockset, $sigset) or Fatal("Can't block SIGCHLD: $!");
foreach my $command ( keys %terminating_processes ) {
my $process = $cmd_hash{$command};
if ( ! $process ) {
Debug("No process found for $command");
delete $terminating_processes{$command};
next;
}
if ( ! $$process{pid} ) {
Warning("Have no pid for $command.");
delete $terminating_processes{$command};
next;
}
my $now = time;
Debug("Have process $command at pid $$process{pid} $now - $$process{term_sent_at} = " . ( $now - $$process{term_sent_at} ));
if ( $$process{term_sent_at} and ( $now - $$process{term_sent_at} > KILL_DELAY ) ) {
dPrint(ZoneMinder::Logger::WARNING, "'$$process{command}' has not stopped at "
.strftime('%y/%m/%d %H:%M:%S', localtime())
.' after ' . KILL_DELAY . ' seconds.'
." Sending KILL to pid $$process{pid}\n"
);
kill('KILL', $$process{pid});
delete $terminating_processes{$command};
}
}
sigprocmask(SIG_UNBLOCK, $blockset) or Fatal("Can't unblock SIGCHLD: $!");
} # end sub check_for_processes_to_kill
sub stop {
my ( $daemon, @args ) = @_;
my $command = join(' ', $daemon, @args);
my $process = $cmd_hash{$command};
if ( !$process ) {
dPrint(ZoneMinder::Logger::WARNING, "Can't find process with command of '$command'");
return;
}
send_stop(1, $process);
}
# restart is the same as stop, except that we flag the processes for restarting once it dies
# One difference is that if we don't know about the process, then we start it.
sub restart {
my ( $daemon, @args ) = @_;
my $command = join(' ', $daemon, @args);
dPrint(ZoneMinder::Logger::DEBUG, "Restarting $command\n");
my $process = $cmd_hash{$command};
if ( !$process ) {
dPrint(ZoneMinder::Logger::WARNING, "Can't find process with command of '$command'\n");
start($daemon, @args);
return;
}
# If already terminating with keepalive set, restart is already scheduled
if ( $terminating_processes{$command} and $process->{keepalive} ) {
dPrint(ZoneMinder::Logger::DEBUG, "'$command' is already terminating and will restart, ignoring duplicate restart request\n");
return;
}
# Start will be handled by the reaper...
# unless it was already pending in which case send_stop will return () so we should start it
if ( !send_stop(0, $process) ) {
dPrint(ZoneMinder::Logger::DEBUG, "!send_stop so starting '$command'\n");
start($daemon, @args);
}
return;
}
sub reload {
my $daemon = shift;
my @args = @_;
my $command = join(' ', $daemon, @args);
my $process = $cmd_hash{$command};
if ( $process ) {
if ( $process->{pid} ) {
kill('HUP', $process->{pid});
}
}
}
sub logrot {
logReinit();
foreach my $process ( values %pid_hash ) {
if ( $process->{pid} ) {
Debug("Rotating logs of $$process{command} at $$process{pid}");
# WINCH, not HUP: HUP means reload, which makes zmc drop its camera
# connection and the perl daemons exit for zmdc to restart. Rotating logs
# nightly must not interrupt capture. See issue #5063.
kill('WINCH', $process->{pid});
} else {
Debug("Not rotating logs of $$process{command}");
}
}
}
sub shutdown_sig_handler {
$zm_terminate = 1;
}
sub chld_sig_handler {
my $saved_status = $!;
# Wait for a child to terminate
while ( (my $cpid = waitpid(-1, WNOHANG)) > 0 ) {
$pids_to_reap{$cpid} = { status=>$?, stopped=>time() };
} # end while waitpid
$SIG{CHLD} = \&chld_sig_handler;
$! = $saved_status;
}
sub reaper {
foreach my $cpid ( keys %pids_to_reap ) {
my $process = $pid_hash{$cpid};
delete $pid_hash{$cpid};
my $reap_info = $pids_to_reap{$cpid};
my ( $status, $stopped ) = @$reap_info{'status','stopped'};
delete $pids_to_reap{$cpid};
if ( !$process ) {
dPrint(ZoneMinder::Logger::INFO, "Can't find child with pid of '$cpid'\n");
next;
}
delete $terminating_processes{$$process{command}};
delete $$process{term_sent_at};
$process->{stopped} = $stopped;
$process->{runtime} = ($process->{stopped}-$process->{started});
delete $process->{pid};
my $exit_status = $status>>8;
# The low 7 bits of a wait status are the signal and 0x80 is the core dump
# flag. Masking with 0xfe dropped the signal's low bit, so TERM was logged
# as 14 and KILL as 8.
my $exit_signal = $status&0x7f;
my $core_dumped = $status&0x80;
my $out_str = "'$process->{command}' ";
if ( $exit_signal ) {
# 15 == TERM, 14 == ALARM
if ( $exit_signal == 15 || $exit_signal == 14 ) {
$out_str .= 'exited';
} else {
$out_str .= 'crashed';
}
$out_str .= ", signal $exit_signal";
} else {
$out_str .= 'exited ';
if ( $exit_status ) {
$out_str .= "abnormally, exit status $exit_status";
} else {
$out_str .= 'normally';
}
}
#print( ", core dumped" ) if ( $core_dumped );
$out_str .= "\n";
if ( $exit_status == 0 ) {
Info($out_str);
} else {
Error($out_str);
}
if ( $process->{keepalive} ) {
# Schedule for immediate restart
$cmd_hash{$process->{command}} = $process;
if ( !$process->{delay} || ($process->{runtime} > $Config{ZM_MAX_RESTART_DELAY} ) ) {
#start( $process->{daemon}, @{$process->{args}} );
$process->{pending} = $process->{stopped};
$process->{delay} = 5;
} else {
$process->{pending} = $process->{stopped}+$process->{delay};
$process->{delay} *= 2;
# Limit the start delay to 15 minutes max
if ( $process->{delay} > $Config{ZM_MAX_RESTART_DELAY} ) {
$process->{delay} = $Config{ZM_MAX_RESTART_DELAY};
}
}
#Debug("Delay for $$process{command} is now $$process{delay}");
} else {
delete $cmd_hash{$$process{command}};
}
} # end foreach pid_to_reap
} # end sub reaper
sub restartPending {
# Restart any pending processes, we list them first because cmd_hash may change in foreach
my @processes = values %cmd_hash;
foreach my $process ( @processes ) {
if ( $process->{pending} && $process->{pending} <= time() ) {
dPrint(ZoneMinder::Logger::INFO, "Starting pending process, $process->{command}\n");
start($process->{daemon}, @{$process->{args}});
}
}
}
sub shutdownAll {
foreach my $pid ( keys %pid_hash ) {
# This is a quick fix because a SIGCHLD can happen and alter pid_hash while we are in here.
next if ! $pid_hash{$pid};
send_stop(1, $pid_hash{$pid});
}
while ( keys %terminating_processes ) {
reaper() if %pids_to_reap;
check_for_processes_to_kill();
if ( %terminating_processes ) {
Debug("Still " . scalar(keys %terminating_processes) . ' to die. sleeping');
sleep(1);
}
}
dPrint(ZoneMinder::Logger::INFO, 'Server shutdown at '
.strftime('%y/%m/%d %H:%M:%S', localtime())
."\n"
);
unlink(main::SOCK_FILE) or Error("Unable to unlink " . main::SOCK_FILE .". Error message was: $!") if ( -e main::SOCK_FILE );
unlink(ZM_PID) or Error("Unable to unlink " . ZM_PID .". Error message was: $!") if ( -e ZM_PID );
close(CLIENT);
close(SERVER);
exit();
}
sub check {
my $daemon = shift;
my @args = @_;
my $command = join(' ', $daemon, @args);
my $process = $cmd_hash{$command};
if ( !$process ) {
cPrint("unknown\n");
} elsif ( $process->{pending} ) {
cPrint("pending\n");
} else {
my $cpid = $process->{pid};
if ( ! $pid_hash{$cpid} ) {
cPrint("stopped\n");
} else {
cPrint("running\n");
}
}
}
sub status {
my $daemon = shift;
if (defined($daemon)) {
my $command = join(' ', $daemon, @_);
my $process = $cmd_hash{$command};
if (!$process) {
dPrint(ZoneMinder::Logger::DEBUG, "'$command' not running\n");
return;
}
if ($process->{pending}) {
dPrint(ZoneMinder::Logger::DEBUG, "'$command' pending at "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{pending}))
."\n"
);
return;
} else {
my $pid = $process->{pid};
if (!$pid_hash{$pid}) {
dPrint(ZoneMinder::Logger::DEBUG, "'$command' not running\n");
return;
}
}
dPrint(ZoneMinder::Logger::DEBUG, "'$command' running since "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{started}))
.", pid = $process->{pid}"
);
} else {
foreach my $pid (sort { $a <=> $b } keys %pid_hash) {
my $process = $pid_hash{$pid};
my $out_str = "'$process->{command}' running since "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{started}))
.", pid = $process->{pid}";
$out_str .= ", valid" if kill(0, $process->{pid});
$out_str .= "\n";
dPrint(ZoneMinder::Logger::DEBUG, $out_str);
}
foreach my $process (values %cmd_hash) {
if ($process->{pending}) {
dPrint(ZoneMinder::Logger::DEBUG, "'$process->{command}' pending at "
.strftime('%y/%m/%d %H:%M:%S', localtime($process->{pending}))
."\n"
);
}
} # end foreach process
}
} # end sub status
sub killAll {
my $delay = shift;
# Why sleep before sending term?
#sleep( $delay );
my $killall;
if ( '@HOST_OS@' eq 'BSD' ) {
$killall = 'killall -q -';
} elsif ( '@HOST_OS@' eq 'solaris' ) {
$killall = 'pkill -';
} else {
$killall = 'killall -q -s ';
}
foreach my $daemon ( @daemons ) {
my $cmd = $killall ."TERM $daemon";
Debug($cmd);
qx($cmd);
}
sleep($delay);
foreach my $daemon ( @daemons ) {
my $cmd = $killall."KILL $daemon";
Debug($cmd);
qx($cmd);
}
}
1;
__END__