#!/usr/bin/perl -wT # # 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. # # ========================================================================== # # User config # # ========================================================================== use constant DAEMON_PATH => '@prefix@/bin/'; use constant DC_SOCK_FILE => '/tmp/zmdc.sock'; # ========================================================================== # # Don't change anything from here on down # # ========================================================================== use strict; use POSIX; use Socket; use IO::Handle; use Data::Dumper; $| = 1; my @daemons = ( 'zmc', 'zma', 'zmf.pl' ); my $command = shift @ARGV; die( "No command given" ) unless( $command ); my $needs_daemon = $command !~ /(?:shutdown|check)/; my $daemon = shift( @ARGV ); die( "No daemon given" ) unless( !$needs_daemon || $daemon ); my @args; my $daemon_patt = '('.join( '|', @daemons ).')'; if ( $needs_daemon ) { if ( $daemon =~ /^${daemon_patt}$/ ) { $daemon = $1; } else { die( "Invalid daemon '$daemon' specified" ); } } foreach my $arg ( @ARGV ) { # Detaint arguments, if they look ok if ( $arg =~ /^(-?[\w\d]+)/ ) { push( @args, $1 ); } else { die( "Bogus argument '$arg' found" ); } } socket( CLIENT, PF_UNIX, SOCK_STREAM, 0 ) or die( "Can't open socket: $!" ); my $saddr = sockaddr_un( DC_SOCK_FILE ); if ( !connect( CLIENT, $saddr ) ) { # The server isn't there print( "Unable to connect, starting server\n" ); close( CLIENT ); if ( my $cpid = fork() ) { # Parent process just sleep and fall through sleep( 2 ); socket( CLIENT, PF_UNIX, SOCK_STREAM, 0 ) or die( "Can't open socket: $!" ); connect( CLIENT, $saddr ) or die( "Can't connect: $!" ); } elsif ( defined($cpid) ) { setpgrp(); dprint( "Server starting at ".strftime( '%y/%m/%d %H:%M:%S', localtime() )."\n" ); socket( SERVER, PF_UNIX, SOCK_STREAM, 0 ) or die( "Can't open socket: $!" ); unlink( DC_SOCK_FILE ); bind( SERVER, $saddr ) or die( "Can't bind: $!" ); listen( SERVER, SOMAXCONN ) or die( "Can't listen: $!" ); $SIG{CHLD} = \&reaper; $SIG{INT} = \&shutdown_all; $SIG{TERM} = \&shutdown_all; $SIG{ABRT} = \&shutdown_all; $SIG{HUP} = \✓ ( $ENV{PATH} ) = ( $ENV{PATH} =~ /^(.*)$/ ); my %cmd_hash; my %pid_hash; sub dprint { if ( fileno(CLIENT) ) { print CLIENT @_ } else { print @_; } } sub start { my $daemon = shift; my @args = @_; my $command = join( ' ', ( $daemon, @args ) ); my $process = $cmd_hash{$command}; if ( !$process ) { $process = { daemon=>$daemon, args=>\@args, command=>$command, keepalive=>!undef }; } elsif ( $process->{pid} && $pid_hash{$process->{pid}} ) { dprint( "'$process->{command}' already running at ".strftime( '%y/%m/%d %H:%M:%S', localtime( $process->{started}) ).", pid = $process->{pid}\n" ); return(); } if ( my $cpid = fork() ) { $process->{pid} = $cpid; $process->{started} = time(); delete( $process->{pending} ); dprint( "'$command' starting at ".strftime( '%y/%m/%d %H:%M:%S', localtime( $process->{started}) ).", pid = $process->{pid}\n" ); $cmd_hash{$process->{command}} = $pid_hash{$cpid} = $process; } elsif ( defined($cpid ) ) { # Child process $SIG{CHLD} = 'DEFAULT'; $SIG{INT} = 'DEFAULT'; $SIG{TERM} = 'DEFAULT'; $SIG{ABRT} = 'DEFAULT'; $SIG{HUP} = 'DEFAULT'; dprint( "'".join( ' ', ( $daemon, @args ) )."' started at ".strftime( '%y/%m/%d %H:%M:%S', localtime() )."\n" ); if ( $daemon =~ /^${daemon_patt}$/ ) { $daemon = DAEMON_PATH.$1; } else { die( "Invalid daemon '$daemon' specified" ); } exec( $daemon, @args ) or die( "Can't exec: $!" ); } else { die( "Can't fork: $!" ); } } sub _stop { my $final = shift; my $daemon = shift; my @args = @_; my $command = "$daemon ".join( ' ', @args ); my $process = $cmd_hash{$command}; if ( !$process ) { dprint( "Can't find process with command of '$command'\n" ); return(); } if ( !$pid_hash{$process->{pid}} ) { dprint( "No process with command of '$command' is running\n" ); return(); } print( "'$daemon ".join( ' ', @args )."' stopping at ".strftime( '%y/%m/%d %H:%M:%S', localtime() )."\n" ); $process->{keepalive} = !$final; kill( 'TERM', $process->{pid} ); delete( $cmd_hash{$command} ); } sub stop { _stop( 1, @_ ); } sub restart { _stop( 0, @_ ); } sub reaper { my $cpid = wait; $SIG{CHLD} = \&reaper; my $process = $pid_hash{$cpid}; delete( $pid_hash{$cpid} ); die( "Can't find child with pid of '$cpid'\n" ) unless( $process ); $process->{stopped} = time(); $process->{runtime} = ($process->{stopped}-$process->{started}); delete( $process->{pid} ); my $exit_status = $?>>8; my $exit_signal = $?&0xfe; my $core_dumped = $?&0x01; if ( $exit_status == 0 ) { print( "'$process->{daemon} ".join( ' ', @{$process->{args}} )."' died at ".strftime( '%y/%m/%d %H:%M:%S', localtime( $process->{stopped} ) ) ); } else { print( "'$process->{daemon} ".join( ' ', @{$process->{args}} )."' crashed at ".strftime( '%y/%m/%d %H:%M:%S', localtime( $process->{stopped} ) ) ); } print( ", exit status $exit_status" ) if ( $exit_status ); print( ", signal $exit_signal" ) if ( $exit_signal ); print( ", core dumped" ) if ( $core_dumped ); print( "\n" ); if ( $process->{keepalive} ) { if ( !$process->{delay} || $process->{runtime} > $process->{delay} ) { start( $process->{daemon}, @{$process->{args}} ); $process->{delay} = 5; } else { $process->{pending} = $process->{stopped}+$process->{delay}; $process->{delay} *= 2; } } } sub shutdown_all() { foreach my $process ( values( %pid_hash ) ) { stop( $process->{daemon}, @{$process->{args}} ); } dprint( "Server shutdown at ".strftime( '%y/%m/%d %H:%M:%S', localtime() )."\n" ); unlink( DC_SOCK_FILE ); close( CLIENT ); close( SERVER ); exit(); } sub check { foreach my $process ( values(%pid_hash) ) { dprint( "'$process->{command}' running at ".strftime( '%y/%m/%d %H:%M:%S', localtime( $process->{started}) ).", pid = $process->{pid}" ); dprint( ", valid" ) if ( kill( 0, $process->{pid} ) ); dprint( "\n" ); } } my $rin = ''; vec( $rin, fileno(SERVER),1) = 1; my $win = $rin; my $ein = $win; my $timeout = 1; #my ( $nfound, $timeleft) = select( $rin, $win, $ein, $timeout ); #print( "F:".fileno(SERVER)."\n" ); while( 1 ) { my $nfound = select( my $rout = $rin, undef, my $eout = $ein, $timeout ); #print( "Off select, NF:$nfound, ER:$!\n" ); #print( vec( $rout, fileno(SERVER),1)."\n" ); #print( vec( $eout, fileno(SERVER),1)."\n" ); #print( "C:".fileno(CLIENT)."S:".fileno(SERVER)."\n" ); if ( $nfound > 0 ) { if ( vec( $rout, fileno(SERVER),1) ) { my $paddr = accept( CLIENT, SERVER ); my $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 'shutdown' ) { shutdown_all(); } elsif ( $command eq 'check' ) { check(); } else { dprint( "Invalid command '$command'\n" ); } close( CLIENT ); } else { die( "Bogus descriptor" ); } } elsif ( $nfound < 0 ) { if ( $! == EINTR ) { # Dead child, will be reaped #print( "Probable dead child\n" ); } else { die( "Can't select: $!" ); } } else { #print( "Select timed out\n" ); foreach my $process ( values( %cmd_hash ) ) { if ( $process->{pending} && $process->{pending} <= time() ) { dprint( "Starting pending process, $process->{command}\n" ); start( $process->{daemon}, @{$process->{args}} ); } } } } dprint( "Server exiting at ".strftime( '%y/%m/%d %H:%M:%S', localtime() )."\n" ); exit(); } else { die( "Can't fork: $!" ); } } # The server is there, connect to it #print( "Writing commands\n" ); CLIENT->autoflush(); my $message = "$command"; $message .= ";$daemon" if ( $daemon ); $message .= ";".join( ';', @args ) if ( @args ); print( CLIENT $message ); shutdown( CLIENT, 1 ); while ( my $line = ) { chomp( $line ); print( "$line\n" ); } close( CLIENT ); #print( "Finished writing, bye\n" );