ridelogic_billingd_using_api 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. #!/usr/bin/perl -Tw
  2. #
  3. # Copyright (c) 2019 Clementine Computing LLC.
  4. #
  5. # This file is part of PopuFare.
  6. #
  7. # PopuFare is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # PopuFare is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with PopuFare. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. require 5.002;
  21. use strict;
  22. use Socket;
  23. use Switch;
  24. use Carp;
  25. use FileHandle;
  26. use Fcntl;
  27. use Digest::MD5 qw(md5 md5_hex md5_base64);
  28. use Getopt::Long qw(:config no_ignore_case);
  29. use POSIX;
  30. use Time::Local;
  31. use Data::Dumper;
  32. use RideLogic;
  33. use RideLogicAPIQueryWrapper;
  34. my $PROGNAME = "ridelogic_billingd_using_api";
  35. my $HOST;
  36. my $DB;
  37. my $DBUSER;
  38. my $DBPASS;
  39. my $DSN;
  40. my $ORG = "ORG";
  41. my $bind_ip = '127.0.0.1';
  42. my $bind_port = 2455;
  43. my $billing_logfile;
  44. my $debug_logfile;
  45. my $REJECT_RULE = $ORG . "-REJECT";
  46. sub unix_to_readable_time {
  47. my $unix_time = shift;
  48. my @a = localtime($unix_time);
  49. return sprintf('%d-%02d-%02d %02d:%02d:%02d', (1900+$a[5]), (1+$a[4]), $a[3], $a[2], $a[1], $a[0]);
  50. }
  51. sub readable_time_cmp {
  52. my $ldate = shift;
  53. my $rdate = shift;
  54. $ldate =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
  55. my $lunx = timelocal($6, $5, $4, $3, $2 - 1, $1);
  56. $rdate =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
  57. my $runx = timelocal($6, $5, $4, $3, $2 - 1, $1);
  58. return $lunx - $runx;
  59. }
  60. sub get_readable_expiration_date {
  61. my $readable_date = shift;
  62. my $ndays = shift;
  63. $readable_date =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
  64. my $t_unix = timelocal($6, $5, $4, $3, $2 - 1, $1);
  65. my $s = $ndays*60*60*24;
  66. my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($t_unix + $s);
  67. return strftime('%Y-%m-%d %H:%M:%S', 0, 30, 2, $mday, $mon, $year);
  68. }
  69. my $DebugMode = 0;
  70. # This function only executes the passed code reference if the global variable $DebugMode is non-zero.
  71. # The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
  72. # performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
  73. #
  74. sub ifdebug(&@)
  75. {
  76. my ($cmd) = @_;
  77. &$cmd() if($DebugMode);
  78. }
  79. sub ExpirePass {
  80. my $rldbh = shift;
  81. my $cardid = shift;
  82. # get active pass
  83. my %rhash;
  84. # $rldbh->get_user_pass(\%rhash,
  85. $rldbh->GetUserPass(\%rhash,
  86. {
  87. CardId => $cardid,
  88. Active => 1
  89. }
  90. );
  91. my $pass_id = $rhash{'PassId'};
  92. my $uc_type = uc($rhash{'Type'});
  93. if ($pass_id)
  94. {
  95. # if the pass exists and has an expired nday, no nrides left
  96. # or is a preactivated card, expire it
  97. my $now_str = strftime("%Y-%m-%d %H:%M:%S", localtime());
  98. if ( ( ($uc_type eq 'NDAY') and ( readable_time_cmp($rhash{'NDayExpiration'}, $now_str) <= 0 ) )
  99. or ( ($uc_type eq 'NRIDE') and ( $rhash{'NRideRemain'} <= 0) )
  100. or ($uc_type eq 'PREACTIVE') )
  101. {
  102. $rldbh->deactivate_user_pass($pass_id);
  103. $rldbh->activate_user_card_pass($cardid);
  104. $rldbh->insert_active_rider_table( { logical_card_id => $cardid } );
  105. }
  106. }
  107. }
  108. sub use_nride {
  109. my $rldbh = shift;
  110. my $pass_entry = shift;
  111. my $art_entry = shift;
  112. my $billing_ride_time = shift;
  113. my $cur_rides = (($pass_entry->{'NRideRemain'} > 0) ? ($pass_entry->{'NRideRemain'}-1) : 0 );
  114. my %update_pass_param =
  115. (
  116. nrides_remain => $cur_rides,
  117. lastused => $billing_ride_time
  118. );
  119. if ( !$pass_entry->{'FirstUsed'} or
  120. (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
  121. {
  122. $update_pass_param{'firstused'} = $billing_ride_time ;
  123. }
  124. $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
  125. if ($cur_rides > 0)
  126. {
  127. $rldbh->insert_active_rider_table( { logical_card_id => $pass_entry->{'CardId'} } );
  128. }
  129. }
  130. sub use_nday {
  131. my $rldbh = shift;
  132. my $pass_entry = shift;
  133. my $art_entry = shift;
  134. my $billing_ride_time = shift;
  135. my %update_pass_param =
  136. (
  137. lastused => $billing_ride_time
  138. );
  139. if ( !$pass_entry->{'FirstUsed'} or
  140. (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
  141. {
  142. $update_pass_param{'firstused'} = $billing_ride_time;
  143. }
  144. if (!$pass_entry->{'NDayExpiration'})
  145. {
  146. my $nday_exp = get_readable_expiration_date($billing_ride_time, $pass_entry->{'NDayOrig'});
  147. $update_pass_param{'nday_expiration'} = $nday_exp;
  148. $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
  149. $rldbh->insert_active_rider_table( { logical_card_id => $pass_entry->{'CardId'} } );
  150. }
  151. else
  152. {
  153. $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
  154. }
  155. }
  156. sub update_domain_card {
  157. my $rldbh = shift;
  158. my $pass_entry = shift;
  159. my $billing_ride_time = shift;
  160. my %update_pass_param;
  161. if ( !$pass_entry->{'LastUsed'} or
  162. (readable_time_cmp($billing_ride_time, $pass_entry->{'LastUsed'}) > 0) )
  163. {
  164. $update_pass_param{'lastused'} = $billing_ride_time;
  165. }
  166. if ( !$pass_entry->{'FirstUsed'} or
  167. (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
  168. {
  169. $update_pass_param{'firstused'} = $billing_ride_time;
  170. }
  171. if (scalar(keys(%update_pass_param)) > 0)
  172. {
  173. $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param );
  174. }
  175. }
  176. sub art_pass_mismatch {
  177. my $rldbh = shift;
  178. my $logical_card_id = shift;
  179. my $billing_cksum = shift;
  180. my $billing_ride_time = shift;
  181. my $billing_action = shift;
  182. my $billing_rule = shift;
  183. my $pass_entry = shift;
  184. my $art_entry = shift;
  185. my $mismatch = 0;
  186. my $reason;
  187. # order matters
  188. if ( !$pass_entry->{'PassId'} )
  189. {
  190. if ( uc($billing_rule) ne $REJECT_RULE )
  191. {
  192. $reason = "Billing entry has rule \"$billing_rule\" but no passes on card";
  193. $mismatch = 1;
  194. }
  195. elsif ( uc($art_entry->{'rule_name'}) ne $REJECT_RULE )
  196. {
  197. $reason = "art rule \"" . $art_entry->{'rule_name'} . "\" with no passes on card";
  198. $mismatch = 1;
  199. }
  200. }
  201. elsif (uc($art_entry->{'rule_name'}) ne uc($pass_entry->{'Rule'}))
  202. {
  203. $mismatch = 1;
  204. $reason = "art rule \"" . $art_entry->{'rule_name'} . "\"";
  205. $reason .= " != pass rule \"" . $pass_entry->{'Rule'} . "\"";
  206. }
  207. elsif ( uc($billing_rule) ne uc($pass_entry->{'Rule'}) )
  208. {
  209. # unless its a passback reject, we have a mismatch
  210. if ( (uc($billing_action) ne 'REJECT') or (uc($billing_rule) ne 'PASSBACK') )
  211. {
  212. # bus got out of sync with art? give user this pass to protect against
  213. # decrementing an nride when an nday (or something else) was reported
  214. $mismatch = 1;
  215. $reason = "billing rule \"$billing_rule\" != pass rule \"". $pass_entry->{'Rule'} ."\"";
  216. }
  217. }
  218. if ($mismatch)
  219. {
  220. $rldbh->diagnostic_log("warning",
  221. "$PROGNAME: cardid $logical_card_id, " .
  222. "cksum $billing_cksum, " .
  223. "passid " . ($pass_entry->{'PassId'} || "n/a" ) . " " .
  224. "seq_num " . ($art_entry->{'seq_num'} || "n/a") . ", " .
  225. "mismatch ($reason): " .
  226. "bill. rule \"$billing_rule\", " .
  227. "pass rule \"" . ($pass_entry->{'Rule'} || "n/a") . "\", " .
  228. "art rule \"" . ($art_entry->{'rule_name'} || "n/a") . "\""
  229. );
  230. }
  231. return $mismatch;
  232. }
  233. sub AdvanceRiderPass {
  234. my $rldbh = shift;
  235. my $logical_card_id = shift;
  236. my $billing_cksum = shift;
  237. my $billing_ride_time = shift;
  238. my $billing_action = shift;
  239. my $billing_rule = shift;
  240. return 1 if !$logical_card_id;
  241. my %art_entry;
  242. $rldbh->get_active_rider_table(\%art_entry,
  243. {
  244. logical_card_id => $logical_card_id
  245. }
  246. );
  247. if ( !$art_entry{'seq_num'} )
  248. {
  249. $rldbh->diagnostic_log('warning', "No seq_num found in billing_log for $logical_card_id");
  250. return 0;
  251. }
  252. my %pass_entry;
  253. # $rldbh->get_user_pass(\%pass_entry,
  254. $rldbh->GetUserPass(\%pass_entry,
  255. {
  256. CardId => $logical_card_id,
  257. Active => 1
  258. }
  259. );
  260. return 0 if (art_pass_mismatch($rldbh,
  261. $logical_card_id,
  262. $billing_cksum,
  263. $billing_ride_time,
  264. $billing_action,
  265. $billing_rule,
  266. \%pass_entry,
  267. \%art_entry));
  268. # we only allow a pass to be used when it's an accept and the database is consistent for this pass
  269. if (uc($billing_action) eq 'ACCEPT')
  270. {
  271. my $uc_type = uc($pass_entry{'Type'});
  272. if ( $uc_type eq 'NRIDE')
  273. {
  274. use_nride($rldbh, \%pass_entry, \%art_entry, $billing_ride_time);
  275. }
  276. elsif ( $uc_type eq 'NDAY')
  277. {
  278. use_nday($rldbh, \%pass_entry, \%art_entry, $billing_ride_time);
  279. }
  280. else # domain card
  281. {
  282. update_domain_card($rldbh, \%pass_entry, $billing_ride_time);
  283. }
  284. }
  285. else
  286. {
  287. # update first used/last used?
  288. }
  289. ExpirePass( $rldbh, $logical_card_id );
  290. return 1;
  291. }
  292. sub ServerReply
  293. {
  294. my $client_query = $_[0];
  295. $/="\n";
  296. chomp($client_query);
  297. my $response = "";
  298. my $client_query_md5 = md5_hex($client_query);
  299. my $rldbh = RideLogicAPIQueryWrapper->connect($DSN, $DBUSER, $DBPASS) || die "ERROR: could not connect to DB";
  300. $rldbh->raise_error( 1 );
  301. my $sth ;
  302. my $loglvl ;
  303. my $message ;
  304. my $logmsg ;
  305. if ($client_query =~ m/^[\s\x00]*$/)
  306. {
  307. $logmsg .= "Ignoring spurious blank line.\n";
  308. $response .= "IGN\t" . $client_query_md5 . "\n";
  309. }
  310. elsif ($client_query =~ m/^\!/) #error
  311. {
  312. $loglvl = "error";
  313. $message = $client_query;
  314. $message =~ s/^.//;
  315. try {
  316. $rldbh->diagnostic_log($loglvl, $message)
  317. or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
  318. $response .= "ACK\t" . $client_query_md5 . "\n";
  319. }
  320. catch {
  321. $logmsg .= $_ . "\n";
  322. $response .= "IGN\t" . $client_query_md5 . "\n";
  323. };
  324. }
  325. elsif ($client_query =~ m/^\*/) #warning
  326. {
  327. $loglvl = "warning";
  328. $message = $client_query;
  329. $message =~ s/^.//;
  330. try {
  331. $rldbh->diagnostic_log($loglvl, $message)
  332. or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
  333. $response .= "ACK\t" . $client_query_md5 . "\n";
  334. }
  335. catch {
  336. $logmsg .= $_ . "\n";
  337. $response .= "IGN\t" . $client_query_md5 . "\n";
  338. };
  339. }
  340. elsif ($client_query =~ m/^\#/) #debug
  341. {
  342. $loglvl = "debug";
  343. $message = $client_query;
  344. $message =~ s/^.//;
  345. try {
  346. $rldbh->diagnostic_log($loglvl, $message)
  347. or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
  348. $response .= "ACK\t" . $client_query_md5 . "\n";
  349. }
  350. catch {
  351. $logmsg .= $_ . "\n";
  352. $response .= "IGN\t" . $client_query_md5 . "\n";
  353. };
  354. }
  355. elsif ($client_query =~ m/^(?:[^\t]*\t)+[^\t]*/) #look for a list of optionally blank tab-delimited fields
  356. {
  357. my @client_values = split(/[\t]/, $client_query, -1); #the -1 keeps split from trimming trailing blank fields
  358. #0. equip_num
  359. #1. driver
  360. #2. paddle
  361. #3. route
  362. #4. trip
  363. #5. stop
  364. #6. ride_time
  365. #7. latitude
  366. #8. longitude
  367. #9. action
  368. #10. rule
  369. #11. ruleparam
  370. #12. reason
  371. #13. credential
  372. #14. logical_card_id
  373. #15. cash_value
  374. #16. stop_name
  375. #17. (unused by DB) usec
  376. try {
  377. my $duplicate_billing_entry =
  378. $rldbh->check_dup_billing_log($client_values[6], $client_query_md5);
  379. if (!$duplicate_billing_entry) {
  380. $rldbh->insert_billing_log($client_query_md5, @client_values[0..16]);
  381. # $rldbh->lock_common();
  382. # $rldbh->begin_work();
  383. $rldbh->begin_locked_transaction_common();
  384. my $r =
  385. AdvanceRiderPass($rldbh, # db handle
  386. $client_values[14], # logical_card_id
  387. $client_query_md5, # billing log md5
  388. unix_to_readable_time($client_values[6]), # ride_time (readable)
  389. $client_values[9], # action (e.g. ACCEPT/REJECT)
  390. $client_values[10]); # rule
  391. $response .= "ACK\t" . $client_query_md5 . "\n";
  392. # $rldbh->commit();
  393. # $rldbh->unlock();
  394. $rldbh->unlock_commit();
  395. }
  396. else
  397. {
  398. $response .= "DUP\t" . $client_query_md5 . "\n";
  399. }
  400. }
  401. catch {
  402. # $rldbh->rollback();
  403. # $rldbh->unlock();
  404. $rldbh->unlock_rollback();
  405. $logmsg .= $_ . "\n";
  406. $response .= "IGN\t" . $client_query_md5 . "\n";
  407. };
  408. }
  409. else
  410. {
  411. $logmsg .= "Malformed log entry \"$client_query\".\n";
  412. $response .= "IGN\t" . $client_query_md5 . "\n";
  413. }
  414. print $logmsg if $logmsg;
  415. return $response;
  416. }
  417. sub debug_print
  418. {
  419. my $line = shift;
  420. if ($debug_logfile =~ /^([^\0]+)$/) {
  421. my $untainted_debug_logfile = $1;
  422. sysopen ( my $fh , $untainted_debug_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
  423. print $fh $line . "\n";
  424. close $fh;
  425. }
  426. }
  427. sub handle_client()
  428. {
  429. close SERVER;
  430. CLIENT->autoflush(1);
  431. my $linebuffer;
  432. while($linebuffer = <CLIENT>)
  433. {
  434. if ($billing_logfile =~ /^([^\0]+)$/) {
  435. my $untainted_billing_logfile = $1;
  436. sysopen ( my $fh , $untainted_billing_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
  437. print $fh $linebuffer;
  438. close $fh;
  439. }
  440. print CLIENT ServerReply($linebuffer);
  441. } #while data from client
  442. close CLIENT;
  443. }
  444. my $waitedpid = 0;
  445. my $sigreceived = 0;
  446. sub REAPER
  447. {
  448. while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
  449. $SIG{CHLD} = \&REAPER; # loathe sysV
  450. $sigreceived = 1;
  451. }
  452. sub spawn
  453. {
  454. my $coderef = shift; #grab the first parameter
  455. unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
  456. {
  457. confess "usage: spawn CODEREF"; #complain if this is not the case
  458. }
  459. my $pid;
  460. if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
  461. {
  462. close CLIENT;
  463. return; #failed to fork, we'd better close the client
  464. }
  465. elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
  466. {
  467. return; # i'm the parent
  468. }
  469. else #otherwise, if the returned process ID is 0, that means we're the child process
  470. {
  471. exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
  472. #exit this (child) process when we've finished our conversation(s) with the
  473. #other (client) end of the socket.
  474. }
  475. }
  476. sub show_help_and_exit {
  477. print "usage:\n";
  478. print " [-i] interactive, do not daemonize\n";
  479. print " [-c cfg] use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
  480. print " [-h] show help (this screen)\n";
  481. exit;
  482. }
  483. #----------------------------------------------------------------------
  484. #
  485. #----------------------------------------------------------------------
  486. my $daemonize = 1;
  487. my $interactive = 0;
  488. my $show_help = 0;
  489. my $cfg_file = $RideLogic::RIDELOGIC_DAEMON_CONF;
  490. my $api_cfg_file = $RideLogic::RIDELOGIC_API_CONF;
  491. GetOptions(
  492. 'i|interactive' => \$interactive,
  493. 'c|config=s' => \$cfg_file,
  494. 'h|help' => \$show_help );
  495. show_help_and_exit() if ($show_help);
  496. $daemonize=0 if ($interactive);
  497. #----------------------------------------------------------------------
  498. # Local network settings for Inter-Process communication.
  499. #----------------------------------------------------------------------
  500. my $proto = getprotobyname('tcp');
  501. my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
  502. #----------------------------------------------------------------------
  503. my $max_retries = 10; #Maximum number of address-binding retries before we give up.
  504. my $retry_count = $max_retries; #number of retries left...
  505. my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
  506. my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
  507. my %CFG_VAR;
  508. read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
  509. read_config($api_cfg_file, \%CFG_VAR) if ($api_cfg_file);
  510. my $logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_billingd.log";
  511. $billing_logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_log";
  512. my $pidfile = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_billingd.pid";
  513. $debug_logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_debug_log";
  514. $HOST = $CFG_VAR{'RIDELOGIC_DB_SERVER'};
  515. $DB = $CFG_VAR{'RIDELOGIC_DB'};
  516. $DBUSER = $CFG_VAR{'RIDELOGIC_DB_USERNAME'};
  517. $DBPASS = $CFG_VAR{'RIDELOGIC_DB_PASSWORD'};
  518. $DSN = "dbi:mysql:host=" . $HOST . ";database=" . $DB;
  519. #my $RLDBH = RideLogicAPIQueryWrapper->connect($DSN, $DBUSER, $DBPASS) || die "ERROR: could not connect to DB";
  520. #my ($query, $result, $row);
  521. #$RLDBH->raise_error( 1 );
  522. daemonize($logfile, $pidfile) if ($daemonize);
  523. # set our pipes to be piping hot
  524. $|=1;
  525. while( ($retry_count > 0) && (!$got_network) )
  526. {
  527. try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
  528. {
  529. socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
  530. setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
  531. bind (SERVER, $addr) || die "bind: $!";
  532. listen(SERVER,5) || die "listen: $!";
  533. $got_network = 1;
  534. }
  535. catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
  536. {
  537. my $errmsg = $_; #Remember the error message
  538. close(SERVER); #Clean up the server socket if it needs it
  539. #Decrement our remaining retry counter
  540. $retry_count = $retry_count - 1;
  541. #Log the message to our debug log
  542. print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
  543. #Wait a reasonable period before trying again
  544. sleep $retry_delay;
  545. };
  546. }
  547. if($got_network) #If we met with success binding to the network, report it
  548. {
  549. my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
  550. print $logmsg;
  551. }
  552. else #If we ran out of patience and gave up, report that as well and exit
  553. {
  554. my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
  555. die $errmsg;
  556. }
  557. # Set up our signal handler which will clean up defunct child processes and let the main
  558. # accept() loop know that the reason accept returned was due to a signal, not a legit connection.
  559. $SIG{CHLD} = \&REAPER;
  560. #This for loop is efficient, but confusting, so I'll break it down by clause
  561. #
  562. # The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
  563. # accept() call was interrupted by a signal. This clause runs once before the first run of the loop
  564. #
  565. # The second clause is the test clause, it will process the contents of the loop if EITHER
  566. # accept() has returned (presumably generating a valid file handle for the CLIENT end of the
  567. # socket, OR the signal received flag is set (thus accept would have returned early without
  568. # having actually accepted a connection.
  569. #
  570. # The third clause (the 'incrementer') is run after each time the body is executed, before the
  571. # test clause is executed again (deciding whether to run the body or drop out... This test
  572. # clause will close the parent process' copy of the CLIENT file handle since (see body below)
  573. # after the body executes, all communication with the socket referred to by that file handle
  574. # will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
  575. # file handle to be used again in the parent process for the next accepted incoming connection.
  576. for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
  577. {
  578. 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
  579. print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
  580. spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
  581. }