class Sockd::Runner

Attributes

name[R]
options[R]

Public Class Methods

new(name = nil, options = {}) { |self| ... } click to toggle source
# File lib/sockd/runner.rb, line 16
def initialize(name = nil, options = {}, &block)
  @name = name || File.basename($0)
  @options = {
    :host      => "127.0.0.1",
    :port      => 0,
    :socket    => false,
    :mode      => 0660,
    :daemonize => true,
    :pid_path  => "/var/run/#{safe_name}.pid",
    :log_path  => false,
    :force     => false,
    :user      => false,
    :group     => false
  }.merge(options)

  [:setup, :teardown, :handle].each do |opt|
    self.public_send(opt, &options[opt]) if options[opt].respond_to?(:call)
  end

  yield self if block_given?
end

Public Instance Methods

handle(message = nil, socket = nil, &block) click to toggle source

define our socket handler by providing a block, or trigger the callback with the provided message @runner.handle { |msg| if msg == 'foo' then return 'bar' … }

# File lib/sockd/runner.rb, line 65
def handle(message = nil, socket = nil, &block)
  return self if block_given? && @handle = block
  raise ArgumentError, "no message handler provided" unless @handle
  @handle.call(message, socket)
end
log(message) click to toggle source

output a timestamped log message

# File lib/sockd/runner.rb, line 161
def log(message)
  puts Time.now.strftime('[%d-%b-%Y %H:%M:%S] ') + message
end
options=(val) click to toggle source

merge options when set with self.options = {…}

# File lib/sockd/runner.rb, line 39
def options=(val)
  @options.merge!(val)
end
restart() click to toggle source

restart our service

# File lib/sockd/runner.rb, line 142
def restart
  stop
  start
end
safe_name() click to toggle source

generate a path-safe and username-safe string from our daemon name

# File lib/sockd/runner.rb, line 44
def safe_name
  name.gsub(/(^[0-9]*|[^0-9a-z])/i, '')
end
send(message, timeout = 30) click to toggle source

send a message to a running service and return the response

# File lib/sockd/runner.rb, line 148
def send(message, timeout = 30)
  client do |sock|
    sock.write "#{message}\r\n"
    ready = IO.select([sock], nil, nil, timeout)
    raise ServiceError, "timed out waiting for server response" unless ready
    sock.recv(256)
  end
rescue Errno::ECONNREFUSED, Errno::ENOENT
  raise ServiceError, "#{name} process not running" unless daemon_running?
  raise ServiceError, "unable to establish connection"
end
setup(&block) click to toggle source

define a “setup” callback by providing a block, or trigger the callback @runner.setup { |opts| Server.new(…) }

# File lib/sockd/runner.rb, line 50
def setup(&block)
  return self if block_given? && @setup = block
  @setup.call(self) if @setup
end
start() click to toggle source

start our service

# File lib/sockd/runner.rb, line 72
def start
  server do |server|

    if options[:daemonize]
      pid = daemon_running?
      raise ServiceError, "#{name} process already running (#{pid})" if pid
      puts "starting #{name} process..."
      unless daemonize
        unless send('ping', 10).chomp == 'pong'
          raise ServiceError, "invalid ping response"
        end
        return self
      end
    end

    drop_privileges options[:user], options[:group]

    setup

    on_interrupt do |signal|
      log "#{signal} received, shutting down..."
      teardown
      # cleanup
      exit 130
    end

    log "listening on #{server.local_address.inspect_sockaddr}"

    while true
      sock = server.accept
      begin
        # wait for input
        if IO.select([sock], nil, nil, 2.0)
          msg = sock.recv(256, Socket::MSG_PEEK)
          if msg.chomp == "ping"
            sock.print "pong\r\n"
          else
            handle msg, sock
          end
        else
          log "connection timed out"
        end
      rescue Errno::EPIPE, Errno::ECONNRESET
        log "connection broken"
      end
      sock.close unless sock.closed?
    end
  end
end
stop() click to toggle source

stop our service

# File lib/sockd/runner.rb, line 123
def stop
  if daemon_running?
    pid = stored_pid
    Process.kill('TERM', pid)
    puts "SIGTERM sent to #{name} (#{pid})"
    if !wait_until(2) { daemon_stopped? pid } && options[:force]
      Process.kill('KILL', pid)
      puts "SIGKILL sent to #{name} (#{pid})"
    end
    raise ServiceError, "unable to stop #{name} process" if daemon_running?
  else
    warn "#{name} process not running"
  end
  self
rescue Errno::EPERM => e
  raise ServiceError, "unable to stop #{name} process (#{e.message})"
end
teardown(&block) click to toggle source

define a “teardown” callback by providing a block, or trigger the callback @runner.teardown { log “shutting down” }

# File lib/sockd/runner.rb, line 57
def teardown(&block)
  return self if block_given? && @teardown = block
  @teardown.call(self) if @teardown
end

Protected Instance Methods

client(&block) click to toggle source

return a UNIXSocket or TCPSocket instance depending on config

# File lib/sockd/runner.rb, line 206
def client(&block)
  if options[:socket]
    UNIXSocket.open(options[:socket], &block)
  else
    TCPSocket.open(options[:host], options[:port], &block)
  end
rescue Errno::EACCES
  sock = options[:socket] || "#{options[:host]}:#{options[:port]}"
  raise ServiceError, "unable to open socket: #{sock} (check permissions)"
end
daemon_running?(pid = nil) click to toggle source

returns the process id if a daemon is running with our pid file

# File lib/sockd/runner.rb, line 259
def daemon_running?(pid = nil)
  pid ||= stored_pid
  Process.kill(0, pid) if pid
  pid
rescue Errno::ESRCH
  false
rescue Errno::EPERM
  pid
end
daemon_stopped?(pid = nil) click to toggle source

reverse of daemon_running?

# File lib/sockd/runner.rb, line 270
def daemon_stopped?(pid = nil)
  !daemon_running? pid
end
daemonize() click to toggle source

daemonize a process. returns true from the forked process, false otherwise

# File lib/sockd/runner.rb, line 225
def daemonize

  # ensure pid file and log file are writable if provided
  pid_path = options[:pid_path] ? writable_file(options[:pid_path]) : nil
  log_path = options[:log_path] ? writable_file(options[:log_path]) : nil

  unless fork
    Process.setsid
    exit if fork
    File.umask 0000
    Dir.chdir "/"

    # save pid file
    File.open(pid_path, 'w') { |f| f.write Process.pid } if pid_path

    # redirect our io
    setup_logging(log_path)

    # trap and ignore SIGHUP
    Signal.trap('HUP') {}

    # trap reopen our log files on SIGUSR1
    Signal.trap('USR1') { setup_logging(log_path) }

    return true
  end

  Process.waitpid
  unless wait_until { daemon_running? }
    raise ServiceError, "failed to start #{name} service"
  end
end
drop_privileges(user, group) click to toggle source

drop privileges to the specified user and group

# File lib/sockd/runner.rb, line 275
def drop_privileges(user, group)
  uid, gid = user_id(user) if user
  gid = group_id(group) if group

  Process::Sys.setgid(gid) if gid
  Process::Sys.setuid(uid) if uid
rescue Errno::EPERM => e
  raise ServiceError, "unable to drop privileges (#{e})"
end
group_id(group) click to toggle source
# File lib/sockd/runner.rb, line 332
def group_id(group)
  Etc.getgrnam(group).gid
rescue ArgumentError
  raise ServiceError, "unable to find group: #{user}"
end
on_interrupt() { |"SIGINT"| ... } click to toggle source

handle process termination signals

# File lib/sockd/runner.rb, line 218
def on_interrupt(&block)
  trap("INT")  { yield "SIGINT" }
  trap("QUIT") { yield "SIGQUIT" }
  trap("TERM") { yield "SIGTERM" }
end
server() { |server| ... } click to toggle source

return a UNIXServer or TCPServer instance depending on config

# File lib/sockd/runner.rb, line 168
def server
  server = if options[:socket]
    begin
      UNIXServer.new(options[:socket])
    rescue Errno::EADDRINUSE
      begin
        send('ping', 20)
      rescue ServiceError
        # socket stale, reopening
        File.delete(options[:socket])
        UNIXServer.new(options[:socket])
      else
        raise ServiceError, "socket #{options[:socket]} already in use by another process"
      end
    end.tap do
      # get user and group ids
      uid, gid = user_id(options[:user]) if options[:user]
      gid = group_id(options[:group]) if options[:group]
      File.chown(uid, gid, options[:socket]) if uid || gid

      # ensure mode is octal if string provided
      options[:mode] = options[:mode].to_i(8) if options[:mode].is_a?(String)
      File.chmod(options[:mode], options[:socket]) if options[:mode] != 0
    end
  else
    TCPServer.new(options[:host], options[:port])
  end
  begin
    yield(server)
  ensure
    server.close
  end
rescue Errno::EACCES, Errno::EADDRINUSE => e
  sock = options[:socket] || "#{options[:host]}:#{options[:port]}"
  raise ServiceError, "unable to open socket: #{sock} (#{e.message})"
end
setup_logging(log_path) click to toggle source

redirect our output as per configuration

# File lib/sockd/runner.rb, line 286
def setup_logging(log_path)
  log_path ||= '/dev/null'
  $stdin.reopen '/dev/null'
  $stdout.reopen(log_path, 'a')
  $stderr.reopen $stdout
  $stdout.sync = true
end
stored_pid() click to toggle source

returns the pid stored in our pid_path

# File lib/sockd/runner.rb, line 295
def stored_pid
  return false unless options[:pid_path]
  path = File.expand_path(options[:pid_path])
  return false unless File.file?(path) && !File.zero?(path)
  File.read(path).chomp.to_i
end
user_id(user) click to toggle source
# File lib/sockd/runner.rb, line 325
def user_id(user)
  user = Etc.getpwnam(user)
  [user.uid, user.gid]
rescue ArgumentError
  raise ServiceError, "unable to find user: #{user}"
end
wait_until(timer = 5, interval = 0.1, &block) click to toggle source
# File lib/sockd/runner.rb, line 317
def wait_until(timer = 5, interval = 0.1, &block)
  until timer < 0 or block.call
    timer -= interval
    sleep interval
  end
  timer > 0
end
writable_file(path) click to toggle source

ensure a writable file exists at the specified path

# File lib/sockd/runner.rb, line 303
def writable_file(path)
  path = File.expand_path(path)
  begin
    FileUtils.mkdir_p(File.dirname(path), :mode => 0755)
    FileUtils.touch path
    File.chmod(0644, path)
  rescue Errno::EACCES, Errno::EISDIR
  end
  unless File.file?(path) && File.writable?(path)
    raise ServiceError, "unable to open file: #{path} (check permissions)"
  end
  path
end