#!/usr/bin/perl -w
# #
# Software subject to following license(s):
#   Apache 2 License (http://www.opensource.org/licenses/apache2.0)
#   Copyright (c) Responsible Organization
#

# #
# Current developer(s):
#   Luis Fernando Muñoz Mejías <Luis.Munoz@UGent.be>
#

# #
# Author(s): Germán Cancio Meliá, Marco Emilio Poleggi
#

# #
# cdp-listend, 26.2.0, 1, Tue Apr 07 2026
#


use strict;
use warnings;

BEGIN {
  # use perl libs in /usr/lib/perl
  unshift(@INC, '/usr/lib/perl');
}


use IO::Socket;
use IO::Select;
use POSIX;
use Sys::Syslog qw( :DEFAULT setlogsock);
use AppConfig;
use Sys::Hostname;

#######################################################################
# configuration variables
#######################################################################

my $NULL = undef;
my $hostname = undef;
my $myaddr = undef;
my $my_ip_addr = undef;
my $port = undef;
my $s= undef;
my $pid = undef;
my $ip1 = undef;
my $r_ipaddr = undef;
my $r_host = undef;
my $newmsg = undef;
my $priority = undef;
my $msg = undef;
my $fetch = undef;
my $nch   = undef;
my $fetch_offset = undef;
my $fetch_smear = undef;
my $nch_smear = undef;
my $facility = undef;
my $pidfile = undef;
my $nofork = undef;

my $fetch_def  = "/usr/sbin/ccm-fetch";
my $nch_def    = "/usr/sbin/cdbsync-nch";
my $config_def = "/etc/cdp-listend.conf";
my $port_def   = 7777;
my $fetch_offset_def = 0;
my $fetch_smear_def  = 300;
my $nch_smear_def    = 0;
my $hostname_def = hostname();


my $config = AppConfig->new({
    PEDANTIC => 1,
    CASE   => 1,
});

$config->define("help|h!",
        "config|c=s",{DEFAULT=>$config_def},
        "port|p=i",{DEFAULT=>$port_def},
        "fetch|f=s",{DEFAULT=>$fetch_def},
        "nch|n=s",{DEFAULT=>$nch_def},
        "fetch_offset=i",{DEFAULT=>$fetch_offset_def},
        "fetch_smear=i",{DEFAULT=>$fetch_smear_def},
        "nch_smear=i",{DEFAULT=>$nch_smear_def},
        "facility=s", {DEFAULT=>"daemon"},
        "pidfile=s", {DEFAULT=>"/run/cdp-listend.pid"},
        "nofork", {DEFAULT=>0},
        "hostname=s", {DEFAULT=>$hostname_def},
         );
if (!($config->getopt()) || $config->get("help")) {
print "usage: $0 [OPTIONS]
  -h, --help              print this message
  -c, --config=FILE       config file location
  -p, --port=PORT         listen port number
  -f, --fetch=PROG        ccm-fetch program
  -n, --nch=PROG          nch program
  --fetch_offset=SECONDS  fetch run offset time
  --fetch_smear=SECONDS   fetch run smearing time
  --nch_smear=SECONDS     nch run smearing time
  --pidfile=FILE          write PID to FILE
  --nofork                do not fork into the background
  --hostname=fqdn         hostname/IP to listen on\n";
  exit(1);
}

logit("info", "Starting $0 with configuration file " . $config->config());

if (-f $config->config()) {
    $config->file($config->config());
}

$port = $config->port();
$fetch = $config->fetch();
$nch = $config->nch();
$fetch_offset = $config->fetch_offset();
$fetch_smear = $config->fetch_smear();
$nch_smear = $config->nch_smear();
$facility = $config->facility();
$pidfile = $config->pidfile();
$nofork = $config->nofork();
$hostname = $config->hostname();


$s = new IO::Select;

if ($hostname =~ m/^((localhost)+\.*(localdomain)*|127(\.\d+){3})/) {
    logit('err',"Exiting: hostname set to $hostname: $!");
    die "Hostname incorrectly set as $hostname\n";
} elsif ($hostname =~ m/^\d+(\.\d+){3}$/) {
    # cannot be a hostname, and IO::Socket::INET will fail if it's not valid
    $my_ip_addr = $hostname;
} else {
    my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($hostname);
    if(@addrs) {
        $myaddr = $addrs[0];
    } else {
        my $err_msg = "failed to retrieve addr from gethostbyname($hostname): ec $?";
        logit('err', "Exiting: $err_msg");
        die "$err_msg\n";
    }

    my @my_ip = unpack("C4", $myaddr);
    $my_ip_addr  = join(".", @my_ip);
}

unless ($ip1=IO::Socket::INET->new(LocalPort => $port, Proto=>'udp',
                                   LocalAddr => $my_ip_addr)) {
    logit('err',"Exiting: error creating UDP listener: $@");
    die "error creating UDP listener for $my_ip_addr  $@\n";
}
$s->add($ip1);


unless ($nofork) {
    $pid = fork;
    exit if $pid;
    if(!(defined($pid))) {
        logit('err',"Exiting: daemon couldn't fork: $!");
        die "Daemon couldn't fork: $!";
    }
    POSIX::setsid();
    exit if fork();
}

# Save the PID.
if ( $pidfile ) {
    if ( ! open(PIDFILE,">".$pidfile) ) {
        logit('err',"Cannot write PID to file \"".
             $pidfile."\": $! : Exiting");
        exit(-1);
    }
    print(PIDFILE "$$");
}

logit('info',"$0 daemon successfully started on port $port");

open(STDIN,  "+>/dev/null");
open(STDOUT, "+>&STDIN");
open(STDERR, "+>&STDIN");

#
# do an initial call of fetch and nch (if installed)
#
if (defined $fetch && -x $fetch) {
    logit('info', "Startup invocation of $fetch");
    system ("$fetch") == 0
        or logit('err',"[WARN] Startup invocation of ccm-fetch ($fetch) failed: $!");
}

if (defined $nch && -x $nch) {
    logit('info', "Startup invocation of $nch");
    system ("$nch") == 0
        or logit('err',"[WARN] Startup invocation of cdbsync-nch ($nch) failed: $!");
}

#
# now wait for notifications
#
while($ip1->recv($newmsg,1024)) {
    chomp($newmsg);
    ($NULL,$r_ipaddr) = sockaddr_in($ip1->peername);
    $r_host = inet_ntoa($r_ipaddr);
    $newmsg =~ /^(\w+).(\d+)$/;
    my $ntype = $1;
    my $time  = $2;
    logit('info',"Received UDP packet ($ntype|$time) from $r_host");
    if ($ntype eq "ccm") {
        my $delay = int(rand($fetch_smear)) + $fetch_offset;
        call_handler($fetch, "--profile-time=$time", $delay);
    } elsif ($ntype eq "cdb") {
        my $delay = int(rand($nch_smear));
        call_handler($nch, "", $delay);
    } else {
        logit('err', "[ERROR] Ignoring packet, unknown notification type");
    }
}

sub call_handler {
    my ($handler, $handler_args, $delay) = @_;
    logit('info', "$handler will be called in $delay seconds");
    sleep($delay);
    logit('info', "Calling $handler now");
    system ("$handler $handler_args") == 0 or logit('err',"[ERROR] call of $handler failed: $!");
}

sub logit {
    my ($priority, $msg) = @_;
    return 0 unless ($priority =~ /info|err|debug/);
    setlogsock('unix');
    eval {
        openlog($0, 'pid', $facility);
        syslog($priority, $msg);
    }; warn $@ if $@;
    closelog();
    return 1;
}


__END__

=head1 NAME

cdp-listend - the CDP notfication daemon. listens for UDP packets
and launches ccm-fetch or nch program accordingly to the type
of notification

=head1 DESCRIPTION

This program is a daemon which runs on the local machine waiting to
receive the CDP notification. On receiving a UDP packet containing the
notification type and UNIX time, ccm-fetch is called with the
profile update time as its only argument or nch.

=head1 SYNOPSIS

cdp-listend [OPTIONS]

  -h, --help              print this message
  -c, --config=FILE       config file location
  -p, --port=PORT         listen port number
  -f, --fetch=PROG        ccm-fetch program
  -n, --nch=PROG          nch program
  --fetch_offset=SECONDS   fetch run offset time
  --fetch_smear=SECONDS   fetch run smearing time
  --nch_smear=SECONDS     nch run smearing time\n";
  --facility              syslog facility
  --hostname=fqdn         hostname/IP to listen on

Once initiated the daemon forks to the background. Information and
error messages are written to syslog 'daemon' facility (or to the facility
defined in config file or command line

=head1 AUTHOR

Michael George
University of Liverpool

Piotr Poznanski
CERN

=cut
