#!/usr/bin/env python # -*- coding: utf-8 -*- # # Daemon: superclass used to implement a daemon easily # # Copyright (C)2009-2012, UPMC Paris Universitas # Authors: # Marc-Olivier Buob # see also: http://www.jejik.com/files/examples/daemon3x.py # This is used to import the daemon package instead of the local module which is # named identically... from __future__ import absolute_import from manifold.util.singleton import Singleton from manifold.util.log import Log from manifold.util.options import Options import atexit, os, signal, lockfile, logging, sys class Daemon(object): __metaclass__ = Singleton DEFAULTS = { # Running "uid" : os.getuid(), "gid" : os.getgid(), "working_directory" : "/", "debugmode" : False, "no_daemon" : False, "pid_filename" : "/var/run/%s.pid" % Options().get_name() } #------------------------------------------------------------------------- # Checks #------------------------------------------------------------------------- def check_python_daemon(self): """ \brief Check whether python-daemon is properly installed \return True if everything is file, False otherwise """ # http://www.python.org/dev/peps/pep-3143/ ret = False try: import daemon getattr(daemon, "DaemonContext") ret = True except AttributeError, e: print e # daemon and python-daemon conflict with each other Log.critical("Please install python-daemon instead of daemon. Remove daemon first.") except ImportError: Log.critical("Please install python-daemon - easy_install python-daemon.") return ret #------------------------------------------------------------------------ # Initialization #------------------------------------------------------------------------ def make_handler_rsyslog(self, rsyslog_host, rsyslog_port, log_level): """ \brief (Internal usage) Prepare logging via rsyslog \param rsyslog_host The hostname of the rsyslog server \param rsyslog_port The port of the rsyslog server \param log_level Log level """ # Prepare the handler shandler = handlers.SysLogHandler( (rsyslog_host, rsyslog_port), facility = handlers.SysLogHandler.LOG_DAEMON ) # The log file must remain open while daemonizing self.files_to_keep.append(shandler.socket) self.prepare_handler(shandler, log_level) return shandler def make_handler_locallog(self, log_filename, log_level): """ \brief (Internal usage) Prepare local logging \param log_filename The file in which we write the logs \param log_level Log level """ # Create directory in which we store the log file log_dir = os.path.dirname(log_filename) if not os.path.exists(log_dir): try: os.makedirs(log_dir) except OSError, why: log_error("OS error: %s" % why) # Prepare the handler shandler = logging.handlers.RotatingFileHandler( log_filename, backupCount = 0 ) # The log file must remain open while daemonizing self.files_to_keep.append(shandler.stream) self.prepare_handler(shandler, log_level) return shandler def prepare_handler(self, shandler, log_level): """ \brief (Internal usage) \param shandler Handler used to log information \param log_level Log level """ shandler.setLevel(log_level) formatter = logging.Formatter("%(asctime)s: %(name)s: %(levelname)s %(message)s") shandler.setFormatter(formatter) self.log.addHandler(shandler) self.log.setLevel(getattr(logging, log_level, logging.INFO)) def __init__( self, #daemon_name, terminate_callback = None #uid = os.getuid(), #gid = os.getgid(), #working_directory = "/", #pid_filename = None, #no_daemon = False, #debug = False, #log = None, # logging.getLogger("plop") #rsyslog_host = "localhost", # Pass None if no rsyslog server #rsyslog_port = 514, #log_file = None, #log_level = logging.INFO ): """ \brief Constructor \param daemon_name The name of the daemon \param uid UID used to run the daemon \param gid GID used to run the daemon \param working_directory Working directory used to run the daemon. Example: /var/lib/foo/ \param pid_filename Absolute path of the PID file Example: /var/run/foo.pid (ignored if no_daemon == True) \param no_daemon Do not detach the daemon from the terminal \param debug Run daemon in debug mode \param log The logger, pass None if unused Example: logging.getLogger('foo')) \param rsyslog_host Rsyslog hostname, pass None if unused. If rsyslog_host is set to None, log are stored locally \param rsyslog_port Rsyslog port \param log_file Absolute path of the local log file. Example: /var/log/foo.log) \param log_level Log level Example: logging.INFO """ # Daemon parameters #self.daemon_name = daemon_name self.terminate_callback = terminate_callback #Options().uid = uid #Options().gid = gid #Options().working_directory = working_directory #self.pid_filename = None if no_daemon else pid_filename #Options().no_daemon = no_daemon #Options().lock_file = None #Options().debug = debug #self.log = log #self.rsyslog_host = rsyslog_host #self.rsyslog_port = rsyslog_port #self.log_file = log_file #self.log_level = log_level # Reference which file descriptors must remain opened while # daemonizing (for instance the file descriptor related to # the logger) self.files_to_keep = [] # Initialize self.log (require self.files_to_keep) #if self.log: # for debugging by using stdout, log may be equal to None # if rsyslog_host: # shandler = self.make_handler_rsyslog( # rsyslog_host, # rsyslog_port, # log_level # ) # elif log_file: # shandler = self.make_handler_locallog( # log_file, # log_level # ) @classmethod def init_options(self): opt = Options() opt.add_option( "--uid", dest = "uid", help = "UID used to run the dispatcher.", default = self.DEFAULTS['uid'] ) opt.add_option( "--gid", dest = "gid", help = "GID used to run the dispatcher.", default = self.DEFAULTS['gid'] ) opt.add_option( "-w", "--working-directory", dest = "working_directory", help = "Working directory.", default = self.DEFAULTS['working_directory'] ) opt.add_option( "-D", "--debugmode", action = "store_false", dest = "debugmode", help = "Daemon debug mode (useful for developers).", default = self.DEFAULTS['debugmode'] ) opt.add_option( "-n", "--no-daemon", action = "store_true", dest = "no_daemon", help = "Run as daemon (detach from terminal).", default = self.DEFAULTS["no_daemon"] ) opt.add_option( "-i", "--pid-file", dest = "pid_filename", help = "Absolute path to the pid-file to use when running as daemon.", default = self.DEFAULTS['pid_filename'] ) #------------------------------------------------------------------------ # Daemon stuff #------------------------------------------------------------------------ def remove_pid_file(self): """ \brief Remove the pid file (internal usage) """ # The lock file is implicitely released while removing the pid file Log.debug("Removing %s" % Options().pid_filename) if os.path.exists(Options().pid_filename) == True: os.remove(Options().pid_filename) def make_pid_file(self): """ \brief Create a pid file in which we store the PID of the daemon if needed """ if Options().pid_filename and Options().no_daemon == False: atexit.register(self.remove_pid_file) file(Options().pid_filename, "w+").write("%s\n" % str(os.getpid())) def get_pid_from_pid_file(self): """ \brief Retrieve the PID of the daemon thanks to the pid file. \return None if the pid file is not readable or does not exists """ pid = None if Options().pid_filename: try: f_pid = file(Options().pid_filename, "r") pid = int(f_pid.read().strip()) f_pid.close() except IOError: pid = None return pid def make_lock_file(self): """ \brief Prepare the lock file required to manage the pid file Initialize Options().lock_file """ if Options().pid_filename and Options().no_daemon == False: Log.debug("Daemonizing using pid file '%s'" % Options().pid_filename) Options().lock_file = lockfile.FileLock(Options().pid_filename) if Options().lock_file.is_locked() == True: log_error("'%s' is already running ('%s' is locked)." % (Options().get_name(), Options().pid_filename)) self.terminate() Options().lock_file.acquire() else: Options().lock_file = None def start(self): """ \brief Start the daemon """ # Check whether daemon module is properly installed if self.check_python_daemon() == False: self.terminate() import daemon # Prepare Options().lock_file self.make_lock_file() # Prepare the daemon context dcontext = daemon.DaemonContext( detach_process = (not Options().no_daemon), working_directory = Options().working_directory, pidfile = Options().lock_file if not Options().no_daemon else None, stdin = sys.stdin, stdout = sys.stdout, stderr = sys.stderr, uid = Options().uid, gid = Options().gid, files_preserve = Log().files_to_keep ) # Prepare signal handling to stop properly if the daemon is killed # Note that signal.SIGKILL can't be handled: # http://crunchtools.com/unixlinux-signals-101/ dcontext.signal_map = { signal.SIGTERM : self.signal_handler, signal.SIGQUIT : self.signal_handler, signal.SIGINT : self.signal_handler } if Options().debugmode == True: self.main() else: with dcontext: self.make_pid_file() try: self.main() except Exception, why: Log.error("Unhandled exception in start: %s" % why) def signal_handler(self, signal_id, frame): """ \brief Stop the daemon (signal handler) The lockfile is implicitly released by the daemon package \param signal_id The integer identifying the signal (see also "man 7 signal") Example: 15 if the received signal is signal.SIGTERM \param frame """ self.terminate() def stop(self): Log.debug("Stopping '%s'" % self.daemon_name) def terminate(self): if self.terminate_callback: self.terminate_callback() else: sys.exit(0) Daemon.init_options()