module Einhorn::Command::Interface

Public Class Methods

command(name, description=nil, &code) click to toggle source

Commands

# File lib/einhorn/command/interface.rb, line 200
def self.command(name, description=nil, &code)
  @@commands[name] = {:description => description, :code => code}
end
command_descriptions() click to toggle source
# File lib/einhorn/command/interface.rb, line 262
def self.command_descriptions
  command_specs = @@commands.select do |_, spec|
    spec[:description]
  end.sort_by {|name, _| name}

  command_specs.map do |name, spec|
    "#{name}: #{spec[:description]}"
  end.join("\n")
end
command_server() click to toggle source
# File lib/einhorn/command/interface.rb, line 14
def self.command_server
  @@command_server
end
command_server=(server) click to toggle source
# File lib/einhorn/command/interface.rb, line 9
def self.command_server=(server)
  raise "Command server already set" if @@command_server && server
  @@command_server = server
end
default_lockfile_path(cmd_name=nil) click to toggle source
# File lib/einhorn/command/interface.rb, line 124
def self.default_lockfile_path(cmd_name=nil)
  cmd_name ||= Einhorn::State.cmd_name
  if cmd_name
    filename = "einhorn-#{cmd_name}.lock"
  else
    filename = "einhorn.lock"
  end
  File.join(Dir.tmpdir, filename)
end
default_pidfile(cmd_name=nil) click to toggle source
# File lib/einhorn/command/interface.rb, line 138
def self.default_pidfile(cmd_name=nil)
  cmd_name ||= Einhorn::State.cmd_name
  if cmd_name
    filename = "einhorn-#{cmd_name}.pid"
  else
    filename = "einhorn.pid"
  end
  File.join(Dir.tmpdir, filename)
end
default_socket_path(cmd_name=nil) click to toggle source
# File lib/einhorn/command/interface.rb, line 110
def self.default_socket_path(cmd_name=nil)
  cmd_name ||= Einhorn::State.cmd_name
  if cmd_name
    filename = "einhorn-#{cmd_name}.sock"
  else
    filename = "einhorn.sock"
  end
  File.join(Dir.tmpdir, filename)
end
destroy_old_command_socket(path) click to toggle source
# File lib/einhorn/command/interface.rb, line 69
def self.destroy_old_command_socket(path)
  # Socket isn't actually owned by anyone
  begin
    sock = UNIXSocket.new(path)
  rescue Errno::ECONNREFUSED
    # This happens with non-socket files and when the listening
    # end of a socket has exited.
  rescue Errno::ENOENT
    # Socket doesn't exist
    return
  else
    # Rats, it's still active
    sock.close
    raise Errno::EADDRINUSE.new("Another process (probably another Einhorn) is listening on the Einhorn command socket at #{path}. If you'd like to run this Einhorn as well, pass a `-d PATH_TO_SOCKET` to change the command socket location.")
  end

  # Socket should still exist, so don't need to handle error.
  stat = File.stat(path)
  unless stat.socket?
    raise Errno::EADDRINUSE.new("Non-socket file present at Einhorn command socket path #{path}. Either remove that file and restart Einhorn, or pass a `-d PATH_TO_SOCKET` to change the command socket location.")
  end

  Einhorn.log_info("Blowing away old Einhorn command socket at #{path}. This likely indicates a previous Einhorn master which exited uncleanly.")
  # Whee, blow it away.
  File.unlink(path)
end
generate_message(conn, request) click to toggle source
# File lib/einhorn/command/interface.rb, line 242
def self.generate_message(conn, request)
  unless command_name = request['command']
    return 'No "command" parameter provided; not sure what you want me to do.'
  end

  if command_spec = @@commands[command_name]
    conn.log_debug("Received command: #{request.inspect}")
    begin
      return command_spec[:code].call(conn, request)
    rescue StandardError => e
      msg = "Error while processing command #{command_name.inspect}: #{e} (#{e.class})\n  #{e.backtrace.join("\n  ")}"
      conn.log_error(msg)
      return msg
    end
  else
    conn.log_debug("Received unrecognized command: #{request.inspect}")
    return unrecognized_command(conn, request)
  end
end
init() click to toggle source
# File lib/einhorn/command/interface.rb, line 18
def self.init
  install_handlers
  at_exit do
    if Einhorn::TransientState.whatami == :master
      to_remove = [pidfile]
      # Don't nuke socket_path if we never successfully acquired it
      to_remove << socket_path if @@command_server
      to_remove.each do |file|
        begin
          File.unlink(file)
        rescue Errno::ENOENT
        end
      end
    end
  end
end
install_handlers() click to toggle source

Signals

# File lib/einhorn/command/interface.rb, line 149
def self.install_handlers
  trap_async("INT") do
    Einhorn::Command.signal_all("USR2")
    Einhorn::Command.stop_respawning
  end
  trap_async("TERM") do
    Einhorn::Command.signal_all("TERM")
    Einhorn::Command.stop_respawning
  end
  # Note that quit is a bit different, in that it will actually
  # make Einhorn quit without waiting for children to exit.
  trap_async("QUIT") do
    Einhorn::Command.signal_all("QUIT")
    Einhorn::Command.stop_respawning
    exit(1)
  end
  trap_async("HUP") {Einhorn::Command.full_upgrade_smooth}
  trap_async("ALRM") do
    Einhorn.log_error("Upgrading using SIGALRM is deprecated. Please switch to SIGHUP")
    Einhorn::Command.full_upgrade_smooth
  end
  trap_async("CHLD") {}
  trap_async("USR2") do
    Einhorn::Command.signal_all("USR2")
    Einhorn::Command.stop_respawning
  end
  at_exit do
    if Einhorn::State.kill_children_on_exit && Einhorn::TransientState.whatami == :master
      Einhorn::Command.signal_all("USR2")
      Einhorn::Command.stop_respawning
    end
  end
end
lockfile() click to toggle source
# File lib/einhorn/command/interface.rb, line 120
def self.lockfile
  Einhorn::State.lockfile || default_lockfile_path
end
normalize_signals(args) click to toggle source
# File lib/einhorn/command/interface.rb, line 474
def self.normalize_signals(args)
  args.map do |signal|
    signal = signal.upcase
    signal = $1 if signal =~ /\ASIG(.*)\Z/
    signal
  end
end
open_command_socket() click to toggle source
# File lib/einhorn/command/interface.rb, line 45
def self.open_command_socket
  path = socket_path

  with_file_lock do
    # Need to avoid time-of-check to time-of-use bugs in blowing
    # away and recreating the old socketfile.
    destroy_old_command_socket(path)
    Einhorn::Compat.unixserver_new(path)
  end
end
persistent_init() click to toggle source
# File lib/einhorn/command/interface.rb, line 35
def self.persistent_init
  socket = open_command_socket
  Einhorn::Event::CommandServer.open(socket)

  # Could also rewrite this on reload. Might be useful in case
  # someone goes and accidentally clobbers/deletes. Should make
  # sure that the clobber is atomic if we we were do do that.
  write_pidfile
end
pidfile() click to toggle source
# File lib/einhorn/command/interface.rb, line 134
def self.pidfile
  Einhorn::State.pidfile || default_pidfile
end
process_command(conn, command) click to toggle source
# File lib/einhorn/command/interface.rb, line 204
def self.process_command(conn, command)
  begin
    request = Einhorn::Client::Transport.deserialize_message(command)
  rescue Einhorn::Client::Transport::ParseError
  end
  unless request.kind_of?(Hash)
    send_message(conn, "Could not parse command")
    return
  end

  message = generate_message(conn, request)
  if !message.nil?
    send_message(conn, message, request['id'], true)
  else
    conn.log_debug("Got back nil response, so not responding to command.")
  end
end
remove_handlers() click to toggle source
# File lib/einhorn/command/interface.rb, line 193
def self.remove_handlers
  %w{INT TERM QUIT HUP ALRM CHLD USR2}.each do |signal|
    Signal.trap(signal, "DEFAULT")
  end
end
send_message(conn, message, request_id=nil, last=false) click to toggle source
# File lib/einhorn/command/interface.rb, line 231
def self.send_message(conn, message, request_id=nil, last=false)
  if request_id
    response = {'message' => message, 'request_id' => request_id }
    response['wait'] = true unless last
  else
    # support old-style protocol
    response = {'message' => message}
  end
  Einhorn::Client::Transport.send_message(conn, response)
end
send_tagged_message(tag, message, last=false) click to toggle source
# File lib/einhorn/command/interface.rb, line 222
def self.send_tagged_message(tag, message, last=false)
  Einhorn::Event.connections.each do |conn|
    if id = conn.subscription(tag)
      self.send_message(conn, message, id, last)
      conn.unsubscribe(tag) if last
    end
  end
end
socket_path() click to toggle source
# File lib/einhorn/command/interface.rb, line 106
def self.socket_path
  Einhorn::State.socket_path || default_socket_path
end
trap_async(signal, &blk) click to toggle source
# File lib/einhorn/command/interface.rb, line 183
def self.trap_async(signal, &blk)
  Signal.trap(signal) do
    # We try to do as little work in the signal handler as
    # possible. This avoids potential races between e.g. iteration
    # and mutation.
    Einhorn::Event.break_loop
    Einhorn::Event.register_signal_action(&blk)
  end
end
uninit() click to toggle source
# File lib/einhorn/command/interface.rb, line 102
def self.uninit
  remove_handlers
end
unrecognized_command(conn, request) click to toggle source
# File lib/einhorn/command/interface.rb, line 272
    def self.unrecognized_command(conn, request)
      <<EOF
Unrecognized command: #{request['command'].inspect}

#{command_descriptions}
EOF
    end
validate_args(args) click to toggle source
# File lib/einhorn/command/interface.rb, line 453
def self.validate_args(args)
  return 'No args provided' unless args
  return 'Args must be an array' unless args.kind_of?(Array)

  args.each do |arg|
    return "Argument is a #{arg.class}, not a string: #{arg.inspect}" unless arg.kind_of?(String)
  end

  nil
end
validate_signals(args) click to toggle source
# File lib/einhorn/command/interface.rb, line 464
def self.validate_signals(args)
  args.each do |signal|
    unless Signal.list.include?(signal)
      return "Invalid signal: #{signal.inspect}"
    end
  end

  nil
end
with_file_lock(&blk) click to toggle source

Lock against other Einhorn workers. Unfortunately, have to leave this lockfile lying around forever.

# File lib/einhorn/command/interface.rb, line 58
def self.with_file_lock(&blk)
  path = lockfile
  File.open(path, 'w', 0600) do |f|
    unless f.flock(File::LOCK_EX|File::LOCK_NB)
      raise "File lock already acquired by another Einhorn process. This likely indicates you tried to run Einhorn masters with the same cmd_name at the same time. This is a pretty rare race condition."
    end

    blk.call
  end
end
write_pidfile() click to toggle source
# File lib/einhorn/command/interface.rb, line 96
def self.write_pidfile
  file = pidfile
  Einhorn.log_info("Writing PID to #{file}")
  File.open(file, 'w') {|f| f.write($$)}
end