#!/usr/bin/perl -Tw
#
# Copyright (c) 2019 Clementine Computing LLC.
#
# This file is part of PopuFare.
#
# PopuFare is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PopuFare 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with PopuFare. If not, see .
#
require 5.002;
use strict;
use Socket;
use Switch;
use Carp;
use DBI;
use FileHandle;
use Fcntl;
use Compress::Zlib;
use Getopt::Long qw(:config no_ignore_case);
use POSIX;
use RideLogic;
my $database_path = 'DBI:mysql:busdb';
my $database_user = '';
my $database_pass = '';
my $bind_ip = '127.0.0.1';
my $bind_port = 7277;
my $DebugMode = 0;
# This function only executes the passed code reference if the global variable $DebugMode is non-zero.
# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
#
sub ifdebug(&@)
{
my ($cmd) = @_;
&$cmd() if($DebugMode);
}
sub ServerReply
{
my $client_query = $_[0];
chomp($client_query);
my $response = "";
my $hangup_flag=0;
#Turning this on will use FLUSH instead of ZFLUSH, which is much slower
my $do_legacy_flush = 0;
switch ($client_query)
{
case /^QUERY\t[0-9][0-9]*$/
{
my $sequence_number = $client_query;
$sequence_number =~ s/^QUERY\t//;
my $dbh = DBI->connect($database_path, $database_user, $database_pass)
or die "Couldn't connect to database: " . DBI->errstr;
#A query to check for the validity of the queried sequence number
my $seqcheck = $dbh->prepare('SELECT seq_num FROM active_rider_table WHERE seq_num = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
#Prepare to send records
my $sth = $dbh->prepare('SELECT deleted, seq_num, logical_card_id, mag_token, rfid_token, rule_name, rule_param FROM active_rider_table a1 WHERE seq_num = ' .
'(SELECT MAX(seq_num) FROM active_rider_table a2 WHERE a1.logical_card_id= a2.logical_card_id) AND seq_num > ? ORDER BY seq_num ASC')
or die "Couldn't prepare statement: " . $dbh->errstr;
$seqcheck->execute($sequence_number);
#Check if the client is on the same page as us
#if not, tell them to flush everything and send it all again
my $flushdata = 0;
if ($sequence_number == 0)
{
$flushdata = 1;
$sth->execute($sequence_number) # Execute the query
or die "Couldn't execute statement: " . $sth->errstr;
}
elsif (!$seqcheck->fetchrow_array())
{
$sth->execute(0) # Get everything
or die "Couldn't execute statement: " . $sth->errstr;
$flushdata = 1;
}
else
{
$sth->execute($sequence_number) # Execute the query
or die "Couldn't execute statement: " . $sth->errstr;
}
# Read the matching records and print them out
# $data[0] = deleted
# $data[1] = seq_num
# $data[2] = logical_card_id
# $data[3] = mag_token
# $data[4] = rfid_token
# $data[5] = rule_name
# $data[6] = rule_param
my @data ;
#If we are doing a flush
if($flushdata)
{
if($do_legacy_flush)
{
$response .= "FLUSH\n" if $flushdata;
while (@data = $sth->fetchrow_array())
{
if (!$data[0])
{
$data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
$data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
$data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
}
}
$response .= "FLUSHDONE\n" if $flushdata;
}
else
{
my $z = deflateInit( -Level => Z_BEST_COMPRESSION ) or die "Cannot create a deflation stream\n";
my $size = 0;
my $dat = "";
my ($zout, $stat);
my $cmpdat;
while (@data = $sth->fetchrow_array())
{
if (!$data[0])
{
$data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
$data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
$data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
$dat .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
}
}
($zout, $stat) = $z->deflate($dat);
$stat == Z_OK or die "deflation failed...";
$cmpdat = $zout;
($zout, $stat) = $z->flush();
$stat == Z_OK or die "deflation failed...";
$cmpdat .= $zout;
$size = $z->total_out();
$response .= "ZFLUSH\t$size\n";
$response .= $cmpdat;
$response .= "ZFLUSHDONE\n";
#Set the "HANG-UP" flag to make the server hang up on a client who has just done a ZFLUSH
#so that the client will start a fresh server session with its shiny new database
$hangup_flag = 1;
}
}
else
{
while (@data = $sth->fetchrow_array())
{
if ($data[0])
{
$response .= "DELETE\t$data[1]\t$data[2]\n";
} else
{
$data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
$data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
$data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
}
}
}
$seqcheck->finish;
$sth->finish;
$dbh->disconnect;
}
else
{
$response = "ERROR\n" . $client_query;
}
}
if($response eq "")
{
$response .= "NOP\n";
}
return ($response, $hangup_flag);
}
sub handle_client()
{
close SERVER;
CLIENT->autoflush(1);
my $linebuffer;
while($linebuffer = )
{
my ($reply, $hangup_flag) = ServerReply($linebuffer);
print CLIENT $reply;
if($hangup_flag)
{
sleep(60);
shutdown(CLIENT, 2);
close CLIENT;
return 0;
}
} #while data from client
close CLIENT;
}
my $waitedpid = 0;
my $sigreceived = 0;
sub REAPER
{
while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
$SIG{CHLD} = \&REAPER; # loathe sysV
$sigreceived = 1;
}
sub spawn
{
my $coderef = shift; #grab the first parameter
unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
{
confess "usage: spawn CODEREF"; #complain if this is not the case
}
my $pid;
if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
{
close CLIENT;
return; #failed to fork, we'd better close the client
}
elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
{
return; # i'm the parent
}
else #otherwise, if the returned process ID is 0, that means we're the child process
{
exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
#exit this (child) process when we've finished our conversation(s) with the
#other (client) end of the socket.
}
}
sub show_help_and_exit {
print "usage:\n";
print " [-i] interactive, do not daemonize\n";
print " [-c cfg] use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
print " [-h] show help (this screen)\n";
exit;
}
#----------------------------------------------------------------------
#
#----------------------------------------------------------------------
my $daemonize = 1;
my $interactive = 0;
my $show_help = 0;
my $cfg_file = $RideLogic::RIDELOGIC_DAEMON_CONF;
GetOptions(
'i|interactive' => \$interactive,
'c|config=s' => \$cfg_file,
'h|help' => \$show_help );
show_help_and_exit() if ($show_help);
$daemonize=0 if ($interactive);
#----------------------------------------------------------------------
# Local network settings for Inter-Process communication.
#----------------------------------------------------------------------
my $proto = getprotobyname('tcp');
my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
#----------------------------------------------------------------------
my $max_retries = 10; #Maximum number of address-binding retries before we give up.
my $retry_count = $max_retries; #number of retries left...
my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
my %CFG_VAR;
read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
my $logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_buspassd.log";
my $pidfile = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_buspassd.pid";
daemonize($logfile, $pidfile) if ($daemonize);
# set our pipes to be piping hot
$|=1;
while( ($retry_count > 0) && (!$got_network) )
{
try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
{
socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
bind (SERVER, $addr) || die "bind: $!";
listen(SERVER,5) || die "listen: $!";
$got_network = 1;
}
catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
{
my $errmsg = $_; #Remember the error message
close(SERVER); #Clean up the server socket if it needs it
#Decrement our remaining retry counter
$retry_count = $retry_count - 1;
#Log the message to our debug log
print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
#Wait a reasonable period before trying again
sleep $retry_delay;
};
}
if($got_network) #If we met with success binding to the network, report it
{
my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
print $logmsg;
}
else #If we ran out of patience and gave up, report that as well and exit
{
my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
die $errmsg;
}
# Set up our signal handler which will clean up defunct child processes and let the main
# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
$SIG{CHLD} = \&REAPER;
#This for loop is efficient, but confusting, so I'll break it down by clause
#
# The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
# accept() call was interrupted by a signal. This clause runs once before the first run of the loop
#
# The second clause is the test clause, it will process the contents of the loop if EITHER
# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
# socket, OR the signal received flag is set (thus accept would have returned early without
# having actually accepted a connection.
#
# The third clause (the 'incrementer') is run after each time the body is executed, before the
# test clause is executed again (deciding whether to run the body or drop out... This test
# clause will close the parent process' copy of the CLIENT file handle since (see body below)
# after the body executes, all communication with the socket referred to by that file handle
# will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
# file handle to be used again in the parent process for the next accepted incoming connection.
for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
{
next if $sigreceived; #If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
}