Module: Einhorn::Command::Interface

Defined in:
lib/einhorn/command/interface.rb

Constant Summary collapse

@@commands =
{}
@@command_server =
nil

Class Method Summary collapse

Class Method Details

.command(name, description = nil, &code) ⇒ Object

Commands



200
201
202
# File 'lib/einhorn/command/interface.rb', line 200

def self.command(name, description=nil, &code)
  @@commands[name] = {:description => description, :code => code}
end

.command_descriptionsObject



262
263
264
265
266
267
268
269
270
# 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_serverObject



14
15
16
# File 'lib/einhorn/command/interface.rb', line 14

def self.command_server
  @@command_server
end

.command_server=(server) ⇒ Object



9
10
11
12
# 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) ⇒ Object



124
125
126
127
128
129
130
131
132
# 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) ⇒ Object



138
139
140
141
142
143
144
145
146
# 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) ⇒ Object



110
111
112
113
114
115
116
117
118
# 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) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 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) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# 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

.initObject



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 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_handlersObject

Signals



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/einhorn/command/interface.rb', line 149

def self.install_handlers
  trap_async("INT") do
    Einhorn::Command.signal_all("USR2", Einhorn::WorkerPool.workers)
    Einhorn::Command.stop_respawning
  end
  trap_async("TERM") do
    Einhorn::Command.signal_all("TERM", Einhorn::WorkerPool.workers)
    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::WorkerPool.workers)
    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::WorkerPool.workers)
    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::WorkerPool.workers)
      Einhorn::Command.stop_respawning
    end
  end
end

.lockfileObject



120
121
122
# File 'lib/einhorn/command/interface.rb', line 120

def self.lockfile
  Einhorn::State.lockfile || default_lockfile_path
end

.normalize_signals(args) ⇒ Object



463
464
465
466
467
468
469
# File 'lib/einhorn/command/interface.rb', line 463

def self.normalize_signals(args)
  args.map do |signal|
    signal = signal.upcase
    signal = $1 if signal =~ /\ASIG(.*)\Z/
    signal
  end
end

.open_command_socketObject



45
46
47
48
49
50
51
52
53
54
# 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_initObject



35
36
37
38
39
40
41
42
43
# 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

.pidfileObject



134
135
136
# File 'lib/einhorn/command/interface.rb', line 134

def self.pidfile
  Einhorn::State.pidfile || default_pidfile
end

.process_command(conn, command) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# 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_handlersObject



193
194
195
196
197
# 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) ⇒ Object



231
232
233
234
235
236
237
238
239
240
# 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) ⇒ Object



222
223
224
225
226
227
228
229
# 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_pathObject



106
107
108
# File 'lib/einhorn/command/interface.rb', line 106

def self.socket_path
  Einhorn::State.socket_path || default_socket_path
end

.trap_async(signal, &blk) ⇒ Object



183
184
185
186
187
188
189
190
191
# 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

.uninitObject



102
103
104
# File 'lib/einhorn/command/interface.rb', line 102

def self.uninit
  remove_handlers
end

.unrecognized_command(conn, request) ⇒ Object



272
273
274
275
276
277
278
# 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) ⇒ Object



442
443
444
445
446
447
448
449
450
451
# File 'lib/einhorn/command/interface.rb', line 442

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) ⇒ Object



453
454
455
456
457
458
459
460
461
# File 'lib/einhorn/command/interface.rb', line 453

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) ⇒ Object

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



58
59
60
61
62
63
64
65
66
67
# 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_pidfileObject



96
97
98
99
100
# 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