| 1 | require 'optparse' |
|---|
| 2 | require 'net/http' |
|---|
| 3 | require 'uri' |
|---|
| 4 | |
|---|
| 5 | if RUBY_PLATFORM =~ /mswin32/ then abort("Reaper is only for Unix") end |
|---|
| 6 | |
|---|
| 7 | # Instances of this class represent a single running process. Processes may |
|---|
| 8 | # be queried by "keyword" to find those that meet a specific set of criteria. |
|---|
| 9 | class ProgramProcess |
|---|
| 10 | class << self |
|---|
| 11 | |
|---|
| 12 | # Searches for all processes matching the given keywords, and then invokes |
|---|
| 13 | # a specific action on each of them. This is useful for (e.g.) reloading a |
|---|
| 14 | # set of processes: |
|---|
| 15 | # |
|---|
| 16 | # ProgramProcess.process_keywords(:reload, "basecamp") |
|---|
| 17 | def process_keywords(action, *keywords) |
|---|
| 18 | processes = keywords.collect { |keyword| find_by_keyword(keyword) }.flatten |
|---|
| 19 | |
|---|
| 20 | if processes.empty? |
|---|
| 21 | puts "Couldn't find any process matching: #{keywords.join(" or ")}" |
|---|
| 22 | else |
|---|
| 23 | processes.each do |process| |
|---|
| 24 | puts "#{action.capitalize}ing #{process}" |
|---|
| 25 | process.send(action) |
|---|
| 26 | end |
|---|
| 27 | end |
|---|
| 28 | end |
|---|
| 29 | |
|---|
| 30 | # Searches for all processes matching the given keyword: |
|---|
| 31 | # |
|---|
| 32 | # ProgramProcess.find_by_keyword("basecamp") |
|---|
| 33 | def find_by_keyword(keyword) |
|---|
| 34 | process_lines_with_keyword(keyword).split("\n").collect { |line| |
|---|
| 35 | next if line =~ /inq|ps axww|grep|spawn-fcgi|spawner|reaper/ |
|---|
| 36 | pid, *command = line.split |
|---|
| 37 | new(pid, command.join(" ")) |
|---|
| 38 | }.compact |
|---|
| 39 | end |
|---|
| 40 | |
|---|
| 41 | private |
|---|
| 42 | def process_lines_with_keyword(keyword) |
|---|
| 43 | `ps axww -o 'pid command' | grep #{keyword}` |
|---|
| 44 | end |
|---|
| 45 | end |
|---|
| 46 | |
|---|
| 47 | # Create a new ProgramProcess instance that represents the process with the |
|---|
| 48 | # given pid, running the given command. |
|---|
| 49 | def initialize(pid, command) |
|---|
| 50 | @pid, @command = pid, command |
|---|
| 51 | end |
|---|
| 52 | |
|---|
| 53 | # Forces the (rails) application to reload by sending a +HUP+ signal to the |
|---|
| 54 | # process. |
|---|
| 55 | def reload |
|---|
| 56 | `kill -s HUP #{@pid}` |
|---|
| 57 | end |
|---|
| 58 | |
|---|
| 59 | # Forces the (rails) application to gracefully terminate by sending a |
|---|
| 60 | # +TERM+ signal to the process. |
|---|
| 61 | def graceful |
|---|
| 62 | `kill -s TERM #{@pid}` |
|---|
| 63 | end |
|---|
| 64 | |
|---|
| 65 | # Forces the (rails) application to terminate immediately by sending a -9 |
|---|
| 66 | # signal to the process. |
|---|
| 67 | def kill |
|---|
| 68 | `kill -9 #{@pid}` |
|---|
| 69 | end |
|---|
| 70 | |
|---|
| 71 | # Send a +USR1+ signal to the process. |
|---|
| 72 | def usr1 |
|---|
| 73 | `kill -s USR1 #{@pid}` |
|---|
| 74 | end |
|---|
| 75 | |
|---|
| 76 | # Force the (rails) application to restart by sending a +USR2+ signal to the |
|---|
| 77 | # process. |
|---|
| 78 | def restart |
|---|
| 79 | `kill -s USR2 #{@pid}` |
|---|
| 80 | end |
|---|
| 81 | |
|---|
| 82 | def to_s #:nodoc: |
|---|
| 83 | "[#{@pid}] #{@command}" |
|---|
| 84 | end |
|---|
| 85 | end |
|---|
| 86 | |
|---|
| 87 | OPTIONS = { |
|---|
| 88 | :action => "restart", |
|---|
| 89 | :dispatcher => File.expand_path(RAILS_ROOT + '/public/dispatch.fcgi') |
|---|
| 90 | } |
|---|
| 91 | |
|---|
| 92 | ARGV.options do |opts| |
|---|
| 93 | opts.banner = "Usage: reaper [options]" |
|---|
| 94 | |
|---|
| 95 | opts.separator "" |
|---|
| 96 | |
|---|
| 97 | opts.on <<-EOF |
|---|
| 98 | Description: |
|---|
| 99 | The reaper is used to restart, reload, gracefully exit, and forcefully exit FCGI processes |
|---|
| 100 | running a Rails Dispatcher. This is commonly done when a new version of the application |
|---|
| 101 | is available, so the existing processes can be updated to use the latest code. |
|---|
| 102 | |
|---|
| 103 | The reaper actions are: |
|---|
| 104 | |
|---|
| 105 | * restart : Restarts the application by reloading both application and framework code |
|---|
| 106 | * reload : Only reloads the application, but not the framework (like the development environment) |
|---|
| 107 | * graceful: Marks all of the processes for exit after the next request |
|---|
| 108 | * kill : Forcefully exists all processes regardless of whether they're currently serving a request |
|---|
| 109 | |
|---|
| 110 | Restart is the most common and default action. |
|---|
| 111 | |
|---|
| 112 | Examples: |
|---|
| 113 | reaper # restarts the default dispatcher |
|---|
| 114 | reaper -a reload |
|---|
| 115 | reaper -a exit -d /my/special/dispatcher.fcgi |
|---|
| 116 | EOF |
|---|
| 117 | |
|---|
| 118 | opts.on(" Options:") |
|---|
| 119 | |
|---|
| 120 | opts.on("-a", "--action=name", "reload|graceful|kill (default: #{OPTIONS[:action]})", String) { |OPTIONS[:action]| } |
|---|
| 121 | opts.on("-d", "--dispatcher=path", "default: #{OPTIONS[:dispatcher]}", String) { |OPTIONS[:dispatcher]| } |
|---|
| 122 | |
|---|
| 123 | opts.separator "" |
|---|
| 124 | |
|---|
| 125 | opts.on("-h", "--help", "Show this help message.") { puts opts; exit } |
|---|
| 126 | |
|---|
| 127 | opts.parse! |
|---|
| 128 | end |
|---|
| 129 | |
|---|
| 130 | ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher]) |
|---|