#!/usr/local/bin/ruby # # This script is intended to live in /etc/rc.d/init.d. It provides basic # functionality to start, stop, and query the status of your Iowa # applications. It was written because I needed to automate the startup # and shutdown of my Iowa apps so that I could be sure that they would be # shut down cleanly and restarted if a machine were rebooted. Also, on # very small servers I like to be able to shut my Iowa apps down and # restart them, automatically, via cron at some regular interval, just to # be paranoid. # # This init script depends on a simple configuration file to tell it # what apps it needs to deal with. The format of the configuration file # is to list the paths to the files to run, one per line. # i.e. # /usr/local/htdocs/foobar.com/doc/twizzler/twizzler.rb # /usr/local/htdocs/dekpr.net/doc/race/go.rb # # If the above were to appear in the configuration file, it would define # two Iowa apps. # # Supported commands in this script are start, stop, restart, and status. # # The script was written on a RedHat Linux system, and has only been # tested on such a system. It should not be difficult to get it to run # on most Unix variants, however. # # ToDo: # # * Make sure this script works under Windows? # require "rbconfig.rb" include Config # If the Sys::ProcTable module is available, we try to use that. # You can get Sys::ProcTable here: # http://raa.ruby-lang.org/list.rhtml?name=sys-proctable begin require 'sys/proctable' include Sys Has_Sys_ProcTable = true rescue LoadError Has_Sys_ProcTable = false end # Location in which to find the configuration file. Conf = '/etc/iowa_apps.conf' # The script maintains a PID database of what it has started and what it # expects to be running. This is the name of the file used to store that # database. PIDdb = '/var/run/iowa/PIDdb' # The directory where the Unix domain Iowa sockets can be found. Socket_dir = '/tmp' # Where the Ruby executable can be found. Ruby_bin = "#{CONFIG["bindir"]}/ruby" # The 'ps' command to use to check for running apps. Make sure that this # is correct for your platform if you are not using Sys::ProcTable. PS_command = 'ps auxw --width 500' #Linux & similar using procps #PS_command = 'ps -eaf --width 500' #Solaris (& similar?) @socket_list = {} @pid_db = {} # Check the process table to see if the various apps are running. # If you don't have Sys::ProcTable, 'ps' will be executed and parsed. # Make sure you've properly defined the correct 'ps' command for your # platform up above. def check_apps flag = false ps_out = [] if !Has_Sys_ProcTable IO.popen(PS_command).each do |line| ps_out.push line end else ProcTable.ps do |ps| ps_out.push ps end end begin File.open(Conf,'r').each do |filename| filename.chomp! # Skip this app if we were provided with a filepath argument, and this # app doesn't match it. next if (ARGV[1].to_s != '' and ARGV[1].to_s != filename) if Has_Sys_ProcTable ps_out.each do |ps| flag = true if (ps.cmdline =~ /ruby\s+#{filename}/) end else ps_out.each do |line| flag = true if (line =~ /ruby\s+#{filename}/) end end end rescue Exception raise $! end if (flag) raise "Iowa application(s) are already running; can not start application(s)." end end # Delete any old socket file. The code tests to see if an old socket file # is present, and deletes it if it is. def remove_sockets @socket_list.each_value do |socket_name| begin socket_file = "#{Socket_dir}/#{socket_name}" File.delete(socket_file) if FileTest.exist?(socket_file) rescue Exception puts "Error deleting socket file #{socket_file}:\n#{$!}" end end end # Start the applications. We make sure they aren't already running, # clear any old socket files, and then start each of them, capturing # their PIDs as we do so. def start_apps check_apps remove_sockets _start_apps(ARGV[1]) end def _start_apps(appname) File.open(Conf,'r').each do |filename| process_pid = 0 filename.chomp! next if (appname.to_s != '' and appname.to_s != filename) # Extract the directory that the file is in from the file path. # I realize that this won't work, as written, on a Windows box. At # the very least one needs to use File::SEPARATOR instead of '/'. # I haven't done that, though, because I don't do these things on # Windows and really have no idea if Iowa will even work under # Windows. It mostly seems like it should, and this script mostly # seems like it should, too. If (when?) I confirm this, I'll change # this script to work under Windows, as well. filename =~ /^(.*?)\/[^\/]+$/ executable_directory = $1 # An assumption is being made here that, regardless of whether the # Iowa app is being started with Iowa.startDaemon or through some # other technique, that the code will return a line to STDOUT # that contains 'PID 12345' where 12345 is the PID of the daemon # process. Once we get that, we can bail out of processing any # more of the process output, since we don't care about anything # else it might have to say. Dir.chdir executable_directory IO.popen("#{Ruby_bin} #{filename}").each do |line| if (line =~ /PID\s+(\d+)/) process_pid = $1.to_i break end end # Report back on our STDOUT that the app has been started. puts "Started #{filename} with PID #{process_pid}" # Save the pid info for the app. @pid_db[filename] = process_pid end end # Kill the Iowa apps. def stop_apps ps_out = [] if !Has_Sys_ProcTable IO.popen(PS_command).each do |line| ps_out.push line end else ProcTable.ps do |ps| ps_out.push ps end end File.open(Conf,'r').each do |filename| filename.chomp! next if (ARGV[1].to_s != '' and ARGV[1].to_s != filename) pid_to_kill = 0 # First we check to see if we have mention of the app in the PID # database. Normally, we should. If we don't have mention of the # app in the PID database, then the process table must be searched # to attempt to find the app and it's PID so that we have something # to kill. if ((@pid_db[filename].to_i || 0) > 0) pid_to_kill = @pid_db[filename].to_i else if Has_Sys_ProcTable ps_out.each do |ps| if (ps.cmdline =~ /ruby\s+#{filename}\s*$/) pid_to_kill = ps.pid.to_i break end end else ps_out.each do |line| if (line =~ /ruby\s+#{filename}\s*$/) line =~ /^\S+\s+(\d+)/ pid_to_kill = $1.to_i break end end end end # Make sure that a PID to kill was found. This is paranoia in case # the app were killed manually or died unexpectedly at some point. # it'd be a real bummer to kill someone's shell session just because # they had the dumb luck to inherit a PID number that used to belong # to an Iowa app. k = false if (pid_to_kill > 0) begin Process.kill('SIGTERM',pid_to_kill) k = true rescue Exception puts "Error killing PID #{pid_to_kill} (#{filename})" end puts "Stopped PID #{pid_to_kill} (#{filename})" if k else puts "Warning: Can't find a PID for #{filename}" end @pid_db.delete filename if k end end # Check the process table to see if the defined apps are running or not. # If monitor_flag is true, then status_apps will issue a restart command # for any non-running apps that it finds. def status_apps(monitor_flag) ps_out = [] if !Has_Sys_ProcTable IO.popen(PS_command).each do |line| ps_out.push line end else ProcTable.ps do |ps| ps_out.push ps end end File.open(Conf,'r').each do |filename| filename.chomp! next if (ARGV[1].to_s != '' and ARGV[1].to_s != filename) pid_should_be = @pid_db[filename] || -2 pid_match = catch (:done) do if Has_Sys_ProcTable ps_out.each do |ps| if ((ps.pid.to_s == pid_should_be.to_s) and (ps.cmdline =~ /ruby\s+#{filename}/)) throw :done, pid_should_be end end -1 else ps_out.each do |line| if (line =~ /^\S+\s+#{pid_should_be}.*ruby\s+#{filename}/) throw :done,pid_should_be end end -1 end end # Normally, all of the PIDs in the PID database should exist. # If the process is found to be running at the expecte PID, # it is reported as running. # If it is not found running at the expecte PID, but an apparent match # to the app can be found in the process table under another PID, # it is reported as "apparently" running. # And if no match can be found at all, it is reported as not running. if (pid_should_be == pid_match) puts "#{filename} RUNNING with PID #{pid_should_be}" else pid_match = catch (:done) do if Has_Sys_ProcTable ps_out.each do |ps| if (ps.cmdline =~ /ruby\s+#{filename}/) throw :done, ps.pid.to_i end end -1 else ps_out.each do |line| if (line =~ /^\S+\s+(\d+).*ruby\s+#{filename}\s*$/) m = $1.to_i throw :done,m end end -1 end end if (pid_match > -1) puts "#{filename} apparently RUNNING with PID #{pid_match}" else if monitor_flag puts "#{filename} NOT RUNNING; restarting..." _start_apps filename else puts "#{filename} NOT RUNNING" end end end end end # Here's where the execution actually starts. begin File.open(Conf,'r') do |conf_file| conf_file.each do |filename| begin filename.chomp! ##### BROKEN ##### ### ### Socket now defined in a config file, and can be a port ### number instead of a filename. ##### # Since the actual name of an Iowa app is unique on a given # system because the socket name is based off of the app name, # the code parses the file for the Iowa.run() invocation, # and then extracts the app name from it. # This works, but I'm not sure I like it as it depends on # the app name always being declared statically within the # code. This will probably change. Odds are that I will # make the app name an additional data item stored in the # configuration file. File.open("#{filename}",'r').each do |line| if (line =~ /Iowa.run\('([^']+)'\)/) app_name = $1 socket_name = "iowa_#{app_name}" @socket_list[app_name] = socket_name end end rescue Exception raise $! end end end rescue Exception raise $! end # If the PID db file doesn't exist, we create it. unless (FileTest.exists? PIDdb) File.open(PIDdb,'w') do |pid_file| pid_file.syswrite Marshal.dump(@pid_db) end end # Make sure that the database file can be read. unless (FileTest.readable? PIDdb) raise "The PID database file, #{PIDdb} is not readable." end File.open(PIDdb,'r') do |pid_file| data = pid_file.read @pid_db = Marshal.load(data) end # Now figure out what we are being asked to do. case ARGV[0] when /restart/i stop_apps start_apps when /start/i start_apps when /stop/i stop_apps when /status/i status_apps false when /monitor/i status_apps true else puts "I don't understand what you want" end # And finally, write the pid db back out to a file. File.open(PIDdb,'w') do |pid_file| pid_file.syswrite Marshal.dump(@pid_db) end