class Foreman::Engine

Constants

HANDLED_SIGNALS

The signals that the engine cares about.

Attributes

env[R]
options[R]
processes[R]

Public Class Methods

new(options={}) click to toggle source

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

base_port() click to toggle source

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() click to toggle source

Clear the processes registered to this Engine

# File lib/foreman/engine.rb, line 158
def clear
  @names     = {}
  @processes = []
end
each_process() { |name, process(name)| ... } click to toggle source

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
environment() click to toggle source

deprecated

# File lib/foreman/engine.rb, line 288
def environment
  env
end
formation() click to toggle source

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_hangup() click to toggle source

Handle a HUP signal

# File lib/foreman/engine.rb, line 130
def handle_hangup
  system "SIGHUP received, starting shutdown"
  @shutdown = true
end
handle_interrupt() click to toggle source

Handle an INT signal

# File lib/foreman/engine.rb, line 123
def handle_interrupt
  system "SIGINT received, starting shutdown"
  @shutdown = true
end
handle_signal(sig) click to toggle source

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
handle_signal_forward(signal) click to toggle source
# File lib/foreman/engine.rb, line 135
def handle_signal_forward(signal)
  system "#{signal} received, forwarding it to children"
  kill_children signal
end
handle_term_signal() click to toggle source

Handle a TERM signal

# File lib/foreman/engine.rb, line 116
def handle_term_signal
  system "SIGTERM received, starting shutdown"
  @shutdown = true
end
kill_children(signal="SIGTERM") click to toggle source

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
killall(signal="SIGTERM") click to toggle source

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_env(filename) click to toggle source

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
load_procfile(filename) click to toggle source

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
notice_signal() click to toggle source

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
port_for(process, instance, base=nil) click to toggle source

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
process(name) click to toggle source

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
process_names() click to toggle source

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(name, command, options={}) click to toggle source

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
register_signal_handlers() click to toggle source

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
restore_default_signal_handlers() click to toggle source

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
root() click to toggle source

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() click to toggle source

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

check_for_termination() click to toggle source
# 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
create_pipe() click to toggle source

Helpers ##########################################################

# File lib/foreman/engine.rb, line 310
def create_pipe
  IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
end
flush_reader(reader) click to toggle source
# 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
handle_io(readers) click to toggle source
# 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
handle_signals() click to toggle source
# File lib/foreman/engine.rb, line 387
def handle_signals
  while sig = Thread.main[:signal_queue].shift
    self.handle_signal(sig)
  end
end
name_for(pid) click to toggle source
# File lib/foreman/engine.rb, line 314
def name_for(pid)
  process, index = @running[pid]
  name_for_index(process, index)
end
name_for_index(process, index) click to toggle source
# File lib/foreman/engine.rb, line 319
def name_for_index(process, index)
  [ @names[process], index.to_s ].compact.join(".")
end
output(name, data) click to toggle source
# File lib/foreman/engine.rb, line 300
def output(name, data)
  raise TypeError, "must use a subclass of Foreman::Engine"
end
output_with_mutex(name, message) click to toggle source
# File lib/foreman/engine.rb, line 333
def output_with_mutex(name, message)
  @mutex.synchronize do
    output name, message
  end
end
parse_formation(formation) click to toggle source
# 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
read_self_pipe() click to toggle source
# 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
shutdown() click to toggle source
# File lib/foreman/engine.rb, line 304
def shutdown
  raise TypeError, "must use a subclass of Foreman::Engine"
end
spawn_processes() click to toggle source

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
startup() click to toggle source

Engine API ######################################################

# File lib/foreman/engine.rb, line 296
def startup
  raise TypeError, "must use a subclass of Foreman::Engine"
end
system(message) click to toggle source
# File lib/foreman/engine.rb, line 339
def system(message)
  output_with_mutex "system", message
end
terminate_gracefully() click to toggle source
# 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
termination_message_for(status) click to toggle source
# 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
wait_for_shutdown_or_child_termination() click to toggle source
# 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
watch_for_output() click to toggle source
# 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