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