class Foreman::Engine
Constants
- HANDLED_SIGNALS
The signals that the engine cares about.
Attributes
Public Class Methods
Create an Engine
for running processes
@param [Hash] options
@option options [String] :formation (all=1) The process formation to use @option options [Fixnum] :port (5000) The base port to assign to processes @option options [String] :root (Dir.pwd) The root directory from which to run processes
# File lib/foreman/engine.rb, line 27 def initialize(options={}) @options = options.dup @options[:formation] ||= "all=1" @options[:timeout] ||= 5 @env = {} @mutex = Mutex.new @names = {} @processes = [] @running = {} @readers = {} @shutdown = false # Self-pipe for deferred signal-handling (ala djb: http://cr.yp.to/docs/selfpipe.html) reader, writer = create_pipe reader.close_on_exec = true if reader.respond_to?(:close_on_exec) writer.close_on_exec = true if writer.respond_to?(:close_on_exec) @selfpipe = { :reader => reader, :writer => writer } # Set up a global signal queue # http://blog.rubybestpractices.com/posts/ewong/016-Implementing-Signal-Handlers.html Thread.main[:signal_queue] = [] end
Public Instance Methods
Get the base port for this foreman instance
@returns [Fixnum] port The base port
# File lib/foreman/engine.rb, line 283 def base_port (options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i end
Clear the processes registered to this Engine
# File lib/foreman/engine.rb, line 158 def clear @names = {} @processes = [] end
Yield each Process
in order
# File lib/foreman/engine.rb, line 250 def each_process process_names.each do |name| yield name, process(name) end end
deprecated
# File lib/foreman/engine.rb, line 288 def environment env end
Get the process formation
@returns [Fixnum] The formation count for the specified process
# File lib/foreman/engine.rb, line 226 def formation @formation ||= parse_formation(options[:formation]) end
Handle a HUP signal
# File lib/foreman/engine.rb, line 130 def handle_hangup system "SIGHUP received, starting shutdown" @shutdown = true end
Handle an INT signal
# File lib/foreman/engine.rb, line 123 def handle_interrupt system "SIGINT received, starting shutdown" @shutdown = true end
Invoke the real handler for signal sig
. This shouldn’t be called directly by signal handlers, as it might invoke code which isn’t re-entrant.
@param [Symbol] sig the name of the signal to be handled
# File lib/foreman/engine.rb, line 99 def handle_signal(sig) case sig when :TERM handle_term_signal when :INT handle_interrupt when :HUP handle_hangup when *HANDLED_SIGNALS handle_signal_forward(sig) else system "unhandled signal #{sig}" end end
# File lib/foreman/engine.rb, line 135 def handle_signal_forward(signal) system "#{signal} received, forwarding it to children" kill_children signal end
Handle a TERM signal
# File lib/foreman/engine.rb, line 116 def handle_term_signal system "SIGTERM received, starting shutdown" @shutdown = true end
Send a signal to all processes started by this Engine
@param [String] signal The signal to send to each process
# File lib/foreman/engine.rb, line 189 def kill_children(signal="SIGTERM") if Foreman.windows? @running.each do |pid, (process, index)| system "sending #{signal} to #{name_for(pid)} at pid #{pid}" begin Process.kill(signal, pid) rescue Errno::ESRCH, Errno::EPERM end end else begin pids = @running.keys.compact Process.kill signal, *pids unless pids.empty? rescue Errno::ESRCH, Errno::EPERM end end end
Send a signal to the whole process group.
@param [String] signal The signal to send
# File lib/foreman/engine.rb, line 211 def killall(signal="SIGTERM") if Foreman.windows? kill_children(signal) else begin Process.kill "-#{signal}", Process.pid rescue Errno::ESRCH, Errno::EPERM end end end
Load a .env file into the env
for this Engine
@param [String] filename A .env file to load into the environment
# File lib/foreman/engine.rb, line 179 def load_env(filename) Foreman::Env.new(filename).entries do |name, value| @env[name] = value end end
Register processes by reading a Procfile
@param [String] filename A Procfile from which to read processes to register
# File lib/foreman/engine.rb, line 167 def load_procfile(filename) options[:root] ||= File.dirname(filename) Foreman::Procfile.new(filename).entries do |name, command| register name, command, :cwd => options[:root] end self end
Wake the main thread up via the selfpipe when there’s a signal
# File lib/foreman/engine.rb, line 85 def notice_signal @selfpipe[:writer].write_nonblock( '.' ) rescue Errno::EAGAIN # Ignore writes that would block rescue Errno::EINTR # Retry if another signal arrived while writing retry end
Get the port for a given process and offset
@param [Foreman::Process] process A Process
associated with this engine @param [Fixnum] instance The instance of the process
@returns [Fixnum] port The port to use for this instance of this process
# File lib/foreman/engine.rb, line 271 def port_for(process, instance, base=nil) if base base + (@processes.index(process.process) * 100) + (instance - 1) else base_port + (@processes.index(process) * 100) + (instance - 1) end end
Get the Process
for a specifid name
@param [String] name The process name
@returns [Foreman::Process] The Process
for the specified name
# File lib/foreman/engine.rb, line 244 def process(name) @names.invert[name] end
List the available process names
@returns [Array] A list of process names
# File lib/foreman/engine.rb, line 234 def process_names @processes.map { |p| @names[p] } end
Register a process to be run by this Engine
@param [String] name A name for this process @param [String] command The command to run @param [Hash] options
@option options [Hash] :env A custom environment for this process
# File lib/foreman/engine.rb, line 148 def register(name, command, options={}) options[:env] ||= env options[:cwd] ||= File.dirname(command.split(" ").first) process = Foreman::Process.new(command, options) @names[process] = name @processes << process end
Set up deferred signal handlers
# File lib/foreman/engine.rb, line 67 def register_signal_handlers HANDLED_SIGNALS.each do |sig| if ::Signal.list.include? sig.to_s trap(sig) { Thread.main[:signal_queue] << sig ; notice_signal } end end end
Unregister deferred signal handlers
# File lib/foreman/engine.rb, line 77 def restore_default_signal_handlers HANDLED_SIGNALS.each do |sig| trap(sig, :DEFAULT) if ::Signal.list.include? sig.to_s end end
Get the root directory for this Engine
@returns [String] The root directory
# File lib/foreman/engine.rb, line 260 def root File.expand_path(options[:root] || Dir.pwd) end
Start the processes registered to this Engine
# File lib/foreman/engine.rb, line 54 def start register_signal_handlers startup spawn_processes watch_for_output sleep 0.1 wait_for_shutdown_or_child_termination shutdown exit(@exitstatus) if @exitstatus end
Private Instance Methods
# File lib/foreman/engine.rb, line 442 def check_for_termination # Check if any of the children have died off pid, status = begin Process.wait2(-1, Process::WNOHANG) rescue Errno::ECHILD return nil end # record the exit status @exitstatus ||= status.exitstatus if status # If no childred have died, nothing to do here return nil unless pid # Log the information about the process that exited output_with_mutex name_for(pid), termination_message_for(status) # Delete it from the list of running processes and return its pid @running.delete(pid) return pid end
Helpers ##########################################################
# File lib/foreman/engine.rb, line 310 def create_pipe IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY") end
# File lib/foreman/engine.rb, line 353 def flush_reader(reader) until reader.eof? data = reader.gets output_with_mutex name_for(@readers.key(reader)), data end end
# File lib/foreman/engine.rb, line 393 def handle_io(readers) readers.each do |reader| next if reader == @selfpipe[:reader] if reader.eof? @readers.delete_if { |key, value| value == reader } else data = reader.gets output_with_mutex name_for(@readers.invert[reader]), data end end end
# File lib/foreman/engine.rb, line 387 def handle_signals while sig = Thread.main[:signal_queue].shift self.handle_signal(sig) end end
# File lib/foreman/engine.rb, line 314 def name_for(pid) process, index = @running[pid] name_for_index(process, index) end
# File lib/foreman/engine.rb, line 319 def name_for_index(process, index) [ @names[process], index.to_s ].compact.join(".") end
# File lib/foreman/engine.rb, line 300 def output(name, data) raise TypeError, "must use a subclass of Foreman::Engine" end
# File lib/foreman/engine.rb, line 333 def output_with_mutex(name, message) @mutex.synchronize do output name, message end end
# File lib/foreman/engine.rb, line 323 def parse_formation(formation) pairs = formation.to_s.gsub(/\s/, "").split(",") pairs.inject(Hash.new(0)) do |ax, pair| process, amount = pair.split("=") process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i ax end end
# File lib/foreman/engine.rb, line 381 def read_self_pipe @selfpipe[:reader].read_nonblock(11) rescue Errno::EAGAIN, Errno::EINTR, Errno::EBADF, Errno::EWOULDBLOCK # ignore end
# File lib/foreman/engine.rb, line 304 def shutdown raise TypeError, "must use a subclass of Foreman::Engine" end
Engine
###########################################################
# File lib/foreman/engine.rb, line 362 def spawn_processes @processes.each do |process| 1.upto(formation[@names[process]]) do |n| reader, writer = create_pipe begin pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s, "PS" => name_for_index(process, n) }) writer.puts "started with pid #{pid}" rescue Errno::ENOENT writer.puts "unknown command: #{process.command}" end @running[pid] = [process, n] @readers[pid] = reader end end end
Engine
API ######################################################
# File lib/foreman/engine.rb, line 296 def startup raise TypeError, "must use a subclass of Foreman::Engine" end
# File lib/foreman/engine.rb, line 339 def system(message) output_with_mutex "system", message end
# File lib/foreman/engine.rb, line 464 def terminate_gracefully restore_default_signal_handlers # Tell all children to stop gracefully if Foreman.windows? system "sending SIGKILL to all processes" kill_children "SIGKILL" else system "sending SIGTERM to all processes" kill_children "SIGTERM" end # Wait for all children to stop or until the time comes to kill them all start_time = Time.now while Time.now - start_time <= options[:timeout] return if @running.empty? check_for_termination # Sleep for a moment and do not blow up if more signals are coming our way begin sleep(0.1) rescue Exception # noop end end # Ok, we have no other option than to kill all of our children system "sending SIGKILL to all processes" kill_children "SIGKILL" end
# File lib/foreman/engine.rb, line 343 def termination_message_for(status) if status.exited? "exited with code #{status.exitstatus}" elsif status.signaled? "terminated by SIG#{Signal.list.invert[status.termsig]}" else "died a mysterious death" end end
# File lib/foreman/engine.rb, line 422 def wait_for_shutdown_or_child_termination loop do # Stop if it is time to shut down (asked via a signal) break if @shutdown # Stop if any of the children died break if check_for_termination # Sleep for a moment and do not blow up if any signals are coming our way begin sleep(1) rescue Exception # noop end end # Ok, we have exited from the main loop, time to shut down gracefully terminate_gracefully end
# File lib/foreman/engine.rb, line 406 def watch_for_output Thread.new do begin loop do io = IO.select([@selfpipe[:reader]] + @readers.values, nil, nil, 30) read_self_pipe handle_signals handle_io(io ? io.first : []) end rescue Exception => ex puts ex.message puts ex.backtrace end end end