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 < PhusionPassenger::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
- fileno_of
- finalize_server
- initialize_server
- new
- quit_main
- server
- server_pid
- start
- start_synchronously
- started?
- stop
Class PhusionPassenger::AbstractServer::ServerError
Class PhusionPassenger::AbstractServer::ServerNotStarted
Class PhusionPassenger::AbstractServer::UnknownMessage
SERVER_TERMINATION_SIGNAL | = | "SIGTERM" |
[RW] | last_activity_time | The last time when this AbstractServer had processed a message. |
[RW] | max_idle_time | The maximum time that this AbstractServer may be idle. Used by AbstractServerCollection to determine when this object should be cleaned up. nil or 0 indicate that this object should never be idle cleaned. |
[RW] | next_cleaning_time | Used by AbstractServerCollection to remember when this AbstractServer should be idle cleaned. |
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 108 108: def initialize 109: @done = false 110: @message_handlers = {} 111: @signal_handlers = {} 112: @orig_signal_handlers = {} 113: @last_activity_time = Time.now 114: end
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 243 243: def server_pid 244: return @pid 245: 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/phusion_passenger/abstract_server.rb, line 123 123: def start 124: if started? 125: raise ServerAlreadyStarted, "Server is already started" 126: end 127: 128: @parent_socket, @child_socket = UNIXSocket.pair 129: before_fork 130: @pid = fork 131: if @pid.nil? 132: begin 133: STDOUT.sync = true 134: STDERR.sync = true 135: @parent_socket.close 136: 137: # During Passenger's early days, we used to close file descriptors based 138: # on a white list of file descriptors. That proved to be way too fragile: 139: # too many file descriptors are being left open even though they shouldn't 140: # be. So now we close file descriptors based on a black list. 141: # 142: # Note that STDIN, STDOUT and STDERR may be temporarily set to 143: # different file descriptors than 0, 1 and 2, e.g. in unit tests. 144: # We don't want to close these either. 145: file_descriptors_to_leave_open = [0, 1, 2, @child_socket.fileno, 146: fileno_of(STDIN), fileno_of(STDOUT), fileno_of(STDERR)].compact.uniq 147: NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) 148: # In addition to closing the file descriptors, one must also close 149: # the associated IO objects. This is to prevent IO.close from 150: # double-closing already closed file descriptors. 151: close_all_io_objects_for_fds(file_descriptors_to_leave_open) 152: 153: # At this point, RubyGems might have open file handles for which 154: # the associated file descriptors have just been closed. This can 155: # result in mysterious 'EBADFD' errors. So we force RubyGems to 156: # clear all open file handles. 157: Gem.clear_paths 158: 159: # Reseed pseudo-random number generator for security reasons. 160: srand 161: 162: start_synchronously(@child_socket) 163: rescue Interrupt 164: # Do nothing. 165: rescue SignalException => signal 166: if signal.message == SERVER_TERMINATION_SIGNAL 167: # Do nothing. 168: else 169: print_exception(self.class.to_s, signal) 170: end 171: rescue Exception => e 172: print_exception(self.class.to_s, e) 173: ensure 174: exit! 175: end 176: end 177: @child_socket.close 178: @parent_channel = MessageChannel.new(@parent_socket) 179: 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/phusion_passenger/abstract_server.rb, line 188 188: def start_synchronously(socket) 189: @child_socket = socket 190: @child_channel = MessageChannel.new(socket) 191: begin 192: reset_signal_handlers 193: initialize_server 194: begin 195: main_loop 196: ensure 197: finalize_server 198: end 199: ensure 200: revert_signal_handlers 201: end 202: end
Return whether the server has been started.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 238 238: def started? 239: return !@parent_channel.nil? 240: 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/phusion_passenger/abstract_server.rb, line 209 209: def stop 210: if !started? 211: raise ServerNotStarted, "Server is not started" 212: end 213: 214: @parent_socket.close 215: @parent_channel = nil 216: 217: # Wait at most 3 seconds for server to exit. If it doesn't do that, 218: # we kill it. If that doesn't work either, we kill it forcefully with 219: # SIGKILL. 220: begin 221: Timeout::timeout(3) do 222: Process.waitpid(@pid) rescue nil 223: end 224: rescue Timeout::Error 225: Process.kill(SERVER_TERMINATION_SIGNAL, @pid) rescue nil 226: begin 227: Timeout::timeout(3) do 228: Process.waitpid(@pid) rescue nil 229: end 230: rescue Timeout::Error 231: Process.kill('SIGKILL', @pid) rescue nil 232: Process.waitpid(@pid, Process::WNOHANG) rescue nil 233: end 234: end 235: 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/phusion_passenger/abstract_server.rb, line 250 250: def before_fork 251: 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/phusion_passenger/abstract_server.rb, line 295 295: def client 296: return @child_channel 297: 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/phusion_passenger/abstract_server.rb, line 270 270: def define_message_handler(message_name, handler) 271: @message_handlers[message_name.to_s] = handler 272: end
Define a handler for a signal.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 275 275: def define_signal_handler(signal, handler) 276: @signal_handlers[signal.to_s] = handler 277: end
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 304 304: def fileno_of(io) 305: return io.fileno 306: rescue 307: return nil 308: 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/phusion_passenger/abstract_server.rb, line 262 262: def finalize_server 263: 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/phusion_passenger/abstract_server.rb, line 256 256: def initialize_server 257: end
Tell the main loop to stop as soon as possible.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 300 300: def quit_main 301: @done = true 302: 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/phusion_passenger/abstract_server.rb, line 286 286: def server 287: if !started? 288: raise ServerNotStarted, "Server hasn't been started yet. Please call start() first." 289: end 290: return @parent_channel 291: end