avls_server.pl 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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 FileHandle;
  27. use Fcntl;
  28. use Compress::Zlib;
  29. use POSIX;
  30. my $DATADIR=$ENV{'HOME'} . "/data";
  31. #my $database_path = 'DBI:SQLite:dbname=../bus.sqlite';
  32. my $database_path = 'DBI:SQLite:dbname=' . $DATADIR .'/bus.sqlite';
  33. my $database_user = '';
  34. my $database_pass = '';
  35. my $bind_ip = '127.0.0.1';
  36. my $bind_port = 2857;
  37. #----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
  38. #-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
  39. #-------------------------------------------function instead of returning an error message in many cases...
  40. # This utility function returns the passed string sans any leading or trailing whitespace.
  41. #
  42. sub strip_whitespace
  43. {
  44. my $str = shift; #grab our first parameter
  45. $str =~ s/^\s+//; #strip leading whitespace
  46. $str =~ s/\s+$//; #strip trailing whitespace
  47. return $str; #return the improved string
  48. }
  49. # This function takes two coderef parameters, the second of which is usually an explicit call to the
  50. # 'catch' function which itself takes a coderef parameter. This allows the code employing this suite of
  51. # functions to look somewhat like a conventional exception handling mechanism:
  52. #
  53. # try
  54. # {
  55. # do_something_that_might_die();
  56. # }
  57. # catch
  58. # {
  59. # my $errmsg = $_;
  60. # log_the_error_message($errmsg);
  61. # perform_some_cleanup();
  62. # };
  63. #
  64. # DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
  65. #
  66. sub try(&$)
  67. {
  68. my ($attempt, $handler) = @_;
  69. eval
  70. {
  71. &$attempt;
  72. };
  73. if($@)
  74. {
  75. do_catch($handler);
  76. }
  77. }
  78. # This function strips off the whitespace from the exception message reported by die()
  79. # and places the result into the default variable such that the code in the catch block can
  80. # just examine $_ to figure out what the cause of the error is, or to display or log
  81. # the error message.
  82. #
  83. sub do_catch(&$)
  84. {
  85. my ($handler) = @_;
  86. local $_ = strip_whitespace($@);
  87. &$handler;
  88. }
  89. # This just takes an explicit coderef and returns it unharmed. The only
  90. # purpose of this is so the try/catch structure looks pretty and familiar.
  91. #
  92. sub catch(&) {$_[0]}
  93. #--------------------------------------------------------------------------------------------------------------------
  94. #my $DebugMode = 1;
  95. my $DebugMode = 0;
  96. # This function only executes the passed code reference if the global variable $DebugMode is non-zero.
  97. # The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
  98. # performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
  99. #
  100. sub ifdebug(&@)
  101. {
  102. my ($cmd) = @_;
  103. &$cmd() if($DebugMode);
  104. }
  105. sub StoreAvls
  106. {
  107. my $client_query = $_[0];
  108. chomp($client_query);
  109. my $dbh = DBI->connect($database_path, $database_user, $database_pass)
  110. or die "Couldn't connect to database: " . DBI->errstr;
  111. #my $sth_avls = $dbh->prepare('INSERT INTO avls_data (equip_num, driver, paddle, route, trip, stop, chirp_time, latitude, longitude, heading, velocity) VALUES (?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?)')
  112. my $sth_avls = $dbh->prepare('INSERT INTO avls_data (equip_num, driver, paddle, route, trip, stop, chirp_time, latitude, longitude, heading, velocity) VALUES (?, ?, ?, ?, ?, ?, datetime(?, "unixepoch"), ?, ?, ?, ?)')
  113. or die "Couldn't prepare statement: " . $dbh->errstr;
  114. #store avls data
  115. $sth_avls->execute(split("\t", $client_query)) # Execute the query
  116. or die "Couldn't execute statement: " . $sth_avls->errstr;
  117. $sth_avls->finish;
  118. $dbh->disconnect;
  119. }
  120. sub handle_client()
  121. {
  122. close SERVER;
  123. CLIENT->autoflush(1);
  124. my $linebuffer;
  125. while($linebuffer = <CLIENT>)
  126. {
  127. StoreAvls($linebuffer);
  128. } #while data from client
  129. close CLIENT;
  130. }
  131. my $waitedpid = 0;
  132. my $sigreceived = 0;
  133. sub REAPER
  134. {
  135. while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
  136. $SIG{CHLD} = \&REAPER; # loathe sysV
  137. $sigreceived = 1;
  138. }
  139. sub spawn
  140. {
  141. my $coderef = shift; #grab the first parameter
  142. unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
  143. {
  144. confess "usage: spawn CODEREF"; #complain if this is not the case
  145. }
  146. my $pid;
  147. if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
  148. {
  149. close CLIENT;
  150. return; #failed to fork, we'd better close the client
  151. }
  152. elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
  153. {
  154. return; # i'm the parent
  155. }
  156. else #otherwise, if the returned process ID is 0, that means we're the child process
  157. {
  158. exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
  159. #exit this (child) process when we've finished our conversation(s) with the
  160. #other (client) end of the socket.
  161. }
  162. }
  163. #----------------------------------------------------------------------
  164. # Local network settings for Inter-Process communication.
  165. #----------------------------------------------------------------------
  166. my $proto = getprotobyname('tcp');
  167. my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
  168. #----------------------------------------------------------------------
  169. my $max_retries = 10; #Maximum number of address-binding retries before we give up.
  170. my $retry_count = $max_retries; #number of retries left...
  171. my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
  172. my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
  173. $|=1;
  174. while( ($retry_count > 0) && (!$got_network) )
  175. {
  176. try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
  177. {
  178. socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
  179. setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
  180. bind (SERVER, $addr) || die "bind: $!";
  181. listen(SERVER,5) || die "listen: $!";
  182. $got_network = 1;
  183. }
  184. catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
  185. {
  186. my $errmsg = $_; #Remember the error message
  187. close(SERVER); #Clean up the server socket if it needs it
  188. #Decrement our remaining retry counter
  189. $retry_count = $retry_count - 1;
  190. #Log the message to our debug log
  191. print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
  192. #Wait a reasonable period before trying again
  193. sleep $retry_delay;
  194. };
  195. }
  196. if($got_network) #If we met with success binding to the network, report it
  197. {
  198. my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
  199. print $logmsg;
  200. }
  201. else #If we ran out of patience and gave up, report that as well and exit
  202. {
  203. my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
  204. die $errmsg;
  205. }
  206. # Set up our signal handler which will clean up defunct child processes and let the main
  207. # accept() loop know that the reason accept returned was due to a signal, not a legit connection.
  208. $SIG{CHLD} = \&REAPER;
  209. #This for loop is efficient, but confusting, so I'll break it down by clause
  210. #
  211. # The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
  212. # accept() call was interrupted by a signal. This clause runs once before the first run of the loop
  213. #
  214. # The second clause is the test clause, it will process the contents of the loop if EITHER
  215. # accept() has returned (presumably generating a valid file handle for the CLIENT end of the
  216. # socket, OR the signal received flag is set (thus accept would have returned early without
  217. # having actually accepted a connection.
  218. #
  219. # The third clause (the 'incrementer') is run after each time the body is executed, before the
  220. # test clause is executed again (deciding whether to run the body or drop out... This test
  221. # clause will close the parent process' copy of the CLIENT file handle since (see body below)
  222. # after the body executes, all communication with the socket referred to by that file handle
  223. # will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
  224. # file handle to be used again in the parent process for the next accepted incoming connection.
  225. for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
  226. {
  227. 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
  228. print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
  229. spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
  230. }