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 28
def initialize(options={})
  @options = options.dup

  @options[:formation] ||= (options[:concurrency] || "all=1")
  @options[:timeout] ||= 5

  @env       = {}
  @mutex     = Mutex.new
  @names     = {}
  @processes = []
  @running   = {}
  @readers   = {}

  # 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 274
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 150
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 241
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 279
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 217
def formation
  @formation ||= parse_formation(options[:formation])
end
handle_hangup() click to toggle source

Handle a HUP signal

# File lib/foreman/engine.rb, line 127
def handle_hangup
  puts "SIGHUP received"
  terminate_gracefully
end
handle_interrupt() click to toggle source

Handle an INT signal

# File lib/foreman/engine.rb, line 120
def handle_interrupt
  puts "SIGINT received"
  terminate_gracefully
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 98
def handle_signal(sig)
  case sig
  when :TERM
    handle_term_signal
  when :INT
    handle_interrupt
  when :HUP
    handle_hangup
  else
    system "unhandled signal #{sig}"
  end
end
handle_term_signal() click to toggle source

Handle a TERM signal

# File lib/foreman/engine.rb, line 113
def handle_term_signal
  puts "SIGTERM received"
  terminate_gracefully
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 181
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
      Process.kill signal, *@running.keys unless @running.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 202
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 171
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 159
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 84
def notice_signal
  @selfpipe[:writer].write_nonblock( '.' )
rescue Errno::EAGAIN
  # Ignore writes that would block
rescue Errno::EINT
  # 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 262
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 235
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 225
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 140
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 66
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 76
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 251
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
  watch_for_termination { terminate_gracefully }
  shutdown
end

Private Instance Methods

create_pipe() click to toggle source

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

# File lib/foreman/engine.rb, line 301
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 344
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 384
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 378
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 305
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 310
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 291
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 324
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 314
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 372
def read_self_pipe
  @selfpipe[:reader].read_nonblock(11)
rescue Errno::EAGAIN, Errno::EINTR, Errno::EBADF
  # ignore
end
shutdown() click to toggle source
# File lib/foreman/engine.rb, line 295
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 353
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 287
def startup
  raise TypeError, "must use a subclass of Foreman::Engine"
end
system(message) click to toggle source
# File lib/foreman/engine.rb, line 330
def system(message)
  output_with_mutex "system", message
end
terminate_gracefully() click to toggle source
# File lib/foreman/engine.rb, line 422
def terminate_gracefully
  return if @terminating
  restore_default_signal_handlers
  @terminating = true
  if Foreman.windows?
    system  "sending SIGKILL to all processes"
    kill_children "SIGKILL"
  else
    system  "sending SIGTERM to all processes"
    kill_children "SIGTERM"
  end
  Timeout.timeout(options[:timeout]) do
    watch_for_termination while @running.length > 0
  end
rescue Timeout::Error
  system  "sending SIGKILL to all processes"
  kill_children "SIGKILL"
end
termination_message_for(status) click to toggle source
# File lib/foreman/engine.rb, line 334
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
watch_for_output() click to toggle source
# File lib/foreman/engine.rb, line 397
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
watch_for_termination() { || ... } click to toggle source
# File lib/foreman/engine.rb, line 413
def watch_for_termination
  pid, status = Process.wait2
  output_with_mutex name_for(pid), termination_message_for(status)
  @running.delete(pid)
  yield if block_given?
  pid
rescue Errno::ECHILD
end