def __init__(self): self.config = ConfigFile(self.config_path) self._setup_logging()
class Daemon(object): """ A daemon with start/stop/etc, pidfile, and logging support. Daemon config should exist in /etc/<daemon_name>.conf, for example: [daemon] user: <user_name_to_run_as> group: <group_name_to_run_as> umask: <octal umask> [logging] level: <debug|info|warning> format: <see python logging module> syslog_host: <host to syslog to> syslog_port: <port to syslog to> """ SYSTEM_CONFIG_BASE = "/etc" SYSTEM_READONLY_BASE = "/usr" SYSTEM_DATA_BASE = "/var" name = "unknown_daemon" description = "" autoreload = False should_daemonize = True signal_alias = {} def __init__(self): self.config = ConfigFile(self.config_path) self._setup_logging() # PATHS @property def sys_runtime_path(self): """System runtime data path""" return os.path.join(self.SYSTEM_DATA_BASE, "run") @property def sys_state_path(self): """System state data path""" return os.path.join(self.SYSTEM_DATA_BASE, "lib") @property def sys_lock_path(self): """System lock file path""" return os.path.join(self.SYSTEM_DATA_BASE, "lock") @property def sys_cache_path(self): """System cache data path""" return os.path.join(self.SYSTEM_DATA_BASE, "cache") @property def sys_bin_path(self): """System executable path""" return os.path.join(self.SYSTEM_READONLY_BASE, "bin") @property def sys_lib_path(self): """System architecture-dependent data""" return os.path.join(self.SYSTEM_READONLY_BASE, "lib") @property def sys_share_path(self): """System architecture-independent data""" return os.path.join(self.SYSTEM_READONLY_BASE, "share") @property def pidfile_dir(self): """Process pidfile directory Required for processes that drop permissions""" return os.path.join(self.sys_runtime_path, self.name) @property def pidfile_path(self): """Process pidfile path""" return os.path.join(self.pidfile_dir, "%s.pid" % self.name) @property def pid(self): """The value of the daemon process's pidfile""" try: return int(open(self.pidfile_path).read()) except IOError as ex: if ex.args[0] == errno.ENOENT: raise DaemonStopped() else: raise @property def config_path(self): """Daemon configuration file path""" return os.path.join(self.SYSTEM_CONFIG_BASE, "%s.conf" % self.name) @property def config_dir_path(self): """Daemon configuration directory path""" return os.path.join(self.SYSTEM_CONFIG_BASE, "%s" % self.name) # DAEMON HELPERS def daemonize(self): """Call the given callback in a new daemonized child process""" self._prepare_daemon() self._load_privileges() self._make_pidfile_dir() # Fork child process (unless we run in foreground) try: if self.should_daemonize: first_fork_retval = self._fork() if first_fork_retval > 0: return os.setsid() self._setup_std_pipes() second_fork_retval = self._fork() if second_fork_retval > 0: sys.exit(0) # Unable to fork for some reason except Exception as ex: self.logger.error("Failed to fork daemon process") self.logger.exception(ex) raise SystemExit(1) # Run daemon try: try: self.logger.info("Started") self.handle_prerun() self._drop_privileges() self._write_pidfile() self._setup_signal_handlers() if self.autoreload: self._setup_conf_watcher() self._do_run() self.logger.info("Stopped") finally: self._remove_pidfile() # Log normal exits except SystemExit as ex: errcode = ex.args[0] if errcode == 0: self.logger.info("Stopped") else: self.logger.warning("Stopped with code %d" % errcode) raise # Log abnormal exits except Exception as ex: self.logger.error("Killed by uncaught exception") self.logger.exception(ex) raise SystemExit(1) def _prepare_daemon(self): """Set the umask and chdir to root in preparation.""" umask = self.config("daemon", "umask", 0007, transform=lambda x: int(x, 8)) os.umask(umask) os.chdir("/") def _setup_std_pipes(self): """Duplicate /dev/null's fd over stdin, stdout, and stderr""" devnull = open("/dev/null", "r+") for pipe in [sys.stdin, sys.stdout, sys.stderr]: pipe.flush() os.dup2(devnull.fileno(), pipe.fileno()) def _make_pidfile_dir(self): """Make the pidfile directory with correct permissions""" if not os.path.exists(self.pidfile_dir): os.mkdir(self.pidfile_dir, 0770) os.lchown(self.pidfile_dir, self._use_uid, self._use_gid) def _fork(self): """ Simple os fork. Use this function to maintain compatibility with alternative implementations of fork such as gevent's fork. """ return os.fork() def _write_pidfile(self): """Insert PID into pidfile_path""" pidfile = open(self.pidfile_path, "w") pidfile.write(str(os.getpid())) def _remove_pidfile(self): """Attempt to remove the pidfile""" try: os.unlink(self.pidfile_path) except OSError as ex: self.logger.warning("Could not remove pidfile") self.logger.exception(ex) def _setup_signal_handlers(self): """Map each signal to a function in the daemon class""" signal.signal(signal.SIGINT, lambda *_: self.handle_stop()) signal.signal(signal.SIGTERM, lambda *_: self.handle_stop()) signal.signal(signal.SIGHUP, lambda *_: self.handle_update()) signal.signal(signal.SIGUSR1, lambda *_: self.handle_usr1()) signal.signal(signal.SIGUSR2, lambda *_: self.handle_usr2()) def _setup_conf_watcher(self): """Use pyinotify to reload the config when it gets changed.""" if _USE_PYINOTIFY and os.path.exists(self.config_path): self.logger.info("Starting pyinotify watcher on %s" % self.config_path) wm = pyinotify.WatchManager() notifier = pyinotify.AsyncNotifier(wm, lambda *_: self.handle_update()) wm.add_watch(self.config_path, pyinotify.IN_MODIFY) asyncore.loop() def _load_privileges(self): """Set user/group from the config, or root/root if none are specified""" username = self.config("daemon", "user", "root") groupname = self.config("daemon", "group", "root") userentry = pwd.getpwnam(username) groupentry = grp.getgrnam(groupname) self._use_uid = userentry.pw_uid self._use_gid = groupentry.gr_gid def _drop_privileges(self): """Use the user/group we've previously loaded""" os.setgid(self._use_gid) os.setuid(self._use_uid) def _setup_logging(self): """ By default, logging will be set up to log just about everything possible. In order to change the logging level change /etc/${name}.conf to contain: [logging] level: (info|debug|warning|warning|error|critical) Only choose one of the logging levels above. Logging will also log to syslog by default. """ logger = logging.getLogger() level_names = { "critical" : logging.CRITICAL, "error" : logging.ERROR, "warning" : logging.WARNING, "info" : logging.INFO, "debug" : logging.DEBUG } level = self.config("logging", "level", "info") logger.setLevel(level_names[level.lower()]) log_format = self.config("logging", "format", "%(name)s: %(message)s") formatter = logging.Formatter(log_format) syslog_host = self.config("logging", "syslog_host", "") syslog_port = self.config("logging", "syslog_port", 514) syslog_address = syslog_host and (syslog_host, syslog_port) or "/dev/log" syslog_facility = logging.handlers.SysLogHandler.LOG_DAEMON syslog_handler = logging.handlers.SysLogHandler(syslog_address, syslog_facility) syslog_handler.setFormatter(formatter) logger.addHandler(syslog_handler) stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) self.logger = logging.getLogger(self.name) def _do_run(self): """ Call handle_run, used as a function to support overloading by subclasses such as the GeventDaemon. """ self.handle_run() # PROCESS CONTROL def start(self): """Fork and run a new daemon process.""" if self.status: raise DaemonRunning() self.daemonize() def foreground(self): """Run the process in the foreground.""" self.should_daemonize = False self.start() def stop(self, kill_after=None): """ Stop the daemon process or kill it if it won't stop. @param kill_after Seconds to wait until we kill the process, None means we never kill it """ self.signal(signal.SIGTERM) #All done! if kill_after is None: return #Poll the process status and kill it if it doesn't stop elapsed = 0 increment = 0.25 while elapsed < kill_after: if self.status is False: return time.sleep(increment) elapsed += increment self.kill() def kill(self): """ Kill the process. Kills the process with no notice and no chance to continue. """ self.logger.warning("User sent kill signal. You may need to clean up things like the pidfile.") self.signal(signal.SIGKILL) def restart(self, kill_after=None): """Restart the process.""" if self.status: self.stop(kill_after) self.start() def update(self): """Tell the process to update itself.""" self.signal(signal.SIGHUP) @property def status(self): """ Check if the daemon process is running. @return True if running """ try: self.signal(0) return True except OSError: self.logger.warning("Pidfile exists but no process is running with pid %s" % self.pid) return False except DaemonStopped: return False def signal(self, signum): """Send a signal to the daemon process.""" try: os.kill(self.pid, signum) except OSError as ex: if ex.args[0] == errno.ESRCH: raise DaemonStopped() else: raise # CHILD IMPLEMENTATION def handle_prerun(self): """Prepare for handle_run after fork as a privileged user. Example: Open a server socket on a privileged port, then drop to an unprivileged user """ pass def handle_run(self): """Daemon's main loop.""" raise NotImplementedError() def handle_stop(self): """Handle a stop/sigterm""" raise SystemExit(0) def handle_update(self): """ Handle an update/sighup. This function is called in an interrupt, watchout for deadlock! Default action is to call self.reload_config() """ self.logger.info("Reloading config") self.config.update() def handle_usr1(self): """Signal handler for SIGUSR1 signal""" pass def handle_usr2(self): """Signal handler for SIGUSR2 signal""" pass