blob: 1c3241b81170620f5c7bbdc5a6b9149c0667af78 [file] [log] [blame]
koder aka kdanilovdda86d32015-03-16 11:20:04 +02001# #!/usr/bin/python
2
3import fcntl
4import os
5import pwd
6import grp
7import sys
8import signal
9import resource
10import logging
11import atexit
12from logging import handlers
13
14
15class 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)