Module: Einhorn::Command::Interface
- Defined in:
- lib/einhorn/command/interface.rb
Constant Summary collapse
- @@commands =
{}
- @@command_server =
nil
Class Method Summary collapse
-
.command(name, description = nil, &code) ⇒ Object
Commands.
- .command_descriptions ⇒ Object
- .command_server ⇒ Object
- .command_server=(server) ⇒ Object
- .default_lockfile_path(cmd_name = nil) ⇒ Object
- .default_pidfile(cmd_name = nil) ⇒ Object
- .default_socket_path(cmd_name = nil) ⇒ Object
- .destroy_old_command_socket(path) ⇒ Object
- .generate_message(conn, request) ⇒ Object
- .init ⇒ Object
-
.install_handlers ⇒ Object
Signals.
- .lockfile ⇒ Object
- .normalize_signals(args) ⇒ Object
- .open_command_socket ⇒ Object
- .persistent_init ⇒ Object
- .pidfile ⇒ Object
- .process_command(conn, command) ⇒ Object
- .remove_handlers ⇒ Object
- .send_message(conn, message, request_id = nil, last = false) ⇒ Object
- .send_tagged_message(tag, message, last = false) ⇒ Object
- .socket_path ⇒ Object
- .trap_async(signal, &blk) ⇒ Object
- .uninit ⇒ Object
- .unrecognized_command(conn, request) ⇒ Object
- .validate_args(args) ⇒ Object
- .validate_signals(args) ⇒ Object
-
.with_file_lock(&blk) ⇒ Object
Lock against other Einhorn workers.
- .write_pidfile ⇒ Object
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_descriptions ⇒ Object
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_server ⇒ Object
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.(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 |
.init ⇒ Object
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_handlers ⇒ Object
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 |
.lockfile ⇒ Object
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_socket ⇒ Object
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_init ⇒ Object
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 |
.pidfile ⇒ Object
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.(command) rescue Einhorn::Client::Transport::ParseError end unless request.is_a?(Hash) (conn, "Could not parse command") return end = (conn, request) if !.nil? (conn, , request["id"], true) else conn.log_debug("Got back nil response, so not responding to command.") end end |
.remove_handlers ⇒ Object
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.(conn, , request_id = nil, last = false) if request_id response = {"message" => , "request_id" => request_id} response["wait"] = true unless last else # support old-style protocol response = {"message" => } end Einhorn::Client::Transport.(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.(tag, , last = false) Einhorn::Event.connections.each do |conn| if (id = conn.subscription(tag)) (conn, , id, last) conn.unsubscribe(tag) if last end end end |
.socket_path ⇒ Object
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 |
.uninit ⇒ Object
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 |