class WorkerProcess(object): """ Worker process encapsulation. These work a lot like server processes, except that they are managed by daemon process as child processes. The implementation uses two contexts: - the server daemon context in which .start_server() and .stop_server() are called - the worker process contect in which .main() is run The .main() method has to be overridden to implement the worker process logic. """ # Note: This code is similar to ServerDaemon, but for worker # processes we don't fork twice since we want the workers to be # child processes of the server process. # Name of the worker name = 'Worker Process' # PID of the worker process; set in both the server and the worker # process context pid = 0 # Started flag. Set by .start_worker()/.stop_worker() in the # server context. started = False # Exit status code. Set by .worker_exited() in the server context. exit_status = 0 # mxLog object to use. Inherited from the ServerDaemon if None log = None # Log id to use in the worker process. Inherited from the # ServerDaemon if None log_id = None # Process name to use for the worker process. Note: this is not # guaranteed to work. Inherited from the ServerDaemon if None process_name = None # Startup time of the worker processes in seconds. The # .start_worker() method will wait this number of second for the # worker process to start up. worker_startup_time = 2 # Shutdown time of the worker processes in seconds. The # .stop_worker() method will wait this number of second for the # worker processes to terminate. worker_shutdown_time = 2 # Kill time of the worker processes in seconds. The .stop_worker() # method will wait this number of second for the worker processes # to terminate after having received the KILL signal. worker_kill_time = 1 # Range of file descriptors to close after the fork; all open fds # except of stdin, stdout, stderr close_file_descriptors = tuple(range(3, 99)) def __init__(self, server_daemon): # Inherit settings from the server if self.log is None: self.log = server_daemon.log if self.log is None: self.log = Log.LogNothing if self.log_id is None: self.log_id = server_daemon.log_id if self.process_name is None: self.process_name = server_daemon.process_name def __repr__(self): return '%s(%s with PID %s)' % (self.__class__.__name__, self.name, self.pid) def setup_worker(self, **parameters): """ Prepare the worker startup and adjust the parameters to be passed on to the worker's .main() method. This method is called by .start_worker() before forking off a child process in order to give the WorkerProcess implementation a chance to adjust itself to the parameters. It has to return a copy of the parameters keyword argument dictionary. This method is called in the context of the server. """ return parameters.copy() def start_worker(self, **parameters): """ Start the worker process and pass the given keyword parameters to the .main() method. """ # Prepare startup parameters = self.setup_worker(**parameters) assert parameters is not None, \ '.setup_worker() did not return a parameters dictionary' # Flush file descriptors sys.stderr.flush() sys.stdout.flush() # Create a socket pair server_socket, worker_socket = socket.socketpair( socket.AF_UNIX, socket.SOCK_STREAM) # Fork a child process, errors will be reported to the caller pid = os.fork() if pid != 0: ### Server process context ... # Close our end of the socket pair server_socket.close() # Wait for the child to start up worker_socket.settimeout(self.worker_startup_time) try: ok = worker_socket.recv(1) except socket.timeout: ok = None worker_socket.close() if not ok: # Terminate the child, if it didn't startup in time self.log( self.log.ERROR, '%s: ' 'Collecting worker process PID %s due to startup failure', self.name, pid) try: self._kill_worker(pid) except WorkerNotStoppedError: pass # Report the failure raise WorkerNotStartedError( '%s: Worker process with PID %s did not start up' % (self.name, pid)) # Remember the worker process pid and return it self.pid = pid self.started = True self.exit_status = 0 return pid ### Worker process context ... # Close our end of the socket pair worker_socket.close() # Close all open fds except of stdin, stdout, stderr self.log.close() server_socket_fd = server_socket.fileno() for i in self.close_file_descriptors: if i == server_socket_fd: # We'll close that manually later on continue try: os.close(i) except (IOError, OSError), reason: pass # Reopen the log file self.log.open() if self.log_id: self.log.setup(log_id=self.log_id) # Redirect stdout and stderr to the log file self.log.redirect_stdout() self.log.redirect_stderr() # Try to rename the process if self.process_name: try: Tools.setproctitle(self.process_name) except AttributeError: pass # Set the PID of the worker process self.pid = os.getpid() # Let the server process know that we've started up server_socket.send('1') server_socket.close() # Run the .main() method rc = 0 try: try: self.log(self.log.INFO, '%s: Worker process PID %s %s', self.name, self.pid, '-' * 40) if _debug > 1: self.log.object( self.log.DEBUG, '%s: Using the following startup parameters:' % self.name, parameters) # Run the worker's .main() method main_rc = self.main(**parameters) # Return the exit code, if it's an integer if main_rc is not None and isinstance(main_rc, int): rc = main_rc except Exception: # Something unexpected happened... log the problem and exit self.log.traceback(self.log.ERROR, '%s: ' 'Unexpected worker process error:', self.name) rc = 1 finally: self.cleanup_worker() # Exit process os._exit(rc)
class ServerDaemon(object): """ Server daemon encapsulation. This class provides an easy way to setup a Unix server daemon that uses a single process. It may still spawn off additional processes, but this encapsulation only manages the main process. The implementation runs two contexts: - the control context in which .start_server() and .stop_server() are called - the server process contect in which .main() is run """ # Name of the server name = 'Server Daemon' # PID of the process pid = 0 # Location of the PID file of the parent process pid_file = 'server.pid' # umask to set for the forked server process umask = 022 # Root dir to change to for the forked server process root_dir = '' # Range of file descriptors to close after the fork; all open fds # except of stdin, stdout, stderr close_file_descriptors = tuple(range(3, 99)) # mxLog object to use log = Log.log # Log id to use in the forked server process log_id = '' # Process name to use for the forked server process. Note: this is # not guaranteed to work process_name = '' # Server startup time in seconds. The .start_server() # method will wait at most this number of seconds for the main # server process to initialize and enter the .main() method. This # includes forking overhead, module import times, etc. It does not # cover the startup time that the server may need to become usable # for external applications. The startup time can be configured # with .server_startup_time server_startup_time = 2 # Startup initialization time of the server in seconds. The # .start_server() method will unconditionally wait this number of # seconds after having initialized the server in order to give the # .main() method a chance to setup any resources it may need to # initialize. server_startup_init_time = 0 # Server shutdown time in seconds. The .stop_server() # method will wait at most this number of seconds for the main # server process to terminate after sending it a TERM signal. server_shutdown_time = 2 # Kill time of the server processes in seconds. The .stop_server() # method will wait this number of second for the worker processes # to terminate after having received the KILL signal. server_kill_time = 1 # Shutdown cleanup time of the server in seconds. The # .stop_server() method will unconditionally wait this number of # seconds after having terminated the main server process in order # to give possibly additionally spawned processes a chance to # terminate cleanly as well. server_shutdown_cleanup_time = 0 ### def setup_server(self, **parameters): """ Prepare the server startup and adjust the parameters to be passed on to the server's .main() method. This method is called by .start_server() before forking off a child process in order to give the WorkerProcess implementation a chance to adjust itself to the parameters. It has to return a copy of the parameters keyword argument dictionary. This method is called in the context of the server. """ return parameters.copy() def _kill_server(self, pid): """ Kill a server process pid and collect it. Returns the process exit status or -1 in case this cannot be determined. Raises a ServerNotStoppedError in case the process cannot be stopped. """ try: return kill_process(pid, shutdown_time=self.server_shutdown_time, kill_time=self.server_kill_time, log=self.log, log_prefix='%s: ' % self.name) except ProcessNotStoppedError: # Did not work out... raise ServerNotStoppedError( '%s: Server process with PID %s did not stop' % (self.name, pid)) def start_server(self, **parameters): """ Starts the server. Keyword parameters are passed on to the forked process' .main() method. Returns the PID of the started server daemon. Raises a ServerAlreadyRunningError if the server is already running. Raises a ServerNotStartedError in case the daemon could not be started. """ # Verify if we have a running server process pid = self.server_status() if pid is not None: raise ServerAlreadyRunningError( 'Server is already running (PID %s)' % pid) # Prepare startup parameters = self.setup_server(**parameters) assert parameters is not None, \ '.setup_server() did not return a parameters dictionary' # Flush the standard file descriptors sys.stderr.flush() sys.stdout.flush() # Fork a child process, errors will be reported to the caller pid = os.fork() if pid != 0: ### Parent process # Collect the first child if _debug: self.log( self.log.DEBUG, '%s: ' 'Waiting for the first child with PID %s to terminate', self.name, pid) os.waitpid(pid, 0) # Wait a few seconds until the server has started if _debug: self.log(self.log.DEBUG, '%s: ' 'Waiting for the server process to startup', self.name) for i in xrange(int(self.server_startup_time * 100) + 1): spid = self.server_status() if spid is not None: break time.sleep(0.01) else: # Server did not startup in time: terminate the first # child self.log(self.log.ERROR, '%s: Server process failed to startup', self.name) try: self._kill_server(pid) except ServerNotStoppedError: pass # Report the problem; XXX Note that the second child # may still startup after this first has already # terminated. raise ServerNotStartedError('%s did not start up' % self.name) if self.server_startup_init_time: time.sleep(self.server_startup_init_time) return spid ### This is the first child process # Daemonize process os.setpgrp() if self.root_dir: os.chdir(self.root_dir) if self.umask: os.umask(self.umask) try: # Try to become a session leader os.setsid() except OSError: # We are already the process session leader pass # Close all open fds except of stdin, stdout, stderr self.log.close() for i in self.close_file_descriptors: try: os.close(i) except (IOError, OSError), reason: pass # Fork again to become a separate daemon process pid = os.fork() if pid != 0: # We need to terminate the "middle" process at this point, since we # don't want to continue with two instances of the original caller. # We must not call any cleanup handlers here. os._exit(0) ### This is the second child process: the server daemon # Turn the daemon into a process group leader os.setpgrp() # Reopen the log file self.log.open() if self.log_id: self.log.setup(log_id=self.log_id) # Redirect stdout and stderr to the log file self.log.redirect_stdout() self.log.redirect_stderr() # Try to rename the process if self.process_name: try: Tools.setproctitle(self.process_name) except AttributeError: pass # Save the PID of the server daemon process self.pid = os.getpid() self.save_server_pid(self.pid) # We need to remove the PID file on exit rc = 0 try: try: self.log(self.log.INFO, '%s: Server process PID %s %s', self.name, self.pid, '*' * 60) # Run the server's .main() method main_rc = self.main(**parameters) # Return the exit code, if it's an integer if main_rc is not None and isinstance(main_rc, int): rc = main_rc except SystemExit, exc: # Normal shutdown rc = exc.code self.log(self.log.INFO, '%s: Shutting down with status: %s', self.name, rc) except Exception: # Something unexpected happened... log the problem and exit self.log.traceback(self.log.ERROR, '%s: Unexpected server error:', self.name) rc = 1