#!/usr/bin/perl -w use strict; use Getopt::Std; # Grab the options my %options; getopts('s:t:ce', \%options); #print_usage(), exit if ($options{h}); my $server = $options{s} or die "Must specify a server name\n"; my $restart_time = $options{t} || '0:00'; my $ctf = $options{c} || 0; my $easypoints = $options{e} || 0; # Autoflush $| = 1; # Declare variables use vars (qw($system_status $child_pid $grandchild_pid $wait_pid $restart_hour $restart_minute)); ### ### MAIN PROGRAM ### die "File /usr/share/wine-c/nerf/System/${server}.ini does not exist" unless (-e "/usr/share/wine-c/nerf/System/${server}.ini"); ($restart_hour, $restart_minute) = parse_time_parm(); # Change to the NAB directory chdir "/usr/share/wine-c/nerf/System" or die "Can't change directory ($!)\n"; # Setup signal handlers $SIG{HUP} = \&terminate; $SIG{INT} = \&terminate; $SIG{QUIT} = \&terminate; $SIG{TERM} = \&terminate; # Loop forever. Signals will kill the process. while (1) { # Save off the old log file and compress it if (-s "$server.log") { print "$server.log EXISTS, COMPRESSING & RENAMING\n"; chomp(my $now = `date +%Y-%m-%d+%T`); my $filename = "$server.log.$now.gz"; $system_status = system("gzip <$server.log >$filename"); die "Compress failed ($system_status, $!)\n" if ( ($system_status & 0xffff) != 0); print_error('Warning: Unable to remove old log file') if (! unlink "$server.log"); } else { print "$server.log DOES *NOT* EXIST OR IS EMPTY\n"; } undef $wait_pid; # Move in new .ini file if it exists get_new_ini(); # Import new maps, if map files exist get_new_maps(); # Fork & exec the Nerf Server # We don't want to do the log copy & compression again if there # was a fork/exec/other process-related problem, we just want to # try running it again, so we wrap the process stuff in a while # loop my $try_again = 0; do { if ($child_pid = fork) { # This is the Parent, the controlling process. # First, let's make sure the child started up. # Sleep a bit to give it time to get going. sleep 20; # Check to see if child didn't start my $child_alive = kill 0, $child_pid; unless ($child_alive) { $try_again = 1; print_error("Child didn't start"); } else { # Check if log file doesn't exist. New child # may have started before old one was # completely cleaned up unless (-e "$server.log") { $try_again = 1; kill_child(); print_error('No log file created'); } else { $try_again = run_parent(); } } } elsif (defined $child_pid) { # Child, who will exec the NAB server or exit if # that fails. run_child(); } else { print_error("Fork failed ($!)"); $try_again = 1; } sleep 300 if $try_again; } while ($try_again); } sub parse_time_parm { # Validate time parameter and put it in a standard format die "Restart time must be \"[n]n:nn\" (24-hour format)\n" unless ($restart_time =~ /(\d{1,2}):(\d{2})/); die "Bad time\n" if ($1 < 0 or $1 > 23 or $2 < 0 or $2 > 59); return( $1, $2 ); } sub run_parent { # Nerf Arena Blast gets another wine program going with the same # command line as a child process. Let's get the 'ps' output # and find out its PID. open PSOUT, 'ps -fC wine |'; foreach () { next unless /UCC\.exe/; /\S+\s+(\S+)\s+(\S+)/; $grandchild_pid = $1, last if ($2 == $child_pid); } close PSOUT; print "GRANDCHILD PID = $grandchild_pid\n"; $SIG{CHLD} = \&reaper; # Wait for the specified time and kill the child # Wait an hour until we're close to the hour of the restart time my ($second, $minute, $hour) = localtime; # Convert it all to minute of the day my $restart_mod = ($restart_hour * 60) + $restart_minute; my $now_mod = ($hour * 60) + $minute; # Add a day's worth of minutes to the restart time if it is earlier # in the day than now $restart_mod += 1440 if ($restart_mod <= $now_mod); # Wait to within 10 minutes of the restart time my $sleep_time = ($restart_mod - $now_mod - 10) * 60; # ...but only do this long wait if we're over an hour from the # restart time if ($sleep_time > 3600) { print "SLEEPING FOR $sleep_time SECONDS\n"; sleep $sleep_time; } # A dead child might have woken me up. We'll return 1 to get # the fork loop to try again; return 1 unless ($child_pid); print "SHORT-TIME SLEEPING - WAITING FOR $restart_hour:$restart_minute\n"; # We're close! Now, wake up every 30 seconds until the time arrives ($second, $minute, $hour) = localtime; print "=== $hour:$minute vs $restart_hour:$restart_minute\n"; until ($restart_hour == $hour and $restart_minute == $minute) { sleep 30; ($second, $minute, $hour) = localtime; print "=== $hour:$minute vs $restart_hour:$restart_minute\n"; } print "TIME! ($hour:$minute)\n"; # Kill the child, wait for it to die, then return to the main loop kill_child() if ($child_pid); $SIG{CHLD} = 'IGNORE'; # Move in new .ini file if it exists get_new_ini(); return 0; } sub run_child { my $run_line = 'wine UCC.exe server ' . ($ctf ? 'ctf-psi.nrf?game=CTF.CTFGame' : 'PM-Amateur.nrf') . '?mutator=GameLogger.GameLogger' . ($easypoints ? '?mutator=EasyPoints.EasyPointsMutator' : '') . " ini=${server}.ini log=${server}.log"; print "RUNNING: $run_line\n"; unless (exec $run_line) { print_error("Exec failed ($!)"); exit; } } sub kill_child { print "KILLING $child_pid\n"; kill 15, $child_pid; print "WAITING...\n"; my $wait_pid = waitpid $child_pid, 0; print_error("Warning: There was no child to wait on ($!)") if ($wait_pid < 0); $child_pid = 0; # Kill the grandchild. It's not direcly related to us # so we don't wait for it. kill 15, $grandchild_pid if ($grandchild_pid); # Pause a bit for the network connection & master server # to recognize the death of the server print "PAUSING...\n"; sleep 30; } sub get_new_ini { # Since the server rewrites the .ini file upon exit, we can't # make changes to it (that we'd hope would take effect at the # next run). Therefore, the file should be saved as # ${server}.ini.new, and at this point, now that the child is # gone, we can move it in overtop the regular .ini file. if (-e "/usr/share/wine-c/nerf/System/${server}.ini.new") { print "LOADING NEW INI FILE\n"; print "RENAME FAILED\n" unless (rename "/usr/share/wine-c/nerf/System/${server}.ini.new", "/usr/share/wine-c/nerf/System/${server}.ini"); } } sub get_new_maps { # Look for files called "${server}.maps.*". Pick one and put # its map list in the server's main ini file. my @map_files = glob("${server}.maps.*"); my $num_files = @map_files; return if ($num_files == 0); my $day_num = (localtime)[3]; # The file to use is (currently) today's date modulo the number # of possible files. The 'glob' function serves up its file list # in sorted order, so this ensures we cycle through all the files # over the course of as many days (restarting on the first day of # each month). my $file_to_use = $map_files[$day_num % $num_files]; # NOTE: This script determines which map section (PM or CTF) to # overwrite based on the -c parameter, *not* based on the name # of the map section (if any) in the .map.* file. This is because # it is pointless to overwrite the PM maps when running a CTF # server. However, just to be safe, if you do specify the map # section and it doesn't jive with the -c flag, nothing is done. # NOTE: This script expects that your map section file will have # the entire set of values for the map (Maps[] and MapNum). # It doesn't check for errors or missing entries. The section # name (the line surrounded by [brackets]) is optional in your # map section file. unless (open INI, "<${server}.ini") { print "UNABLE TO OPEN ${server}.ini ($!)\n"; return; } unless (open OUTI, ">${server}.ini.temp") { close INI; print "UNABLE TO OPEN ${server}.ini.temp ($!)\n"; return; } unless (open MAPLIST, "<$file_to_use") { close OUTI; close INI; print "UNABLE TO OPEN $file_to_use ($!)\n"; return; } my $map_section = ($ctf ? '[CTF.CTFMapList]' : '[NerfI.PMmaplist]'); # The variable $print_it will be set to 0 when it should be skipping # the proper section of map files in the original ini file. my $print_it = 1; while () { if (/^\Q$map_section\E/) { print OUTI $_; # Toss the maplist into OUTI and start ignoring # records from INI my $map_record = ''; while (defined $map_record) { $map_record = ; if (defined $map_record) { # Do we have a map section header? if ($map_record =~ /^\[/ and $map_record !~ /\Q$map_section\E/) { print "INCORRECT MAP SECTION NAME IN $file_to_use ($map_record vs $map_section)\n"; close MAPLIST; close OUTI; close INI; unlink "${server}.ini.temp"; return; } next if ($map_record =~ /^\[/); print OUTI $map_record; } } $print_it = 0; } else { print OUTI $_ if $print_it; # Resume sending INI to OUTI if we've gone past the # MapNum entry for this map section. $print_it = 1 if ($print_it == 0 and /^MapNum/); } } close MAPLIST; close OUTI; close INI; # Juuuuuust in case... rename "${server}.ini", "${server}.ini.bak"; rename "${server}.ini.temp", "${server}.ini"; } sub reaper { wait; $child_pid = 0; # loathe sysV: it makes us not only reinstate # the handler, but place it after the wait $SIG{CHLD} = \&reaper; } sub print_error { chomp(my $now = `date`); print STDERR $now, ':', @_, "\n"; } sub terminate { if ($child_pid) { kill 9, $child_pid; waitpid $child_pid, 0; } print_error('Exiting'); exit; }