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