version_daemon.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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 Carp;
  24. use DBI;
  25. use FileHandle;
  26. use Fcntl;
  27. use POSIX;
  28. my $database_path = 'DBI:mysql:busdb';
  29. my $database_user = '';
  30. my $database_pass = '';
  31. my $bind_ip = '127.0.0.1';
  32. my $bind_port = 8377;
  33. #----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
  34. #-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
  35. #-------------------------------------------function instead of returning an error message in many cases...
  36. # This utility function returns the passed string sans any leading or trailing whitespace.
  37. #
  38. sub strip_whitespace
  39. {
  40. my $str = shift; #grab our first parameter
  41. $str =~ s/^\s+//; #strip leading whitespace
  42. $str =~ s/\s+$//; #strip trailing whitespace
  43. return $str; #return the improved string
  44. }
  45. # This function takes two coderef parameters, the second of which is usually an explicit call to the
  46. # 'catch' function which itself takes a coderef parameter. This allows the code employing this suite of
  47. # functions to look somewhat like a conventional exception handling mechanism:
  48. #
  49. # try
  50. # {
  51. # do_something_that_might_die();
  52. # }
  53. # catch
  54. # {
  55. # my $errmsg = $_;
  56. # log_the_error_message($errmsg);
  57. # perform_some_cleanup();
  58. # };
  59. #
  60. # DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
  61. #
  62. sub try(&$)
  63. {
  64. my ($attempt, $handler) = @_;
  65. eval
  66. {
  67. &$attempt;
  68. };
  69. if($@)
  70. {
  71. do_catch($handler);
  72. }
  73. }
  74. # This function strips off the whitespace from the exception message reported by die()
  75. # and places the result into the default variable such that the code in the catch block can
  76. # just examine $_ to figure out what the cause of the error is, or to display or log
  77. # the error message.
  78. #
  79. sub do_catch(&$)
  80. {
  81. my ($handler) = @_;
  82. local $_ = strip_whitespace($@);
  83. &$handler;
  84. }
  85. # This just takes an explicit coderef and returns it unharmed. The only
  86. # purpose of this is so the try/catch structure looks pretty and familiar.
  87. #
  88. sub catch(&) {$_[0]}
  89. #--------------------------------------------------------------------------------------------------------------------
  90. #my $DebugMode = 1;
  91. my $DebugMode = 0;
  92. # This function only executes the passed code reference if the global variable $DebugMode is non-zero.
  93. # The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
  94. # performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
  95. #
  96. sub ifdebug(&@)
  97. {
  98. my ($cmd) = @_;
  99. &$cmd() if($DebugMode);
  100. }
  101. sub CheckinServerReply
  102. {
  103. my $client_query = $_[0];
  104. my $dbh = DBI->connect($database_path, $database_user, $database_pass)
  105. or die "Couldn't connect to database: " . DBI->errstr;
  106. my $sth ;
  107. my $logmsg ;
  108. my $response = '';
  109. my @client_values = split(/[\t]/, $client_query, -1); #the -1 keeps split from trimming trailing blank fields
  110. #0. viper_num (0 for Phase II)
  111. #1. equip_num (usually bogus for Phase I)
  112. #2. eth0_mac (Effectively a serial number of the SBC (be it Viper, Titan, or some Atom based system)
  113. #3. cell_imei (Effectively a serial number of the Cell Modem)
  114. #4. cell_imsi (Effectively a serial number of the SIM card inserted in the modem)
  115. #5. version_strings (a concatenation of package versions)
  116. $client_values[0] =~ s/^[^0-9]*//; #Strip the leading '#' (and anything else non-numeric) from our string
  117. $sth = $dbh->prepare('INSERT INTO bus_checkin_log (viper_num, equip_num, eth0_mac, cell_imei, cell_imsi, version_data) VALUES (?, ?, ?, ?, ?, ?)');
  118. # We explicitly chop this down to the 6 fields we want to insert, rather than passing @client_values as a parameter so
  119. #that if some foolish version string goes and contains a tab (this should never happen!) it will be trunctated instead
  120. #of the whole update being shitcanned because the array has too many data fields for the quiery...
  121. try
  122. {
  123. $sth->execute(@client_values[0..5]);
  124. $response .= "Thanks.\n";
  125. }
  126. catch
  127. {
  128. $logmsg .= $_ . "\n";
  129. $response .= "Server Side Error.\n";
  130. };
  131. print $logmsg if $logmsg;
  132. return $response;
  133. }
  134. sub ServerReply
  135. {
  136. my $client_query = $_[0];
  137. $/="\n";
  138. chomp($client_query);
  139. if ($client_query =~ m/^\#/) #A leading '#' signals a bus_checkin_log entry, rather than an package update checkin
  140. {
  141. return CheckinServerReply($client_query);
  142. }
  143. my $response = "";
  144. my $dbh = DBI->connect($database_path, $database_user, $database_pass)
  145. or die "Couldn't connect to database: " . DBI->errstr;
  146. my $sth ;
  147. my $logmsg ;
  148. $sth = $dbh->prepare('SELECT client_file, checksum, file_size, file_path, fileversion FROM update_level t1 WHERE (serial = (SELECT serial FROM update_level WHERE client_file = t1.client_file AND (equip_num = 0 OR equip_num = ?) ORDER BY equip_num DESC, serial DESC LIMIT 1)) ORDER BY client_file ASC');
  149. my @client_values = split(/[\t]/, $client_query, -1); #the -1 keeps split from trimming trailing blank fields
  150. #0. equip_num
  151. #1. filename=md5sum
  152. #2 ...
  153. my $i;
  154. my %filetable = ();
  155. for($i = 1; $i < @client_values; $i = $i + 1)
  156. {
  157. my ($client_file, $client_checksum) = split(/=/, $client_values[$i]);
  158. if($client_file && $client_checksum)
  159. {
  160. $filetable{$client_file} = $client_checksum;
  161. }
  162. }
  163. try
  164. {
  165. $sth->execute($client_values[0]) or die "Couldn't execute statement: " . $sth->errstr;
  166. }
  167. catch
  168. {
  169. $logmsg .= $_ . "\n";
  170. };
  171. while(my @data = $sth->fetchrow_array())
  172. {
  173. #0 client_file
  174. #1 checksum
  175. #2 file_size
  176. #3 file_path
  177. #4 fileversion
  178. if(defined $filetable{$data[0]} && $filetable{$data[0]} eq $data[1])
  179. {
  180. #do nothing, the client is up to date
  181. }
  182. else
  183. {
  184. $response .= "$data[0]\t$data[1]\t$data[2]\t$data[3]\t$data[4]\n";
  185. }
  186. }
  187. print $logmsg if $logmsg;
  188. return $response;
  189. }
  190. sub handle_client()
  191. {
  192. close SERVER;
  193. CLIENT->autoflush(1);
  194. my $linebuffer;
  195. while($linebuffer = <CLIENT>)
  196. {
  197. print CLIENT ServerReply($linebuffer);
  198. } #while data from client
  199. close CLIENT;
  200. }
  201. my $waitedpid = 0;
  202. my $sigreceived = 0;
  203. sub REAPER
  204. {
  205. while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
  206. $SIG{CHLD} = \&REAPER; # loathe sysV
  207. $sigreceived = 1;
  208. }
  209. sub spawn
  210. {
  211. my $coderef = shift; #grab the first parameter
  212. unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
  213. {
  214. confess "usage: spawn CODEREF"; #complain if this is not the case
  215. }
  216. my $pid;
  217. if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
  218. {
  219. close CLIENT;
  220. return; #failed to fork, we'd better close the client
  221. }
  222. elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
  223. {
  224. return; # i'm the parent
  225. }
  226. else #otherwise, if the returned process ID is 0, that means we're the child process
  227. {
  228. exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
  229. #exit this (child) process when we've finished our conversation(s) with the
  230. #other (client) end of the socket.
  231. }
  232. }
  233. #----------------------------------------------------------------------
  234. # Local network settings for Inter-Process communication.
  235. #----------------------------------------------------------------------
  236. my $proto = getprotobyname('tcp');
  237. my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
  238. #----------------------------------------------------------------------
  239. my $max_retries = 10; #Maximum number of address-binding retries before we give up.
  240. my $retry_count = $max_retries; #number of retries left...
  241. my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
  242. my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
  243. while( ($retry_count > 0) && (!$got_network) )
  244. {
  245. try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
  246. {
  247. socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
  248. setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
  249. bind (SERVER, $addr) || die "bind: $!";
  250. listen(SERVER,5) || die "listen: $!";
  251. $got_network = 1;
  252. }
  253. catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
  254. {
  255. my $errmsg = $_; #Remember the error message
  256. close(SERVER); #Clean up the server socket if it needs it
  257. #Decrement our remaining retry counter
  258. $retry_count = $retry_count - 1;
  259. #Log the message to our debug log
  260. print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
  261. #Wait a reasonable period before trying again
  262. sleep $retry_delay;
  263. };
  264. }
  265. if($got_network) #If we met with success binding to the network, report it
  266. {
  267. my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
  268. print $logmsg;
  269. }
  270. else #If we ran out of patience and gave up, report that as well and exit
  271. {
  272. my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
  273. die $errmsg;
  274. }
  275. # Set up our signal handler which will clean up defunct child processes and let the main
  276. # accept() loop know that the reason accept returned was due to a signal, not a legit connection.
  277. $SIG{CHLD} = \&REAPER;
  278. #This for loop is efficient, but confusting, so I'll break it down by clause
  279. #
  280. # The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
  281. # accept() call was interrupted by a signal. This clause runs once before the first run of the loop
  282. #
  283. # The second clause is the test clause, it will process the contents of the loop if EITHER
  284. # accept() has returned (presumably generating a valid file handle for the CLIENT end of the
  285. # socket, OR the signal received flag is set (thus accept would have returned early without
  286. # having actually accepted a connection.
  287. #
  288. # The third clause (the 'incrementer') is run after each time the body is executed, before the
  289. # test clause is executed again (deciding whether to run the body or drop out... This test
  290. # clause will close the parent process' copy of the CLIENT file handle since (see body below)
  291. # after the body executes, all communication with the socket referred to by that file handle
  292. # will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
  293. # file handle to be used again in the parent process for the next accepted incoming connection.
  294. for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
  295. {
  296. 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
  297. print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
  298. spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
  299. }