#!/usr/bin/perl
#
#  - Check for device to appear
#  - While device exists, keep n2kd running. If it quits, restart it.
#  - Also keep n2k.php -monitor running. If it quits, restart it.
#  - Stop n2kd if device disappears.
#
# This assumes there is a configuration file /etc/default/n2kd
# containing one or more of the following configuration settings:
#
#       INTERFACE_PROGRAM="ikonvert-serial"
#       INTERFACE_DEVICE="/dev/ikonvert"
#       INTERFACE_OPTIONS="--rate-limit-off -p"
#       ANALYZER_OPTIONS=
#       N2KD_OPTIONS=
#
# Obsolete options:
#       CAN_INTERFACE=can0
#       ACTISENSE_PRIMARY=/dev/actisense-1
#       ACTISENSE_SECONDARY=/dev/actisense-2
#       MONITOR=false
#
# Leave out ACTISENSE_SECONDARY if you have only one Actisense gateway.
# Leave MONITOR set to false for now; its contents have not been open sourced yet.
#
# (C) 2009-2023, Kees Verruijt, Harlingen, The Netherlands.
#  
# This file is part of CANboat.
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# 

use Config::General;

my $configFile = '/etc/default/n2kd';
my $configObject = Config::General->new(-ConfigFile => $configFile, -MergeDuplicateOptions => 1,);

die "Could not read config from $configFile!\n" unless ref $configObject;

my %config = $configObject->getall();

my $INTERFACE_PROGRAM = $config{'INTERFACE_PROGRAM'};
my $INTERFACE_DEVICE = $config{'INTERFACE_DEVICE'};
my $CAN_INTERFACE = $config{'CAN_INTERFACE'};
my $ACTISENSE_PRIMARY = $config{'ACTISENSE_PRIMARY'};
my $ACTISENSE_SECONDARY = $config{'ACTISENSE_SECONDARY'};
my $MONITOR = $config{'MONITOR'};
my $INTERFACE_OPTIONS = $config{'INTERFACE_OPTIONS'};
my $N2KD_OPTIONS = $config{'N2KD_OPTIONS'};
my $ANALYZER_OPTIONS = $config{'ANALYZER_OPTIONS'};

my $CAN = 0;

# Convert old options to new
if ($ACTISENSE_PRIMARY)
{
  $INTERFACE_PROGRAM='actisense-serial';
  $INTERFACE_DEVICE=$ACTISENSE_PRIMARY;
  if ($ACTISENSE_SECONDARY)
  {
    $INTERFACE_OPTIONS .= "| actisense-serial $ACTISENSE_SECONDARY";
  }
}
if ($CAN_INTERFACE)
{
  $INTERFACE_PROGRAM='candump';
  $INTERFACE_DEVICE=$CAN_INTERFACE;
  $INTERFACE_OPTIONS .= " | candump2analyzer";
}
elsif ($INTERFACE_DEVICE =~ /^can/)
{
  $CAN=1;
}
die "Configuration file $configFile incomplete: No INTERFACE_PROGRAM" unless ($INTERFACE_PROGRAM);

my $BASH_COMMAND = "$INTERFACE_PROGRAM $INTERFACE_DEVICE $INTERFACE_OPTIONS | analyzer $ANALYZER_OPTIONS -json -nv | n2kd $N2KD_OPTIONS";

my $LOGFILE = '/var/log/n2kd_monitor.log';
my $N2KD_LOGFILE = '/var/log/n2kd.log';
my $MONITOR_LOGFILE = '/var/log/n2k-status.log';

my $stat;
my $n2kd;
my $monitor;
my $child;
my $stop = 0;
my $last_monitor = 0;

use POSIX();
if ($MONITOR ne "true" && $MONITOR ne "yes")
{
  # Disable the monitoring part. This is not open source yet, so disable it by default.
  $last_monitor = POSIX::LONG_MAX;
}


sub logText($)
{
  my ($t) = @_;

  print STDERR POSIX::strftime('%Y-%m-%d %T: ', localtime) . $t . "\n";
}

sub daemonize()
{
  chdir '/';
  open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  open STDOUT, '>>', $LOGFILE or die "Can't write $LOGFILE: $!";
  defined(my $pid = fork) or die "Can't fork: $!";
  exit if $pid;
  die "Can't start a new session: $!" if setsid == -1;
  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
}

sub sigHandler()
{
  logText("Got signal to quit.\n");
  $stop = 1;
}

# checks if CAN_INTERFACE network interface is configured and up
# if not true, it configures and brings it up
# return undef, if it could not bring the interfac up and 1 on success
sub manageCanInterface($)
{
  # disable experimental warnings (smartmatch), because
  # the messages lead to failing n2kd_monitor service
  no if ($] >= 5.018), 'warnings' => 'experimental';

  my $CAN_INTERFACE = shift;

  my @result = `ip a show $CAN_INTERFACE up`;
  if(scalar(@result) < 1 or !(/$CAN_INTERFACE/ ~~ @result))
  {
    @result = `ip a show $CAN_INTERFACE`;
    if(scalar(@result) < 1 or !(/$CAN_INTERFACE/ ~~ @result))
    {
      if(system("ip link add $CAN_INTERFACE type can bitrate 250000") == 0)
      {
        logText("Added $CAN_INTERFACE network interface\n");
      }
      else
      {
        logText("Failed to add $CAN_INTERFACE\n");
        return undef;
      }
    }
    @result = `ip link set $CAN_INTERFACE up type can bitrate 250000`;
    if(scalar(@result) != 0)
    {
      logText("Failed to bring $CAN_INTERFACE up\n");
      return undef;
    }
  }
  logText("$CAN_INTERFACE up");
  return 1;
}

if ($#ARGV >= 0 && $ARGV[0] eq "-fg")
{
  logText("Starting n2kd_monitor service");
}
else
{
  logText("Starting n2kd monitor.");
  daemonize();
}

$SIG{'INT'} = 'sigHandler';
$SIG{'HUP'} = 'sigHandler';

if ($CAN)
{
  logText("Using socket can interface $INTERFACE_DEVICE");
  if(!manageCanInterface($INTERFACE_DEVICE))
  {
    die "Could not add or bring $INTERFACE_DEVICE up\n";
  }
}

if (!$CAN and !stat($INTERFACE_DEVICE))
{
  logText("Waiting for $INTERFACE_DEVICE to appear.");
}

if (!pipe(PIPEREAD, PIPEWRITE))
{
  die "Cannot create pipes\n";
}

for (;;)
{
  while (($child = POSIX::waitpid(-1, POSIX::WNOHANG)) > 0)
  {
    if ($child == $n2kd)
    {
      logText "N2KD monitor port daemon $child finished.";
      $n2kd = undef;
    }
    elsif ($child == $monitor)
    {
      $monitor = undef;
    }
  }

  if ($stop == 0 and ($CAN or stat($INTERFACE_DEVICE)))
  {
    if (!$CAN and !$stat)
    {
      logText("Hardware device $INTERFACE_DEVICE found.");
      $stat = 1;
    }
    if (!$n2kd)
    {
      if (($n2kd = fork()) == 0)
      {
        open STDIN, '<&PIPEREAD' or die "Can't read PIPEREAD: $!";
        open STDOUT, '>&PIPEWRITE' or die "Can't reassign PIPEWRITE: $!";
        open STDERR, '>>', $N2KD_LOGFILE or die "Can't write to $N2KD_LOGFILE $!";
        $ENV{'PATH'} = '/usr/local/bin:/bin:/usr/bin';

        logText("Executing '$BASH_COMMAND'");
        exec '/bin/bash', '-c', $BASH_COMMAND;
      }
      elsif ($n2kd)
      {
        logText("Starting N2K daemon $n2kd.");
      }
      else
      {
        logText("Fork failed.");
      }
      sleep(5);
    }
    if (!$monitor && (time > $last_monitor + 30))
    {
      $last_monitor = time;
      if (($monitor = fork()) == 0)
      {
        open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
        open STDOUT, '>>', $MONITOR_LOGFILE or die "Can't write to $MONITOR_LOGFILE $!";
        open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
        exec 'php5', '/usr/local/bin/n2k.php', '-monitor';
	die "unable to exec monitor"
      }
      if (!$monitor)
      {
        logText("Fork monitor failed.");
      }
    }
  }
  else
  {
    if ($stop == 0 and !$CAN and $stat)
    {
      logText("Hardware device $ACTISENSE_PRIMARY disappeared.");
      $stat = undef;
    }
    if ($n2kd)
    {
      logText("Requesting stop for N2K port daemon $n2kd.");
      kill 2, $n2kd;
      system "killall -9 $INTERFACE_PROGRAM";
    }
    if ($stop)
    {
      exit(0);
    }
  }
  sleep(5);
}
