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



194
195
196
# File 'lib/einhorn/command/interface.rb', line 194

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

.command_descriptionsObject



256
257
258
259
260
261
262
263
264
# File 'lib/einhorn/command/interface.rb', line 256

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



122
123
124
125
126
127
128
129
130
# File 'lib/einhorn/command/interface.rb', line 122

def self.default_lockfile_path(cmd_name = nil)
  cmd_name ||= Einhorn::State.cmd_name
  filename = if cmd_name
    "einhorn-#{cmd_name}.lock"
  else
    "einhorn.lock"
  end
  File.join(Dir.tmpdir, filename)
end

.default_pidfile(cmd_name = nil) ⇒ Object



136
137
138
139
140
141
142
143
144
# File 'lib/einhorn/command/interface.rb', line 136

def self.default_pidfile(cmd_name = nil)
  cmd_name ||= Einhorn::State.cmd_name
  filename = if cmd_name
    "einhorn-#{cmd_name}.pid"
  else
    "einhorn.pid"
  end
  File.join(Dir.tmpdir, filename)
end

.default_socket_path(cmd_name = nil) ⇒ Object



108
109
110
111
112
113
114
115
116
# File 'lib/einhorn/command/interface.rb', line 108

def self.default_socket_path(cmd_name = nil)
  cmd_name ||= Einhorn::State.cmd_name
  filename = if cmd_name
    "einhorn-#{cmd_name}.sock"
  else
    "einhorn.sock"
  end
  File.join(Dir.tmpdir, filename)
end

.destroy_old_command_socket(path) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/einhorn/command/interface.rb', line 67

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



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/einhorn/command/interface.rb', line 236

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
      command_spec[:code].call(conn, request)
    rescue => e
      msg = "Error while processing command #{command_name.inspect}: #{e} (#{e.class})\n  #{e.backtrace.join("\n  ")}"
      conn.log_error(msg)
      msg
    end
  else
    conn.log_debug("Received unrecognized command: #{request.inspect}")
    unrecognized_command(conn, request)
  end
end

.initObject



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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|
        File.unlink(file)
      rescue Errno::ENOENT
      end
    end
  end
end

.install_handlersObject

Signals



147
148
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
# File 'lib/einhorn/command/interface.rb', line 147

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("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

.lockfileObject



118
119
120
# File 'lib/einhorn/command/interface.rb', line 118

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

.normalize_signals(args) ⇒ Object



468
469
470
471
472
473
474
# File 'lib/einhorn/command/interface.rb', line 468

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

.open_command_socketObject



43
44
45
46
47
48
49
50
51
52
# File 'lib/einhorn/command/interface.rb', line 43

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



33
34
35
36
37
38
39
40
41
# File 'lib/einhorn/command/interface.rb', line 33

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



132
133
134
# File 'lib/einhorn/command/interface.rb', line 132

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

.process_command(conn, command) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/einhorn/command/interface.rb', line 198

def self.process_command(conn, command)
  begin
    request = Einhorn::Client::Transport.deserialize_message(command)
  rescue Einhorn::Client::Transport::ParseError
  end
  unless request.is_a?(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



187
188
189
190
191
# File 'lib/einhorn/command/interface.rb', line 187

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



225
226
227
228
229
230
231
232
233
234
# File 'lib/einhorn/command/interface.rb', line 225

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



216
217
218
219
220
221
222
223
# File 'lib/einhorn/command/interface.rb', line 216

def self.send_tagged_message(tag, message, last = false)
  Einhorn::Event.connections.each do |conn|
    if (id = conn.subscription(tag))
      send_message(conn, message, id, last)
      conn.unsubscribe(tag) if last
    end
  end
end

.socket_pathObject



104
105
106
# File 'lib/einhorn/command/interface.rb', line 104

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

.trap_async(signal, &blk) ⇒ Object



177
178
179
180
181
182
183
184
185
# File 'lib/einhorn/command/interface.rb', line 177

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



100
101
102
# File 'lib/einhorn/command/interface.rb', line 100

def self.uninit
  remove_handlers
end

.unrecognized_command(conn, request) ⇒ Object



266
267
268
269
270
271
272
# File 'lib/einhorn/command/interface.rb', line 266

def self.unrecognized_command(conn, request)
  <<~EOF
    Unrecognized command: #{request["command"].inspect}
    
    #{command_descriptions}
  EOF
end

.validate_args(args) ⇒ Object



447
448
449
450
451
452
453
454
455
456
# File 'lib/einhorn/command/interface.rb', line 447

def self.validate_args(args)
  return "No args provided" unless args
  return "Args must be an array" unless args.is_a?(Array)

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

  nil
end

.validate_signals(args) ⇒ Object



458
459
460
461
462
463
464
465
466
# File 'lib/einhorn/command/interface.rb', line 458

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.



56
57
58
59
60
61
62
63
64
65
# File 'lib/einhorn/command/interface.rb', line 56

def self.with_file_lock(&blk)
  path = lockfile
  File.open(path, "w", 0o600) 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



94
95
96
97
98
# File 'lib/einhorn/command/interface.rb', line 94

def self.write_pidfile
  file = pidfile
  Einhorn.log_info("Writing PID to #{file}")
  File.write(file, $$)
end