Files
zoneminder/scripts/zmx10.pl.in
T
Steve GilvarryandClaude Opus 5 ba49bbb33c fix: make the Perl scripts' taint-safe PATH configurable
The scripts run under -T, so they cannot trust the caller's PATH and set
their own. Sixteen of them hardcoded /bin:/usr/bin:/usr/local/bin, which
assumes everything they shell out to lives under /usr or /usr/local.

ZoneMinder::General::findDbCommand looks for a database client on that
PATH, and zmupdate.pl and zmcamtool.pl run what it finds. So an install
whose client sits anywhere else cannot apply schema changes:

  sh: mysql: command not found
  Command 'mysql -u'zmuser' ... ' exited with status: 127

even with the client on the caller's PATH. Homebrew on Apple Silicon is
the case that surfaced it - the client is in /opt/homebrew/bin - but a
--prefix=/opt install on Linux has the same shape, as does anything that
keeps its database client outside the FHS locations.

Replaced the literal with @ZM_SCRIPT_PATH@, defaulting to the same three
directories plus wherever cmake actually found a client, and overridable
for packagers who want to pin it. Warn at configure time when no client
is found at all, since that failure otherwise appears much later and
says something unrelated.

Nothing changes for an install whose client is already under /usr/bin:
the directory is only appended when it is not in the list, so the
default stays exactly as it was.

Memory.pm is deliberately left alone. It has a narrower PATH of
/bin:/usr/bin, and the only command it runs is uname through an absolute
path from ZM_PATH_UNAME, so it does not need widening.

Verified on macOS across the three cases: with the client in
/opt/homebrew/bin the directory is appended; -DZM_SCRIPT_PATH= is
respected verbatim; and pointing detection at /usr/bin/mariadb leaves
the default untouched. Build clean, suite 146 cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5KL9Xbi7K5aGsauLtd8tG
2026-09-13 13:04:59 +10:00

673 lines
19 KiB
Plaintext

#!@PERL_EXECUTABLE@ -wT
#
# ==========================================================================
#
# ZoneMinder X10 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
zmx10.pl - ZoneMinder X10 Control Script
=head1 SYNOPSIS
zmx10.pl -c <command>,--command=<command> [-u <unit code>,--unit-code=<unit code>]
=head1 DESCRIPTION
This script controls the monitoring of the X10 interface and the consequent
management of the ZM daemons based on the receipt of X10 signals.
=head1 OPTIONS
-c <command>, --command=<command> - Command to issue, one of 'on','off','dim','bright','start', 'status','shutdown'
-u <unit code>, --unit-code=<unit code> - Unit code to act on required for all commands
except 'status' (optional) and 'shutdown'
-v, --verison - Pirnts the currently installed version of ZoneMinder
=cut
use strict;
use bytes;
# ==========================================================================
#
# These are the elements you can edit to suit your installation
#
# ==========================================================================
use constant CAUSE_STRING => 'X10'; # What gets written as the cause of any events
# ==========================================================================
#
# Don't change anything below here
#
# ==========================================================================
@EXTRA_PERL_LIB@
use ZoneMinder;
use POSIX;
use Socket;
use Getopt::Long;
use autouse 'Pod::Usage'=>qw(pod2usage);
use autouse 'Data::Dumper'=>qw(Dumper);
use constant SOCK_FILE => $Config{ZM_PATH_SOCKS}.'/zmx10.sock';
$| = 1;
$ENV{PATH} = '@ZM_SCRIPT_PATH@';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
my $command;
my $unit_code;
my $version;
GetOptions(
'command=s' =>\$command,
'unit-code=i' =>\$unit_code,
'version' =>\$version
) or pod2usage(-exitstatus => -1);
if ( $version ) {
print ZoneMinder::Base::ZM_VERSION;
exit(0);
}
die 'No command given' unless $command;
die 'No unit code given'
unless( $unit_code || ($command =~ /(?:start|status|shutdown)/) );
if ( $command eq 'start' ) {
X10Server::runServer();
exit();
}
logInit();
logSetSignal();
my $zm_terminate = 0;
sub TermHandler {
Info('Received TERM, exiting');
$zm_terminate = 1;
}
$SIG{TERM} = \&TermHandler;
$SIG{INT} = \&TermHandler;
socket(CLIENT, PF_UNIX, SOCK_STREAM, 0)
or Fatal("Can't open socket: $!");
my $saddr = sockaddr_un(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);
logReinit();
socket(CLIENT, PF_UNIX, SOCK_STREAM, 0) or Fatal("Can't open socket: $!");
connect(CLIENT, $saddr) or Fatal("Can't connect: $!");
} elsif ( defined($cpid) ) {
setpgrp();
logReinit();
X10Server::runServer();
} else {
Fatal("Can't fork: $!");
}
}
# The server is there, connect to it
CLIENT->autoflush();
my $message = $command;
$message .= ';'.$unit_code if $unit_code;
print(CLIENT $message);
shutdown(CLIENT, 1);
while ( my $line = <CLIENT> ) {
chomp($line);
print("$line\n");
}
close(CLIENT);
exit;
#
# ==========================================================================
#
# This is the X10 Server package
#
# ==========================================================================
#
package X10Server;
use strict;
use bytes;
use ZoneMinder;
use POSIX;
use DBI;
use Socket;
use X10::ActiveHome;
use autouse 'Data::Dumper'=>qw(Dumper);
our $dbh;
our $x10;
our %monitor_hash;
our %device_hash;
our %pending_tasks;
sub runServer {
Info('X10 server starting');
socket(SERVER, PF_UNIX, SOCK_STREAM, 0) or Fatal("Can't open socket: $!");
unlink(main::SOCK_FILE);
my $saddr = sockaddr_un(main::SOCK_FILE);
bind(SERVER, $saddr) or Fatal("Can't bind: $!");
listen(SERVER, SOMAXCONN) or Fatal("Can't listen: $!");
$dbh = zmDbConnect();
$x10 = new X10::ActiveHome(
port=>$Config{ZM_X10_DEVICE},
house_code=>$Config{ZM_X10_HOUSE_CODE},
debug=>1
);
Fatal("Failed to open x10 device at $Config{ZM_X10_DEVICE}") if !$x10;
loadTasks();
$x10->register_listener(\&x10listen);
my $rin = '';
vec($rin, fileno(SERVER), 1) = 1;
vec($rin, $x10->select_fds(), 1) = 1;
my $timeout = 1.0; # WHy .2? Why not 1s?
Debug(2, 'F:'.fileno(SERVER)."\n" );
my $reload = undef;
my $reload_count = 0;
my $reload_limit = $Config{ZM_X10_DB_RELOAD_INTERVAL} / $timeout;
while ( !$zm_terminate ) {
Debug(2, "Selecting for $timeout");
my $nfound = select(my $rout = $rin, undef, undef, $timeout);
#print( "Off select, NF:$nfound, ER:$!\n" );
#print( vec( $rout, fileno(SERVER),1)."\n" );
#print( vec( $rout, $x10->select_fds(),1)."\n" );
if ( $nfound > 0 ) {
Debug(2, "Nfound $nfound");
if ( vec($rout, fileno(SERVER), 1) ) {
my $paddr = accept(CLIENT, SERVER);
my $message = <CLIENT>;
my ($command, $unit_code) = split(';', $message);
my $device;
if ( defined($unit_code) ) {
if ( $unit_code < 1 || $unit_code > 16 ) {
dPrint(ZoneMinder::Logger::ERROR, "Invalid unit code '$unit_code'\n");
next;
}
$device = $device_hash{$unit_code};
if ( !$device ) {
$device = $device_hash{$unit_code} = {
appliance=>$x10->Appliance(unit_code=>$unit_code),
status=>'unknown'
};
}
} # end if defined($unit_code)
my $result;
if ( $command eq 'on' ) {
$result = $device->{appliance}->on();
} elsif ( $command eq 'off' ) {
$result = $device->{appliance}->off();
}
#elsif ( $command eq 'dim' )
#{
#$result = $device->{appliance}->dim();
#}
#elsif ( $command eq 'bright' )
#{
#$result = $device->{appliance}->bright();
#}
elsif ( $command eq 'status' ) {
if ( $device ) {
dPrint(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n");
} else {
foreach my $unit_code ( sort( keys(%device_hash) ) ) {
my $device = $device_hash{$unit_code};
dPrint(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n");
}
}
} elsif ( $command eq 'shutdown' ) {
last;
} else {
dPrint(ZoneMinder::Logger::ERROR, "Invalid command '$command'\n");
}
if ( defined($result) ) {
# FIXME
if ( 1 || $result ) {
$device->{status} = uc($command);
dPrint(ZoneMinder::Logger::DEBUG, $device->{appliance}->address()." $command, ok\n");
#x10listen( new X10::Event( sprintf("%s %s", $device->{appliance}->address, uc($command) ) ) );
} else {
dPrint(ZoneMinder::Logger::ERROR, $device->{appliance}->address()." $command, failed\n");
}
} # end if defined result
close(CLIENT);
} elsif ( vec($rout, $x10->select_fds(), 1) ) {
$x10->handle_input();
} else {
Fatal('Bogus descriptor');
}
} elsif ( $nfound < 0 ) {
if ( $! != EINTR ) {
Fatal("Can't select: $!");
} else {
Debug(2, "Nfound $nfound $!");
}
} else {
Debug(2, "Select timed out");
# Check for state changes
foreach my $monitor_id ( sort(keys(%monitor_hash) ) ) {
my $monitor = $monitor_hash{$monitor_id};
my $state = zmGetMonitorState($monitor);
if ( !defined($state) ) {
$reload = !undef;
next;
}
if ( defined( $monitor->{LastState} ) ) {
my $task_list;
if ( ($state == STATE_ALARM || $state == STATE_ALERT)
&& ($monitor->{LastState} == STATE_IDLE )
) # Gone into alarm state
{
Debug("Applying ON_list for $monitor_id");
$task_list = $monitor->{ON_list};
} elsif ($state == STATE_IDLE && $monitor->{LastState} != STATE_IDLE) {
# Come out of alarm state
Debug("Applying OFF_list for $monitor_id");
$task_list = $monitor->{OFF_list};
}
if ( $task_list ) {
foreach my $task ( @$task_list ) {
processTask($task);
}
}
} # end if defined laststate
$monitor->{LastState} = $state;
} # end foreach monitor
# Check for pending tasks
my $now = time();
foreach my $activation_time ( sort(keys(%pending_tasks) ) ) {
last if ( $activation_time > $now );
my $pending_list = $pending_tasks{$activation_time};
foreach my $task ( @$pending_list ) {
processTask($task);
}
delete $pending_tasks{$activation_time};
}
if ( $reload or (++$reload_count >= $reload_limit) ) {
loadTasks();
$reload = undef;
$reload_count = 0;
}
}
}
Info("X10 server exiting");
close(SERVER);
exit();
}
sub addToDeviceList {
my $unit_code = shift;
my $event = shift;
my $monitor = shift;
my $function = shift;
my $limit = shift;
Debug("Adding to device list, uc:$unit_code, ev:$event, mo:"
.$monitor->{Id}.", fu:$function, li:$limit"
);
my $device = $device_hash{$unit_code};
if ( !$device ) {
$device = $device_hash{$unit_code} = {
appliance=>$x10->Appliance(unit_code=>$unit_code),
status=>'unknown'
};
}
my $task = {
type=>'device',
monitor=>$monitor,
address=>$device->{appliance}->address(),
function=>$function
};
if ( $limit ) {
$task->{limit} = $limit
}
my $task_list = $device->{$event.'_list'};
if ( !$task_list ) {
$task_list = $device->{$event.'_list'} = [];
}
push @$task_list, $task;
} # end sub addToDeviceList
sub addToMonitorList {
my $monitor = shift;
my $event = shift;
my $unit_code = shift;
my $function = shift;
my $limit = shift;
Debug("Adding to monitor list, uc:$unit_code, ev:$event, mo:".$monitor->{Id}
.", fu:$function, li:$limit"
);
my $device = $device_hash{$unit_code};
if ( !$device ) {
$device = $device_hash{$unit_code} = {
appliance=>$x10->Appliance(unit_code=>$unit_code),
status=>'unknown'
};
}
my $task = {
type=>'monitor',
device=>$device,
id=>$monitor->{Id},
function=>$function
};
if ( $limit ) {
$task->{limit} = $limit;
}
my $task_list = $monitor->{$event.'_list'};
if ( !$task_list ) {
$task_list = $monitor->{$event.'_list'} = [];
}
push @$task_list, $task;
} # end sub addToMonitorList
sub loadTasks {
%monitor_hash = ();
Debug('Loading tasks');
# Clear out all old device task lists
foreach my $unit_code ( sort keys(%device_hash) ) {
my $device = $device_hash{$unit_code};
$device->{ON_list} = [];
$device->{OFF_list} = [];
}
my $sql = 'SELECT M.*,T.* FROM Monitors as M
INNER JOIN TriggersX10 as T on (M.Id = T.MonitorId)
WHERE M.Capturing != \'None\'
AND find_IN_set(\'X10\', M.Triggers)';
my $sth = $dbh->prepare_cached( $sql )
or Fatal("Can't prepare '$sql': ".$dbh->errstr());
my $res = $sth->execute()
or Fatal("Can't execute: ".$sth->errstr());
while( my $monitor = $sth->fetchrow_hashref() ) {
# Check shared memory ok
if ( !zmMemVerify($monitor) ) {
zmMemInvalidate($monitor);
next;
}
$monitor_hash{$monitor->{Id}} = $monitor;
if ( $monitor->{Activation} ) {
Debug("$monitor->{Name} has active string '$monitor->{Activation}'");
foreach my $code_string ( split(',', $monitor->{Activation}) ) {
#Debug( "Code string: $code_string\n" );
my ( $invert, $unit_code, $modifier, $limit )
= ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
$limit = 0 if !$limit;
if ( $unit_code ) {
if ( !$modifier || $modifier eq '+' ) {
addToDeviceList( $unit_code,
'ON',
$monitor,
(!$invert ? 'start_active' : 'stop_active'),
$limit
);
}
if ( !$modifier || $modifier eq '-' ) {
addToDeviceList( $unit_code,
'OFF',
$monitor,
(!$invert ? 'stop_active' : 'start_active'),
$limit
);
}
} # end if unit_code
} # end foreach code_string
}
if ( $monitor->{AlarmInput} ) {
Debug("$monitor->{Name} has alarm input string '$monitor->{AlarmInput}'");
foreach my $code_string ( split(',', $monitor->{AlarmInput}) ) {
#Debug( "Code string: $code_string\n" );
my ( $invert, $unit_code, $modifier, $limit )
= ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
$limit = 0 if !$limit;
if ( $unit_code ) {
if ( !$modifier || $modifier eq '+' ) {
addToDeviceList( $unit_code,
'ON',
$monitor,
(!$invert ? 'start_alarm' : 'stop_alarm'),
$limit
);
}
if ( !$modifier || $modifier eq '-' ) {
addToDeviceList( $unit_code,
'OFF',
$monitor,
(!$invert ? 'stop_alarm' : 'start_alarm'),
$limit
);
}
} # end if unit_code
} # end foreach code_string
} # end if AlarmInput
if ( $monitor->{AlarmOutput} ) {
Debug("$monitor->{Name} has alarm output string '$monitor->{AlarmOutput}'");
foreach my $code_string ( split( ',', $monitor->{AlarmOutput} ) ) {
#Debug( "Code string: $code_string\n" );
my ( $invert, $unit_code, $modifier, $limit )
= ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
$limit = 0 if !$limit;
if ( $unit_code ) {
if ( !$modifier || $modifier eq '+' ) {
addToMonitorList( $monitor,
'ON',
$unit_code,
(!$invert ? 'on' : 'off'),
$limit
);
}
if ( !$modifier || $modifier eq '-' ) {
addToMonitorList( $monitor,
'OFF',
$unit_code,
(!$invert ? 'off' : 'on'),
$limit
);
}
} # end if unit_code
} # end foreach code_string
} # end if AlarmOutput
zmMemInvalidate($monitor);
}
} # end sub loadTasks
sub addPendingTask {
my $task = shift;
# Check whether we are just extending a previous pending task
# and remove it if it's there
foreach my $activation_time ( sort keys(%pending_tasks) ) {
my $pending_list = $pending_tasks{$activation_time};
my $new_pending_list = [];
foreach my $pending_task ( @$pending_list ) {
if ( $task->{type} ne $pending_task->{type} ) {
push( @$new_pending_list, $pending_task )
} elsif ( $task->{type} eq 'device' ) {
if (( $task->{monitor}->{Id} != $pending_task->{monitor}->{Id} )
|| ( $task->{function} ne $pending_task->{function} ))
{
push @$new_pending_list, $pending_task;
}
} elsif ( $task->{type} eq 'monitor' ) {
if (( $task->{device}->{appliance}->unit_code()
!= $pending_task->{device}->{appliance}->unit_code()
)
|| ( $task->{function} ne $pending_task->{function} )
) {
push @$new_pending_list, $pending_task;
}
} # end switch task->type
} # end foreach pending_task
if ( @$new_pending_list ) {
$pending_tasks{$activation_time} = $new_pending_list;
} else {
delete $pending_tasks{$activation_time};
}
} # end foreach activation_time
my $end_time = time() + $task->{limit};
my $pending_list = $pending_tasks{$end_time};
if ( !$pending_list ) {
$pending_list = $pending_tasks{$end_time} = [];
}
my $pending_task;
if ( $task->{type} eq 'device' ) {
$pending_task = {
type=>$task->{type},
monitor=>$task->{monitor},
function=>$task->{function}
};
$pending_task->{function} =~ s/start/stop/;
} elsif ( $task->{type} eq 'monitor' ) {
$pending_task = {
type=>$task->{type},
device=>$task->{device},
function=>$task->{function}
};
$pending_task->{function} =~ s/on/off/;
}
push @$pending_list, $pending_task;
} # end sub addPendingTask
sub processTask {
my $task = shift;
if ( $task->{type} eq 'device' ) {
my ( $instruction, $class ) = ( $task->{function} =~ /^(.+)_(.+)$/ );
if ( $class eq 'active' ) {
if ( $instruction eq 'start' ) {
zmMonitorEnable($task->{monitor});
if ( $task->{limit} ) {
addPendingTask($task);
}
} elsif( $instruction eq 'stop' ) {
zmMonitorDisable($task->{monitor});
}
} elsif( $class eq 'alarm' ) {
if ( $instruction eq 'start' ) {
zmTriggerEventOn(
$task->{monitor},
0,
main::CAUSE_STRING,
$task->{address}
);
if ( $task->{limit} ) {
addPendingTask($task);
}
} elsif( $instruction eq 'stop' ) {
zmTriggerEventCancel($task->{monitor});
}
} # end switch class
} elsif( $task->{type} eq 'monitor' ) {
if ( $task->{function} eq 'on' ) {
$task->{device}->{appliance}->on();
if ( $task->{limit} ) {
addPendingTask($task);
}
} elsif ( $task->{function} eq 'off' ) {
$task->{device}->{appliance}->off();
}
}
}
sub dPrint {
my $dbg_level = shift;
if ( fileno(CLIENT) ) {
print CLIENT @_
}
if ( $dbg_level == ZoneMinder::Logger::DEBUG ) {
Debug(@_);
} elsif ( $dbg_level == ZoneMinder::Logger::INFO ) {
Info(@_);
} elsif ( $dbg_level == ZoneMinder::Logger::WARNING ) {
Warning(@_);
}
elsif ( $dbg_level == ZoneMinder::Logger::ERROR ) {
Error( @_ );
} elsif ( $dbg_level == ZoneMinder::Logger::FATAL ) {
Fatal( @_ );
}
}
sub x10listen {
foreach my $event ( @_ ) {
#print( Data::Dumper( $_ )."\n" );
if ( $event->house_code() eq $Config{ZM_X10_HOUSE_CODE} ) {
my $unit_code = $event->unit_code();
my $device = $device_hash{$unit_code};
if ( !$device ) {
$device = $device_hash{$unit_code} = {
appliance=>$x10->Appliance(unit_code=>$unit_code),
status=>'unknown'
};
}
next if ( $event->func() !~ /(?:ON|OFF)/ );
$device->{status} = $event->func();
my $task_list = $device->{$event->func().'_list'};
if ( $task_list ) {
foreach my $task ( @$task_list ) {
processTask($task);
}
}
} else {
Debug("Not for out house code ".$event->house_code().' eq '.$Config{ZM_X10_HOUSE_CODE});
} # end if correct house code
Debug('Got event - '.$event->as_string());
}
} # end sub x10listen
1;
__END__