module Workhorse class Daemon def initialize(count: 1, pidfile: nil, quiet: false, &block) @count = count @pidfile = pidfile @quiet = quiet @block = block fail 'Count must be an integer > 0.' unless count.is_a?(Integer) && count > 0 if @pidfile.nil? @pidfile = count > 1 ? 'tmp/pids/workhorse.%i.pid' : 'tmp/pids/workhorse.pid' elsif @count > 1 && !@pidfile.include?('%s') fail 'Pidfile must include placeholder "%s" for worker id when specifying a count > 1.' elsif @count == 0 && @pidfile.include?('%s') fail 'Pidfile must not include placeholder "%s" for worker id when specifying a count of 1.' end end def start code = 0 for_each_worker do |worker_id| pid_file, pid = read_pid(worker_id) if pid_file && pid warn "Worker ##{worker_id}: Already started (PID #{pid})" code = 1 elsif pid_file File.delete pid_file puts "Worker ##{worker_id}: Starting (stale pid file)" start_worker worker_id else warn "Worker ##{worker_id}: Starting" start_worker worker_id end end return code end def stop code = 0 for_each_worker do |worker_id| pid_file, pid = read_pid(worker_id) if pid_file && pid puts "Worker ##{worker_id}: Stopping" stop_worker pid_file, pid elsif pid_file File.delete pid_file puts "Worker ##{worker_id}: Already stopped (stale PID file)" else warn "Worker ##{worker_id}: Already stopped" code = 1 end end return code end def status(quiet: false) code = 0 for_each_worker do |worker_id| pid_file, pid = read_pid(worker_id) if pid_file && pid puts "Worker ##{worker_id}: Running" unless quiet elsif pid_file warn "Worker ##{worker_id}: Not running (stale PID file)" unless quiet code = 1 else warn "Worker ##{worker_id}: Not running" unless quiet code = 1 end end return code end def watch if defined?(Rails) should_be_running = !File.exist?(Rails.root.join('tmp/stop.txt')) else should_be_running = true end if should_be_running && status(quiet: true) != 0 return start else return 0 end end def restart stop return start end private def for_each_worker(&block) 1.upto(@count, &block) end def start_worker(worker_id) pid = fork do $0 = process_name(worker_id) @block.call end IO.write(pid_file_for(worker_id), pid) end def stop_worker(pid_file, pid) loop do begin Process.kill('TERM', pid) rescue Errno::ESRCH break end sleep 1 end File.delete(pid_file) end def process_name(worker_id) if defined?(Rails) path = Rails.root else path = $PROGRAM_NAME end return "Workhorse Worker ##{worker_id}: #{path}" end def process?(pid) return begin Process.getpgid(pid) true rescue Errno::ESRCH false end end def pid_file_for(worker_id) @pidfile % worker_id end def read_pid(worker_id) file = pid_file_for(worker_id) if File.exist?(file) pid = IO.read(file).to_i return file, process?(pid) ? pid : nil else return nil, nil end end end end