ridelogic_billingd 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  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 DBI;
  26. use Date::Calc qw(:all);
  27. use FileHandle;
  28. use Fcntl;
  29. use Digest::MD5 qw(md5 md5_hex md5_base64);
  30. use Getopt::Long qw(:config no_ignore_case);
  31. use POSIX;
  32. use Data::Dumper;
  33. use RideLogic;
  34. my $ORG = "ORG";
  35. my $database_path = 'DBI:mysql:busdb';
  36. my $database_user = '';
  37. my $database_pass = '';
  38. my $bind_ip = '127.0.0.1';
  39. my $bind_port = 2455;
  40. my $billing_logfile;
  41. sub unix_to_readable_time {
  42. my $unix_time = shift;
  43. my @a = localtime($unix_time);
  44. return sprintf('%d-%02d-%02d %02d:%02d:%02d', (1900+$a[5]), (1+$a[4]), $a[3], $a[2], $a[1], $a[0]);
  45. }
  46. my $DebugMode = 0;
  47. # This function only executes the passed code reference if the global variable $DebugMode is non-zero.
  48. # The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
  49. # performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
  50. #
  51. sub ifdebug(&@)
  52. {
  53. my ($cmd) = @_;
  54. &$cmd() if($DebugMode);
  55. }
  56. sub ExpirePass {
  57. my $dbh = shift;
  58. my $cardid = shift;
  59. my $dummy_passid = shift;
  60. my $ride_time = shift;
  61. my @oldrow = @_;
  62. local $dbh->{RaiseError};
  63. local $dbh->{PrintError};
  64. $dbh->{RaiseError} = 1;
  65. $dbh->{PrintError} = 1;
  66. $dbh->begin_work;
  67. # get passes to expire for a cardid
  68. my $query = $dbh->prepare("select p.user_pass_id, p.queue_order, p.rule, p.nrides_remain, p.nday_expiration, rc.ruleclass
  69. from user_pass p, rule_class rc
  70. where p.logical_card_id = ? and p.active = 1 and p.expired = 0 and
  71. ( ( rc.ruleclass = 'NDAY' and p.nday_expiration < now() ) or
  72. ( rc.ruleclass = 'NRIDE' and p.nrides_remain <= 0 ) or
  73. ( rc.rulename = 'PREACTIVE' ) ) ");
  74. $query->execute($cardid);
  75. if ($query->rows == 0) { $dbh->commit; return; }
  76. my $href = $query->fetchrow_hashref;
  77. my $passid = $href->{'user_pass_id'};
  78. my $current_q_num = $href->{'queue_order'};
  79. # expire old pass
  80. my $audit_pass_id = audit_user_pass_start($dbh, $passid, "billing_server: ExpirePass: deactivating and expiring pass");
  81. $query = $dbh->prepare("update user_pass set active = 0, expired = 1, deactivated = now() where user_pass_id = ?");
  82. $query->execute($passid);
  83. audit_user_pass_end($dbh, $passid, $audit_pass_id);
  84. # activate new pass
  85. $query = $dbh->prepare("select p.user_pass_id, p.rule, p.nday_orig, p.nday_expiration, p.nrides_orig, p.queue_order, rc.ruleclass
  86. from user_pass p, rule_class rc
  87. where p.logical_card_id = ?
  88. and p.expired = 0 and p.rule = rc.rulename
  89. and p.queue_order = ( select min(t.queue_order)
  90. from user_pass t
  91. where t.logical_card_id = ?
  92. and t.queue_order > ?
  93. and t.expired = 0) ");
  94. $query->execute($cardid, $cardid, $current_q_num);
  95. # no passes left, put in reject rule, finish transaction
  96. if ($query->rows == 0) {
  97. $query = $dbh->prepare("lock tables active_rider_table write");
  98. $query->execute();
  99. $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
  100. values (?,?,?,?,?,?,?)");
  101. $query->execute($cardid, @oldrow[1,2], $ORG . '-REJECT', 'reject', 0, $oldrow[7]);
  102. $dbh->commit;
  103. $query = $dbh->prepare("unlock tables");
  104. $query->execute();
  105. return;
  106. }
  107. # else make new pass active and update art with new pass
  108. $href = $query->fetchrow_hashref;
  109. my $pass_param = '';
  110. if ($href->{'ruleclass'} eq 'NRIDE') {
  111. $pass_param = $href->{'nrides_orig'};
  112. } elsif ($href->{'ruleclass'} eq 'NDAY') {
  113. $pass_param = $href->{'nday_orig'};
  114. $pass_param .= " " . $href->{'nday_expiration'} if $href->{'nday_expiration'};
  115. }
  116. $audit_pass_id = audit_user_pass_start($dbh, $href->{'user_pass_id'}, "billing_server: ExpirePass: activating pass");
  117. $query = $dbh->prepare("update user_pass set active = 1, activated = ? where user_pass_id = ?");
  118. $query->execute($ride_time, $href->{'user_pass_id'} );
  119. audit_user_pass_end($dbh, $href->{'user_pass_id'}, $audit_pass_id);
  120. $query = $dbh->prepare("lock tables active_rider_table write");
  121. $query->execute();
  122. $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
  123. values (?,?,?,?,?,?,?)");
  124. $query->execute($cardid, @oldrow[1,2], $href->{'rule'}, $pass_param, 0, $oldrow[7]);
  125. $dbh->commit;
  126. $query = $dbh->prepare("unlock tables");
  127. $query->execute();
  128. }
  129. sub AdvanceRiderPass {
  130. my $dbh = shift;
  131. my $logical_card_id = shift;
  132. my $billing_cksum = shift;
  133. my $billing_ride_time = shift;
  134. my $billing_action = shift;
  135. my $billing_rule = shift;
  136. local $dbh->{RaiseError};
  137. local $dbh->{PrintError};
  138. $dbh->{RaiseError} = 1;
  139. $dbh->{PrintError} = 1;
  140. $dbh->begin_work;
  141. my $sth_find = $dbh->prepare('SELECT active_rider_table.logical_card_id, active_rider_table.rfid_token,
  142. active_rider_table.mag_token, active_rider_table.rule_name,
  143. active_rider_table.rule_param, active_rider_table.deleted,
  144. active_rider_table.parent_entity, active_rider_table.notes,
  145. active_rider_table.seq_num
  146. FROM active_rider_table
  147. WHERE logical_card_id = ?
  148. AND NOT(deleted)
  149. AND seq_num = (SELECT max(seq_num) FROM active_rider_table WHERE logical_card_id = ?) ');
  150. $sth_find->execute($logical_card_id, $logical_card_id);
  151. if ($sth_find->rows != 1) { $dbh->commit; return; }
  152. #@oldrow:
  153. #0. logical_card_id
  154. #1. rfid_token
  155. #2. mag_token
  156. #3. rule_name
  157. #4. rule_param
  158. #5. deleted
  159. #6. parent_entity
  160. #7. notes
  161. #8. seq_num
  162. my @oldrow = $sth_find->fetchrow_array();
  163. my $sth_pass = $dbh->prepare("select p.user_pass_id, p.nrides_remain, p.nday_orig, p.nday_expiration, p.rule
  164. from user_pass p, user_card c
  165. where p.logical_card_id = ?
  166. and c.logical_card_id = p.logical_card_id
  167. and c.active = 1
  168. and p.active = 1
  169. and p.expired = 0
  170. and p.activated <= ?");
  171. $sth_pass->execute($logical_card_id, $billing_ride_time);
  172. if ($sth_pass->rows != 1) {
  173. if (uc($billing_action) ne "REJECT") {
  174. my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message)
  175. values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', dropping billing entry: no matching pass entry') ) ");
  176. $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
  177. }
  178. $dbh->commit;
  179. return;
  180. }
  181. my $pass = $sth_pass->fetchrow_hashref;
  182. my $t = $dbh->prepare("select ruleclass from rule_class where rulename = ?");
  183. $t->execute($pass->{'rule'});
  184. my $rule_class = 'OTHER';
  185. if ($t->rows == 1) {
  186. $rule_class = $t->fetchrow_hashref->{'ruleclass'};
  187. } elsif ($t->rows < 1) {
  188. my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message)
  189. values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', no rule class found, dropping billing entry') ) ");
  190. $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
  191. $dbh->commit;
  192. return;
  193. } else {
  194. my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message)
  195. values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', multiple rule classes found, dropping billing entry') ) ");
  196. $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
  197. $dbh->commit;
  198. return;
  199. }
  200. if (uc($billing_action) eq "REJECT") {
  201. # bus not sync'd?
  202. $dbh->commit;
  203. } elsif ($oldrow[3] ne $pass->{'rule'}) {
  204. # raise warning?
  205. my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message)
  206. values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(1): art rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"') )");
  207. $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $oldrow[3], $pass->{'user_pass_id'}, $pass->{'rule'});
  208. $dbh->commit;
  209. } elsif ($billing_rule ne $pass->{'rule'}) {
  210. # bus got out of sync with art? give user this pass at the risk to prevent against
  211. # decrementing an nride when an nday (or something else) was reported
  212. my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message)
  213. values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(2): billing rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"' ) )");
  214. $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $billing_rule, $pass->{'user_pass_id'}, $pass->{'rule'});
  215. $dbh->commit;
  216. } elsif ( $rule_class eq 'NRIDE') {
  217. my $cur_rides = (($pass->{'nrides_remain'} > 0) ? ($pass->{'nrides_remain'}-1) : 0 );
  218. $oldrow[4] = $cur_rides;
  219. my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nride");
  220. my $q = $dbh->prepare('update user_pass set nrides_remain = ?, lastused = ? where user_pass_id = ?');
  221. $q->execute($cur_rides, $billing_ride_time, $pass->{'user_pass_id'});
  222. audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
  223. # expire passes will take care of it if #rides == 0
  224. if ($cur_rides>0) {
  225. $q = $dbh->prepare("lock tables active_rider_table write");
  226. $q->execute();
  227. $q = $dbh->prepare('insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes)
  228. values (?, ?, ?,?, ?, ?, ?, ?)');
  229. $q->execute(@oldrow[0..7]);
  230. }
  231. $dbh->commit;
  232. if ($cur_rides>0) { $q = $dbh->prepare("unlock tables"); $q->execute(); }
  233. } elsif ($rule_class eq 'NDAY') {
  234. # update user_pass with expiration and update active_rider_table with new param
  235. if (!$pass->{'nday_expiration'}) {
  236. my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday");
  237. my $q = $dbh->prepare("update user_pass
  238. set nday_expiration = addtime( adddate(convert(date(?), datetime), nday_orig), '2:30'), firstused = ?, lastused = ?
  239. where user_pass_id = ?");
  240. $q->execute($billing_ride_time, $billing_ride_time, $billing_ride_time, $pass->{'user_pass_id'});
  241. audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
  242. $oldrow[4] = $pass->{'nday_orig'} . " " . join('-', Add_Delta_Days(Today, $pass->{'nday_orig'} )) . " 2:30:00";
  243. $q = $dbh->prepare("lock tables active_rider_table write"); $q->execute();
  244. my $sth_new_expires = $dbh->prepare('INSERT INTO active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes)
  245. VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
  246. $sth_new_expires->execute(@oldrow[0..7]);
  247. $dbh->commit;
  248. $q = $dbh->prepare("unlock tables");
  249. $q->execute();
  250. } else { # else just update last used
  251. my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday (lastused only)");
  252. my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
  253. $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
  254. audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
  255. $dbh->commit;
  256. }
  257. } else {
  258. # domain card, do nothing
  259. my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating domain (lastused only)");
  260. my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
  261. $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
  262. audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
  263. $dbh->commit;
  264. }
  265. ExpirePass( $dbh, $logical_card_id, $pass->{'user_pass_id'}, $billing_ride_time, @oldrow );
  266. }
  267. sub ServerReply
  268. {
  269. my $client_query = $_[0];
  270. $/="\n";
  271. chomp($client_query);
  272. my $response = "";
  273. my $client_query_md5 = md5_hex($client_query);
  274. my $dbh = DBI->connect($database_path, $database_user, $database_pass)
  275. or die "Couldn't connect to database: " . DBI->errstr;
  276. my $sth ;
  277. my $loglvl ;
  278. my $message ;
  279. my $logmsg ;
  280. if ($client_query =~ m/^[\s\x00]*$/)
  281. {
  282. $logmsg .= "Ignoring spurious blank line.\n";
  283. $response .= "IGN\t" . $client_query_md5 . "\n";
  284. }
  285. elsif ($client_query =~ m/^\!/) #error
  286. {
  287. $loglvl = "error";
  288. $message = $client_query;
  289. $message =~ s/^.//;
  290. try {
  291. $sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log (loglvl, message) VALUES (?, ?)')
  292. or die "Couldn't prepare statement: " . $dbh->errstr;
  293. $sth->execute($loglvl, $message) # Execute the query
  294. or die "Couldn't execute statement: " . $sth->errstr;
  295. }
  296. catch {
  297. $logmsg .= $_ . "\n";
  298. $response .= "IGN\t" . $client_query_md5 . "\n";
  299. };
  300. if ($sth->rows < 1) {
  301. $response .= "DUP\t" . $client_query_md5 . "\n";
  302. } else {
  303. $response .= "ACK\t" . $client_query_md5 . "\n";
  304. }
  305. }
  306. elsif ($client_query =~ m/^\*/) #warning
  307. {
  308. $loglvl = "warning";
  309. $message = $client_query;
  310. $message =~ s/^.//;
  311. try {
  312. $sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log (loglvl, message) VALUES (?, ?)')
  313. or die "Couldn't prepare statement: " . $dbh->errstr;
  314. $sth->execute($loglvl, $message) # Execute the query
  315. or die "Couldn't execute statement: " . $sth->errstr;
  316. }
  317. catch {
  318. $logmsg .= $_ . "\n";
  319. $response .= "IGN\t" . $client_query_md5 . "\n";
  320. };
  321. if ($sth->rows < 1) {
  322. $response .= "DUP\t" . $client_query_md5 . "\n";
  323. } else {
  324. $response .= "ACK\t" . $client_query_md5 . "\n";
  325. }
  326. }
  327. elsif ($client_query =~ m/^\#/) #debug
  328. {
  329. $loglvl = "debug";
  330. $message = $client_query;
  331. $message =~ s/^.//;
  332. try {
  333. $sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log (loglvl, message) VALUES (?, ?)')
  334. or die "Couldn't prepare statement: " . $dbh->errstr;
  335. $sth->execute($loglvl, $message) # Execute the query
  336. or die "Couldn't execute statement: " . $sth->errstr;
  337. }
  338. catch {
  339. $logmsg .= $_ . "\n";
  340. $response .= "IGN\t" . $client_query_md5 . "\n";
  341. };
  342. if ($sth->rows < 1) {
  343. $response .= "DUP\t" . $client_query_md5 . "\n";
  344. } else {
  345. $response .= "ACK\t" . $client_query_md5 . "\n";
  346. }
  347. }
  348. elsif ($client_query =~ m/^(?:[^\t]*\t)+[^\t]*/) #look for a list of optionally blank tab-delimited fields
  349. {
  350. my @client_values = split(/[\t]/, $client_query, -1); #the -1 keeps split from trimming trailing blank fields
  351. #0. equip_num
  352. #1. driver
  353. #2. paddle
  354. #3. route
  355. #4. trip
  356. #5. stop
  357. #6. ride_time
  358. #7. latitude
  359. #8. longitude
  360. #9. action
  361. #10. rule
  362. #11. ruleparam
  363. #12. reason
  364. #13. credential
  365. #14. logical_card_id
  366. #15. cash_value
  367. #16. stop_name
  368. #17. (unused by DB) usec
  369. my $duplicate_billing_entry=0;
  370. try {
  371. $sth = $dbh->prepare('select count(*) num from billing_log where ride_time = FROM_UNIXTIME(?) and conf_checksum = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
  372. $sth->execute($client_values[6], $client_query_md5) or die "Couldn't execute statement: " . $sth->errstr;
  373. $duplicate_billing_entry=1 if ($sth->fetchrow_arrayref->[0] > 0);
  374. if (!$duplicate_billing_entry) {
  375. $sth = $dbh->prepare('REPLACE INTO billing_log (conf_checksum, equip_num, driver, paddle, route, trip, stop, ride_time, latitude, longitude, action, rule, ruleparam, reason, credential, logical_card_id, cash_value, stop_name) VALUES (?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
  376. or die "Couldn't prepare statement: " . $dbh->errstr;
  377. $sth->execute($client_query_md5, @client_values[0..16]) # Execute the query
  378. or die "Couldn't execute statement: " . $sth->errstr;
  379. }
  380. }
  381. catch {
  382. $logmsg .= $_ . "\n";
  383. $response .= "IGN\t" . $client_query_md5 . "\n";
  384. };
  385. if ($duplicate_billing_entry)
  386. {
  387. $response .= "DUP\t" . $client_query_md5 . "\n";
  388. } elsif ($sth->rows == 1) #if the billing log update was sucessful and wasn't a duplicate
  389. {
  390. AdvanceRiderPass($dbh, $client_values[14], $client_query_md5, unix_to_readable_time($client_values[6]), $client_values[9], $client_values[10]);
  391. $response .= "ACK\t" . $client_query_md5 . "\n";
  392. }
  393. #elsif ($sth->rows > 1)
  394. #{
  395. # $response .= "DUP\t" . $client_query_md5 . "\n";
  396. #}
  397. else
  398. {
  399. $logmsg .= "Error inserting $client_query_md5 $client_query into billing_log\n" ;
  400. }
  401. }
  402. else
  403. {
  404. $logmsg .= "Malformed log entry \"$client_query\".\n";
  405. $response .= "IGN\t" . $client_query_md5 . "\n";
  406. }
  407. print $logmsg if $logmsg;
  408. return $response;
  409. }
  410. sub handle_client()
  411. {
  412. close SERVER;
  413. CLIENT->autoflush(1);
  414. my $linebuffer;
  415. while($linebuffer = <CLIENT>)
  416. {
  417. if ($billing_logfile =~ /^([^\0]+)$/) {
  418. my $untainted_billing_logfile = $1;
  419. sysopen ( my $fh , $untainted_billing_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
  420. print $fh $linebuffer;
  421. close $fh;
  422. }
  423. print CLIENT ServerReply($linebuffer);
  424. } #while data from client
  425. close CLIENT;
  426. }
  427. my $waitedpid = 0;
  428. my $sigreceived = 0;
  429. sub REAPER
  430. {
  431. while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
  432. $SIG{CHLD} = \&REAPER; # loathe sysV
  433. $sigreceived = 1;
  434. }
  435. sub spawn
  436. {
  437. my $coderef = shift; #grab the first parameter
  438. unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
  439. {
  440. confess "usage: spawn CODEREF"; #complain if this is not the case
  441. }
  442. my $pid;
  443. if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
  444. {
  445. close CLIENT;
  446. return; #failed to fork, we'd better close the client
  447. }
  448. elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
  449. {
  450. return; # i'm the parent
  451. }
  452. else #otherwise, if the returned process ID is 0, that means we're the child process
  453. {
  454. exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
  455. #exit this (child) process when we've finished our conversation(s) with the
  456. #other (client) end of the socket.
  457. }
  458. }
  459. sub show_help_and_exit {
  460. print "usage:\n";
  461. print " [-i] interactive, do not daemonize\n";
  462. print " [-c cfg] use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
  463. print " [-h] show help (this screen)\n";
  464. exit;
  465. }
  466. #----------------------------------------------------------------------
  467. #
  468. #----------------------------------------------------------------------
  469. my $daemonize = 1;
  470. my $interactive = 0;
  471. my $show_help = 0;
  472. my $cfg_file = $RideLogic::RIDELOGIC_DAEMON_CONF;
  473. GetOptions(
  474. 'i|interactive' => \$interactive,
  475. 'c|config=s' => \$cfg_file,
  476. 'h|help' => \$show_help );
  477. show_help_and_exit() if ($show_help);
  478. $daemonize=0 if ($interactive);
  479. #----------------------------------------------------------------------
  480. # Local network settings for Inter-Process communication.
  481. #----------------------------------------------------------------------
  482. my $proto = getprotobyname('tcp');
  483. my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
  484. #----------------------------------------------------------------------
  485. my $max_retries = 10; #Maximum number of address-binding retries before we give up.
  486. my $retry_count = $max_retries; #number of retries left...
  487. my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
  488. my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
  489. my %CFG_VAR;
  490. read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
  491. my $logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_billingd.log";
  492. $billing_logfile = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_log";
  493. my $pidfile = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_billingd.pid";
  494. daemonize($logfile, $pidfile) if ($daemonize);
  495. # set our pipes to be piping hot
  496. $|=1;
  497. while( ($retry_count > 0) && (!$got_network) )
  498. {
  499. try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
  500. {
  501. socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
  502. setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
  503. bind (SERVER, $addr) || die "bind: $!";
  504. listen(SERVER,5) || die "listen: $!";
  505. $got_network = 1;
  506. }
  507. catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
  508. {
  509. my $errmsg = $_; #Remember the error message
  510. close(SERVER); #Clean up the server socket if it needs it
  511. #Decrement our remaining retry counter
  512. $retry_count = $retry_count - 1;
  513. #Log the message to our debug log
  514. print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
  515. #Wait a reasonable period before trying again
  516. sleep $retry_delay;
  517. };
  518. }
  519. if($got_network) #If we met with success binding to the network, report it
  520. {
  521. my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
  522. print $logmsg;
  523. }
  524. else #If we ran out of patience and gave up, report that as well and exit
  525. {
  526. my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
  527. die $errmsg;
  528. }
  529. # Set up our signal handler which will clean up defunct child processes and let the main
  530. # accept() loop know that the reason accept returned was due to a signal, not a legit connection.
  531. $SIG{CHLD} = \&REAPER;
  532. #This for loop is efficient, but confusting, so I'll break it down by clause
  533. #
  534. # The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
  535. # accept() call was interrupted by a signal. This clause runs once before the first run of the loop
  536. #
  537. # The second clause is the test clause, it will process the contents of the loop if EITHER
  538. # accept() has returned (presumably generating a valid file handle for the CLIENT end of the
  539. # socket, OR the signal received flag is set (thus accept would have returned early without
  540. # having actually accepted a connection.
  541. #
  542. # The third clause (the 'incrementer') is run after each time the body is executed, before the
  543. # test clause is executed again (deciding whether to run the body or drop out... This test
  544. # clause will close the parent process' copy of the CLIENT file handle since (see body below)
  545. # after the body executes, all communication with the socket referred to by that file handle
  546. # will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
  547. # file handle to be used again in the parent process for the next accepted incoming connection.
  548. for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
  549. {
  550. 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
  551. print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
  552. spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
  553. }