koder aka kdanilov | dda86d3 | 2015-03-16 11:20:04 +0200 | [diff] [blame] | 1 | # #!/usr/bin/python |
| 2 | |
| 3 | import fcntl |
| 4 | import os |
| 5 | import pwd |
| 6 | import grp |
| 7 | import sys |
| 8 | import signal |
| 9 | import resource |
| 10 | import logging |
| 11 | import atexit |
| 12 | from logging import handlers |
| 13 | |
| 14 | |
| 15 | class Daemonize(object): |
| 16 | """ Daemonize object |
| 17 | Object constructor expects three arguments: |
| 18 | - app: contains the application name which will be sent to syslog. |
| 19 | - pid: path to the pidfile. |
| 20 | - action: your custom function which will be executed after daemonization. |
| 21 | - keep_fds: optional list of fds which should not be closed. |
| 22 | - auto_close_fds: optional parameter to not close opened fds. |
| 23 | - privileged_action: action that will be executed before drop privileges if user or |
| 24 | group parameter is provided. |
| 25 | If you want to transfer anything from privileged_action to action, such as |
| 26 | opened privileged file descriptor, you should return it from |
| 27 | privileged_action function and catch it inside action function. |
| 28 | - user: drop privileges to this user if provided. |
| 29 | - group: drop privileges to this group if provided. |
| 30 | - verbose: send debug messages to logger if provided. |
| 31 | - logger: use this logger object instead of creating new one, if provided. |
| 32 | """ |
| 33 | def __init__(self, app, pid, action, keep_fds=None, auto_close_fds=True, privileged_action=None, user=None, group=None, verbose=False, logger=None): |
| 34 | self.app = app |
| 35 | self.pid = pid |
| 36 | self.action = action |
| 37 | self.keep_fds = keep_fds or [] |
| 38 | self.privileged_action = privileged_action or (lambda: ()) |
| 39 | self.user = user |
| 40 | self.group = group |
| 41 | self.logger = logger |
| 42 | self.verbose = verbose |
| 43 | self.auto_close_fds = auto_close_fds |
| 44 | |
| 45 | def sigterm(self, signum, frame): |
| 46 | """ sigterm method |
| 47 | These actions will be done after SIGTERM. |
| 48 | """ |
| 49 | self.logger.warn("Caught signal %s. Stopping daemon." % signum) |
| 50 | os.remove(self.pid) |
| 51 | sys.exit(0) |
| 52 | |
| 53 | def exit(self): |
| 54 | """ exit method |
| 55 | Cleanup pid file at exit. |
| 56 | """ |
| 57 | self.logger.warn("Stopping daemon.") |
| 58 | os.remove(self.pid) |
| 59 | sys.exit(0) |
| 60 | |
| 61 | def start(self): |
| 62 | """ start method |
| 63 | Main daemonization process. |
| 64 | """ |
| 65 | # If pidfile already exists, we should read pid from there; to overwrite it, if locking |
| 66 | # will fail, because locking attempt somehow purges the file contents. |
| 67 | if os.path.isfile(self.pid): |
| 68 | with open(self.pid, "r") as old_pidfile: |
| 69 | old_pid = old_pidfile.read() |
| 70 | # Create a lockfile so that only one instance of this daemon is running at any time. |
| 71 | try: |
| 72 | lockfile = open(self.pid, "w") |
| 73 | except IOError: |
| 74 | print("Unable to create the pidfile.") |
| 75 | sys.exit(1) |
| 76 | try: |
| 77 | # Try to get an exclusive lock on the file. This will fail if another process has the file |
| 78 | # locked. |
| 79 | fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 80 | except IOError: |
| 81 | print("Unable to lock on the pidfile.") |
| 82 | # We need to overwrite the pidfile if we got here. |
| 83 | with open(self.pid, "w") as pidfile: |
| 84 | pidfile.write(old_pid) |
| 85 | sys.exit(1) |
| 86 | |
| 87 | # Fork, creating a new process for the child. |
| 88 | process_id = os.fork() |
| 89 | if process_id < 0: |
| 90 | # Fork error. Exit badly. |
| 91 | sys.exit(1) |
| 92 | elif process_id != 0: |
| 93 | # This is the parent process. Exit. |
| 94 | sys.exit(0) |
| 95 | # This is the child process. Continue. |
| 96 | |
| 97 | # Stop listening for signals that the parent process receives. |
| 98 | # This is done by getting a new process id. |
| 99 | # setpgrp() is an alternative to setsid(). |
| 100 | # setsid puts the process in a new parent group and detaches its controlling terminal. |
| 101 | process_id = os.setsid() |
| 102 | if process_id == -1: |
| 103 | # Uh oh, there was a problem. |
| 104 | sys.exit(1) |
| 105 | |
| 106 | # Add lockfile to self.keep_fds. |
| 107 | self.keep_fds.append(lockfile.fileno()) |
| 108 | |
| 109 | # Close all file descriptors, except the ones mentioned in self.keep_fds. |
| 110 | devnull = "/dev/null" |
| 111 | if hasattr(os, "devnull"): |
| 112 | # Python has set os.devnull on this system, use it instead as it might be different |
| 113 | # than /dev/null. |
| 114 | devnull = os.devnull |
| 115 | |
| 116 | if self.auto_close_fds: |
| 117 | for fd in range(3, resource.getrlimit(resource.RLIMIT_NOFILE)[0]): |
| 118 | if fd not in self.keep_fds: |
| 119 | try: |
| 120 | os.close(fd) |
| 121 | except OSError: |
| 122 | pass |
| 123 | |
| 124 | devnull_fd = os.open(devnull, os.O_RDWR) |
| 125 | os.dup2(devnull_fd, 0) |
| 126 | os.dup2(devnull_fd, 1) |
| 127 | os.dup2(devnull_fd, 2) |
| 128 | |
| 129 | if self.logger is None: |
| 130 | # Initialize logging. |
| 131 | self.logger = logging.getLogger(self.app) |
| 132 | self.logger.setLevel(logging.DEBUG) |
| 133 | # Display log messages only on defined handlers. |
| 134 | self.logger.propagate = False |
| 135 | |
| 136 | # Initialize syslog. |
| 137 | # It will correctly work on OS X, Linux and FreeBSD. |
| 138 | if sys.platform == "darwin": |
| 139 | syslog_address = "/var/run/syslog" |
| 140 | else: |
| 141 | syslog_address = "/dev/log" |
| 142 | |
| 143 | # We will continue with syslog initialization only if actually have such capabilities |
| 144 | # on the machine we are running this. |
| 145 | if os.path.isfile(syslog_address): |
| 146 | syslog = handlers.SysLogHandler(syslog_address) |
| 147 | if self.verbose: |
| 148 | syslog.setLevel(logging.DEBUG) |
| 149 | else: |
| 150 | syslog.setLevel(logging.INFO) |
| 151 | # Try to mimic to normal syslog messages. |
| 152 | formatter = logging.Formatter("%(asctime)s %(name)s: %(message)s", |
| 153 | "%b %e %H:%M:%S") |
| 154 | syslog.setFormatter(formatter) |
| 155 | |
| 156 | self.logger.addHandler(syslog) |
| 157 | |
| 158 | # Set umask to default to safe file permissions when running as a root daemon. 027 is an |
| 159 | # octal number which we are typing as 0o27 for Python3 compatibility. |
| 160 | os.umask(0o27) |
| 161 | |
| 162 | # Change to a known directory. If this isn't done, starting a daemon in a subdirectory that |
| 163 | # needs to be deleted results in "directory busy" errors. |
| 164 | os.chdir("/") |
| 165 | |
| 166 | # Execute privileged action |
| 167 | privileged_action_result = self.privileged_action() |
| 168 | if not privileged_action_result: |
| 169 | privileged_action_result = [] |
| 170 | |
| 171 | # Change gid |
| 172 | if self.group: |
| 173 | try: |
| 174 | gid = grp.getgrnam(self.group).gr_gid |
| 175 | except KeyError: |
| 176 | self.logger.error("Group {0} not found".format(self.group)) |
| 177 | sys.exit(1) |
| 178 | try: |
| 179 | os.setgid(gid) |
| 180 | except OSError: |
| 181 | self.logger.error("Unable to change gid.") |
| 182 | sys.exit(1) |
| 183 | |
| 184 | # Change uid |
| 185 | if self.user: |
| 186 | try: |
| 187 | uid = pwd.getpwnam(self.user).pw_uid |
| 188 | except KeyError: |
| 189 | self.logger.error("User {0} not found.".format(self.user)) |
| 190 | sys.exit(1) |
| 191 | try: |
| 192 | os.setuid(uid) |
| 193 | except OSError: |
| 194 | self.logger.error("Unable to change uid.") |
| 195 | sys.exit(1) |
| 196 | |
| 197 | try: |
| 198 | lockfile.write("%s" % (os.getpid())) |
| 199 | lockfile.flush() |
| 200 | except IOError: |
| 201 | self.logger.error("Unable to write pid to the pidfile.") |
| 202 | print("Unable to write pid to the pidfile.") |
| 203 | sys.exit(1) |
| 204 | |
| 205 | # Set custom action on SIGTERM. |
| 206 | signal.signal(signal.SIGTERM, self.sigterm) |
| 207 | atexit.register(self.exit) |
| 208 | |
| 209 | self.logger.warn("Starting daemon.") |
| 210 | |
| 211 | self.action(*privileged_action_result) |