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