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
- 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 235 235: def server_pid 236: return @pid 237: 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: file_descriptors_to_leave_open = [0, 1, 2, @child_socket.fileno] 142: NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) 143: # In addition to closing the file descriptors, one must also close 144: # the associated IO objects. This is to prevent IO.close from 145: # double-closing already closed file descriptors. 146: close_all_io_objects_for_fds(file_descriptors_to_leave_open) 147: 148: # At this point, RubyGems might have open file handles for which 149: # the associated file descriptors have just been closed. This can 150: # result in mysterious 'EBADFD' errors. So we force RubyGems to 151: # clear all open file handles. 152: Gem.clear_paths 153: 154: start_synchronously(@child_socket) 155: rescue Interrupt 156: # Do nothing. 157: rescue SignalException => signal 158: if signal.message == SERVER_TERMINATION_SIGNAL 159: # Do nothing. 160: else 161: print_exception(self.class.to_s, signal) 162: end 163: rescue Exception => e 164: print_exception(self.class.to_s, e) 165: ensure 166: exit! 167: end 168: end 169: @child_socket.close 170: @parent_channel = MessageChannel.new(@parent_socket) 171: 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 180 180: def start_synchronously(socket) 181: @child_socket = socket 182: @child_channel = MessageChannel.new(socket) 183: begin 184: reset_signal_handlers 185: initialize_server 186: begin 187: main_loop 188: ensure 189: finalize_server 190: end 191: ensure 192: revert_signal_handlers 193: end 194: end
Return whether the server has been started.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 230 230: def started? 231: return !@parent_channel.nil? 232: 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 201 201: def stop 202: if !started? 203: raise ServerNotStarted, "Server is not started" 204: end 205: 206: @parent_socket.close 207: @parent_channel = nil 208: 209: # Wait at most 3 seconds for server to exit. If it doesn't do that, 210: # we kill it. If that doesn't work either, we kill it forcefully with 211: # SIGKILL. 212: begin 213: Timeout::timeout(3) do 214: Process.waitpid(@pid) rescue nil 215: end 216: rescue Timeout::Error 217: Process.kill(SERVER_TERMINATION_SIGNAL, @pid) rescue nil 218: begin 219: Timeout::timeout(3) do 220: Process.waitpid(@pid) rescue nil 221: end 222: rescue Timeout::Error 223: Process.kill('SIGKILL', @pid) rescue nil 224: Process.waitpid(@pid, Process::WNOHANG) rescue nil 225: end 226: end 227: 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 242 242: def before_fork 243: 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 287 287: def client 288: return @child_channel 289: 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 262 262: def define_message_handler(message_name, handler) 263: @message_handlers[message_name.to_s] = handler 264: end
Define a handler for a signal.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 267 267: def define_signal_handler(signal, handler) 268: @signal_handlers[signal.to_s] = handler 269: 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 254 254: def finalize_server 255: 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 248 248: def initialize_server 249: end
Tell the main loop to stop as soon as possible.
[ show source ]
# File lib/phusion_passenger/abstract_server.rb, line 292 292: def quit_main 293: @done = true 294: 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 278 278: def server 279: if !started? 280: raise ServerNotStarted, "Server hasn't been started yet. Please call start() first." 281: end 282: return @parent_channel 283: end