An abstract base class for a server, with the following properties:
- The server has exactly one client, and is connected to that client at all times. The server will quit when the connection closes.
- The server‘s main loop may be run in a child process (and so is asynchronous from the main process).
- One can communicate with the server through discrete messages (as opposed to byte streams).
- The server can pass file descriptors (IO objects) back to the client.
A message is just an ordered list of strings. The first element in the message is the _message name_.
The server will also reset all signal handlers (in the child process). That is, it will respond to all signals in the default manner. The only exception is SIGHUP, which is ignored. One may define additional signal handlers using define_signal_handler().
Before an AbstractServer can be used, it must first be started by calling start(). When it is no longer needed, stop() should be called.
Here‘s an example on using AbstractServer:
class MyServer < Passenger::AbstractServer def initialize super() define_message_handler(:hello, :handle_hello) end def hello(first_name, last_name) send_to_server('hello', first_name, last_name) reply, pointless_number = recv_from_server puts "The server said: #{reply}" puts "In addition, it sent this pointless number: #{pointless_number}" end private def handle_hello(first_name, last_name) send_to_client("Hello #{first_name} #{last_name}, how are you?", 1234) end end server = MyServer.new server.start server.hello("Joe", "Dalton") server.stop
- before_fork
- client
- define_message_handler
- define_signal_handler
- finalize_server
- initialize_server
- new
- quit_main
- server
- server_pid
- start
- start_synchronously
- started?
- stop
Class Passenger::AbstractServer::ServerError
Class Passenger::AbstractServer::ServerNotStarted
Class Passenger::AbstractServer::UnknownMessage
SERVER_TERMINATION_SIGNAL | = | "SIGTERM" |
[ show source ]
# File lib/passenger/abstract_server.rb, line 91 91: def initialize 92: @done = false 93: @message_handlers = {} 94: @signal_handlers = {} 95: @orig_signal_handlers = {} 96: end
[ show source ]
# File lib/passenger/abstract_server.rb, line 216 216: def server_pid 217: return @pid 218: end
Start the server. This method does not block since the server runs asynchronously from the current process.
You may only call this method if the server is not already started. Otherwise, a ServerAlreadyStarted will be raised.
Derived classes may raise additional exceptions.
[ show source ]
# File lib/passenger/abstract_server.rb, line 105 105: def start 106: if started? 107: raise ServerAlreadyStarted, "Server is already started" 108: end 109: 110: @parent_socket, @child_socket = UNIXSocket.pair 111: before_fork 112: @pid = fork do 113: begin 114: STDOUT.sync = true 115: STDERR.sync = true 116: @parent_socket.close 117: 118: # During Passenger's early days, we used to close file descriptors based 119: # on a white list of file descriptors. That proved to be way too fragile: 120: # too many file descriptors are being left open even though they shouldn't 121: # be. So now we close file descriptors based on a black list. 122: file_descriptors_to_leave_open = [0, 1, 2, @child_socket.fileno] 123: NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) 124: # In addition to closing the file descriptors, one must also close 125: # the associated IO objects. This is to prevent IO.close from 126: # double-closing already closed file descriptors. 127: close_all_io_objects_for_fds(file_descriptors_to_leave_open) 128: 129: # At this point, RubyGems might have open file handles for which 130: # the associated file descriptors have just been closed. This can 131: # result in mysterious 'EBADFD' errors. So we force RubyGems to 132: # clear all open file handles. 133: Gem.clear_paths 134: 135: start_synchronously(@child_socket) 136: rescue Interrupt 137: # Do nothing. 138: rescue SignalException => signal 139: if signal.message == SERVER_TERMINATION_SIGNAL 140: # Do nothing. 141: else 142: print_exception(self.class.to_s, signal) 143: end 144: rescue Exception => e 145: print_exception(self.class.to_s, e) 146: ensure 147: exit! 148: end 149: end 150: @child_socket.close 151: @parent_channel = MessageChannel.new(@parent_socket) 152: end
Start the server, but in the current process instead of in a child process. This method blocks until the server‘s main loop has ended.
socket is the socket that the server should listen on. The server main loop will end if the socket has been closed.
All hooks will be called, except before_fork().
[ show source ]
# File lib/passenger/abstract_server.rb, line 161 161: def start_synchronously(socket) 162: @child_socket = socket 163: @child_channel = MessageChannel.new(socket) 164: begin 165: reset_signal_handlers 166: initialize_server 167: begin 168: main_loop 169: ensure 170: finalize_server 171: end 172: ensure 173: revert_signal_handlers 174: end 175: end
Return whether the server has been started.
[ show source ]
# File lib/passenger/abstract_server.rb, line 211 211: def started? 212: return !@parent_channel.nil? 213: end
Stop the server. The server will quit as soon as possible. This method waits until the server has been stopped.
When calling this method, the server must already be started. If not, a ServerNotStarted will be raised.
[ show source ]
# File lib/passenger/abstract_server.rb, line 182 182: def stop 183: if !started? 184: raise ServerNotStarted, "Server is not started" 185: end 186: 187: @parent_socket.close 188: @parent_channel = nil 189: 190: # Wait at most 3 seconds for server to exit. If it doesn't do that, 191: # we kill it. If that doesn't work either, we kill it forcefully with 192: # SIGKILL. 193: begin 194: Timeout::timeout(3) do 195: Process.waitpid(@pid) rescue nil 196: end 197: rescue Timeout::Error 198: Process.kill(SERVER_TERMINATION_SIGNAL, @pid) rescue nil 199: begin 200: Timeout::timeout(3) do 201: Process.waitpid(@pid) rescue nil 202: end 203: rescue Timeout::Error 204: Process.kill('SIGKILL', @pid) rescue nil 205: Process.waitpid(@pid, Process::WNOHANG) rescue nil 206: end 207: end 208: end
A hook which is called when the server is being started, just before forking a new process. The default implementation does nothing, this method is supposed to be overrided by child classes.
[ show source ]
# File lib/passenger/abstract_server.rb, line 223 223: def before_fork 224: end
Return the communication channel with the client (i.e. the parent process that started the server). This is a MessageChannel object.
[ show source ]
# File lib/passenger/abstract_server.rb, line 268 268: def client 269: return @child_channel 270: end
Define a handler for a message. message_name is the name of the message to handle, and handler is the name of a method to be called (this may either be a String or a Symbol).
A message is just a list of strings, and so handler will be called with the message as its arguments, excluding the first element. See also the example in the class description.
[ show source ]
# File lib/passenger/abstract_server.rb, line 243 243: def define_message_handler(message_name, handler) 244: @message_handlers[message_name.to_s] = handler 245: end
Define a handler for a signal.
[ show source ]
# File lib/passenger/abstract_server.rb, line 248 248: def define_signal_handler(signal, handler) 249: @signal_handlers[signal.to_s] = handler 250: end
A hook which is called when the server is being stopped. This is called in the child process, after the main loop has been left. The default implementation does nothing, this method is supposed to be overrided by child classes.
[ show source ]
# File lib/passenger/abstract_server.rb, line 235 235: def finalize_server 236: end
A hook which is called when the server is being started. This is called in the child process, before the main loop is entered. The default implementation does nothing, this method is supposed to be overrided by child classes.
[ show source ]
# File lib/passenger/abstract_server.rb, line 229 229: def initialize_server 230: end
Tell the main loop to stop as soon as possible.
[ show source ]
# File lib/passenger/abstract_server.rb, line 273 273: def quit_main 274: @done = true 275: end
Return the communication channel with the server. This is a MessageChannel object.
Raises ServerNotStarted if the server hasn‘t been started yet.
This method may only be called in the parent process, and not in the started server process.
[ show source ]
# File lib/passenger/abstract_server.rb, line 259 259: def server 260: if !started? 261: raise ServerNotStarted, "Server hasn't been started yet. Please call start() first." 262: end 263: return @parent_channel 264: end