| # #!/usr/bin/python |
| |
| import os |
| import pwd |
| import grp |
| import sys |
| import fcntl |
| import signal |
| import atexit |
| import logging |
| import resource |
| |
| |
| class Daemonize(object): |
| """ Daemonize object |
| Object constructor expects three arguments: |
| - app: contains the application name which will be sent to syslog. |
| - pid: path to the pidfile. |
| - action: your custom function which will be executed after daemonization. |
| - keep_fds: optional list of fds which should not be closed. |
| - auto_close_fds: optional parameter to not close opened fds. |
| - privileged_action: action that will be executed before |
| drop privileges if user or |
| group parameter is provided. |
| If you want to transfer anything from privileged |
| action to action, such as opened privileged file |
| descriptor, you should return it from |
| privileged_action function and catch it inside action |
| function. |
| - user: drop privileges to this user if provided. |
| - group: drop privileges to this group if provided. |
| - verbose: send debug messages to logger if provided. |
| - logger: use this logger object instead of creating new one, if provided. |
| """ |
| def __init__(self, app, pid, action, keep_fds=None, auto_close_fds=True, |
| privileged_action=None, user=None, group=None, verbose=False, |
| logger=None): |
| self.app = app |
| self.pid = pid |
| self.action = action |
| self.keep_fds = keep_fds or [] |
| self.privileged_action = privileged_action or (lambda: ()) |
| self.user = user |
| self.group = group |
| self.logger = logger |
| self.verbose = verbose |
| self.auto_close_fds = auto_close_fds |
| |
| def sigterm(self, signum, frame): |
| """ sigterm method |
| These actions will be done after SIGTERM. |
| """ |
| self.logger.warn("Caught signal %s. Stopping daemon." % signum) |
| os.remove(self.pid) |
| sys.exit(0) |
| |
| def exit(self): |
| """ exit method |
| Cleanup pid file at exit. |
| """ |
| self.logger.warn("Stopping daemon.") |
| os.remove(self.pid) |
| sys.exit(0) |
| |
| def start(self): |
| """ start method |
| Main daemonization process. |
| """ |
| # If pidfile already exists, we should read pid from there; |
| # to overwrite it, if locking |
| # will fail, because locking attempt somehow purges the file contents. |
| if os.path.isfile(self.pid): |
| with open(self.pid, "r") as old_pidfile: |
| old_pid = old_pidfile.read() |
| # Create a lockfile so that only one instance of this daemon is |
| # running at any time. |
| try: |
| lockfile = open(self.pid, "w") |
| except IOError: |
| print("Unable to create the pidfile.") |
| sys.exit(1) |
| try: |
| # Try to get an exclusive lock on the file. This will fail if |
| # another process has the file |
| # locked. |
| fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| except IOError: |
| print("Unable to lock on the pidfile.") |
| # We need to overwrite the pidfile if we got here. |
| with open(self.pid, "w") as pidfile: |
| pidfile.write(old_pid) |
| sys.exit(1) |
| |
| # Fork, creating a new process for the child. |
| process_id = os.fork() |
| if process_id < 0: |
| # Fork error. Exit badly. |
| sys.exit(1) |
| elif process_id != 0: |
| # This is the parent process. Exit. |
| sys.exit(0) |
| # This is the child process. Continue. |
| |
| # Stop listening for signals that the parent process receives. |
| # This is done by getting a new process id. |
| # setpgrp() is an alternative to setsid(). |
| # setsid puts the process in a new parent group and detaches |
| # its controlling terminal. |
| process_id = os.setsid() |
| if process_id == -1: |
| # Uh oh, there was a problem. |
| sys.exit(1) |
| |
| # Add lockfile to self.keep_fds. |
| self.keep_fds.append(lockfile.fileno()) |
| |
| # Close all file descriptors, except the ones mentioned in |
| # self.keep_fds. |
| devnull = "/dev/null" |
| if hasattr(os, "devnull"): |
| # Python has set os.devnull on this system, use it instead as it |
| # might be different |
| # than /dev/null. |
| devnull = os.devnull |
| |
| if self.auto_close_fds: |
| for fd in range(3, resource.getrlimit(resource.RLIMIT_NOFILE)[0]): |
| if fd not in self.keep_fds: |
| try: |
| os.close(fd) |
| except OSError: |
| pass |
| |
| devnull_fd = os.open(devnull, os.O_RDWR) |
| os.dup2(devnull_fd, 0) |
| os.dup2(devnull_fd, 1) |
| os.dup2(devnull_fd, 2) |
| |
| if self.logger is None: |
| # Initialize logging. |
| self.logger = logging.getLogger(self.app) |
| self.logger.setLevel(logging.DEBUG) |
| # Display log messages only on defined handlers. |
| self.logger.propagate = False |
| |
| # Initialize syslog. |
| # It will correctly work on OS X, Linux and FreeBSD. |
| if sys.platform == "darwin": |
| syslog_address = "/var/run/syslog" |
| else: |
| syslog_address = "/dev/log" |
| |
| # We will continue with syslog initialization only if |
| # actually have such capabilities |
| # on the machine we are running this. |
| if os.path.isfile(syslog_address): |
| syslog = logging.handlers.SysLogHandler(syslog_address) |
| if self.verbose: |
| syslog.setLevel(logging.DEBUG) |
| else: |
| syslog.setLevel(logging.INFO) |
| # Try to mimic to normal syslog messages. |
| format_t = "%(asctime)s %(name)s: %(message)s" |
| formatter = logging.Formatter(format_t, |
| "%b %e %H:%M:%S") |
| syslog.setFormatter(formatter) |
| |
| self.logger.addHandler(syslog) |
| |
| # Set umask to default to safe file permissions when running |
| # as a root daemon. 027 is an |
| # octal number which we are typing as 0o27 for Python3 compatibility. |
| os.umask(0o27) |
| |
| # Change to a known directory. If this isn't done, starting a daemon |
| # in a subdirectory that |
| # needs to be deleted results in "directory busy" errors. |
| os.chdir("/") |
| |
| # Execute privileged action |
| privileged_action_result = self.privileged_action() |
| if not privileged_action_result: |
| privileged_action_result = [] |
| |
| # Change gid |
| if self.group: |
| try: |
| gid = grp.getgrnam(self.group).gr_gid |
| except KeyError: |
| self.logger.error("Group {0} not found".format(self.group)) |
| sys.exit(1) |
| try: |
| os.setgid(gid) |
| except OSError: |
| self.logger.error("Unable to change gid.") |
| sys.exit(1) |
| |
| # Change uid |
| if self.user: |
| try: |
| uid = pwd.getpwnam(self.user).pw_uid |
| except KeyError: |
| self.logger.error("User {0} not found.".format(self.user)) |
| sys.exit(1) |
| try: |
| os.setuid(uid) |
| except OSError: |
| self.logger.error("Unable to change uid.") |
| sys.exit(1) |
| |
| try: |
| lockfile.write("%s" % (os.getpid())) |
| lockfile.flush() |
| except IOError: |
| self.logger.error("Unable to write pid to the pidfile.") |
| print("Unable to write pid to the pidfile.") |
| sys.exit(1) |
| |
| # Set custom action on SIGTERM. |
| signal.signal(signal.SIGTERM, self.sigterm) |
| atexit.register(self.exit) |
| |
| self.logger.warn("Starting daemon.") |
| |
| self.action(*privileged_action_result) |