ext_daemon.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. """
  2. The Daemon Extension enables applications Built on Cement (tm) to
  3. easily perform standard daemonization functions.
  4. Requirements
  5. ------------
  6. * Python 2.6+, Python 3+
  7. * Available on Unix/Linux only
  8. Features
  9. --------
  10. * Configurable runtime user and group
  11. * Adds the ``--daemon`` command line option
  12. * Adds ``app.daemonize()`` function to trigger daemon functionality where
  13. necessary (either in a cement ``pre_run`` hook or an application controller
  14. sub-command, etc)
  15. * Manages a pid file including cleanup on ``app.close()``
  16. Configuration
  17. -------------
  18. The daemon extension is configurable with the following settings under the
  19. [daemon] section.
  20. * **user** - The user name to run the process as.
  21. Default: os.getlogin()
  22. * **group** - The group name to run the process as.
  23. Default: The primary group of the 'user'.
  24. * **dir** - The directory to run the process in.
  25. Default: /
  26. * **pid_file** - The filesystem path to store the PID (Process ID) file.
  27. Default: None
  28. * **umask** - The umask value to pass to os.umask().
  29. Default: 0
  30. Configurations can be passed as defaults to a CementApp:
  31. .. code-block:: python
  32. from cement.core.foundation import CementApp
  33. from cement.utils.misc import init_defaults
  34. defaults = init_defaults('myapp', 'daemon')
  35. defaults['daemon']['user'] = 'myuser'
  36. defaults['daemon']['group'] = 'mygroup'
  37. defaults['daemon']['dir'] = '/var/lib/myapp/'
  38. defaults['daemon']['pid_file'] = '/var/run/myapp/myapp.pid'
  39. defaults['daemon']['umask'] = 0
  40. app = CementApp('myapp', config_defaults=defaults)
  41. Application defaults are then overridden by configurations parsed via a
  42. ``[demon]`` config section in any of the applications configuration paths.
  43. An example configuration block would look like:
  44. .. code-block:: text
  45. [daemon]
  46. user = myuser
  47. group = mygroup
  48. dir = /var/lib/myapp/
  49. pid_file = /var/run/myapp/myapp.pid
  50. umask = 0
  51. Usage
  52. -----
  53. The following example shows how to add the daemon extension, as well as
  54. trigger daemon functionality before ``app.run()`` is called.
  55. .. code-block:: python
  56. from time import sleep
  57. from cement.core.foundation import CementApp
  58. class MyApp(CementApp):
  59. class Meta:
  60. label = 'myapp'
  61. extensions = ['daemon']
  62. with MyApp() as app:
  63. app.daemonize()
  64. app.run()
  65. count = 0
  66. while True:
  67. count = count + 1
  68. print('Iteration: %s' % count)
  69. sleep(10)
  70. An alternative to the above is to put the ``daemonize()`` call within a
  71. framework hook:
  72. .. code-block:: python
  73. def make_daemon(app):
  74. app.daemonize()
  75. def load(app):
  76. app.hook.register('pre_run', make_daemon)
  77. Finally, some applications may prefer to only daemonize certain sub-commands
  78. rather than the entire parent application. For example:
  79. .. code-block:: python
  80. from cement.core.foundation import CementApp
  81. from cement.core.controller import CementBaseController, expose
  82. class MyBaseController(CementBaseController):
  83. class Meta:
  84. label = 'base'
  85. @expose(help="run the daemon command.")
  86. def run_forever(self):
  87. from time import sleep
  88. self.app.daemonize()
  89. count = 0
  90. while True:
  91. count = count + 1
  92. print(count)
  93. sleep(10)
  94. class MyApp(CementApp):
  95. class Meta:
  96. label = 'myapp'
  97. base_controller = MyBaseController
  98. extensions = ['daemon']
  99. with MyApp() as app:
  100. app.run()
  101. By default, even after ``app.daemonize()`` is called... the application will
  102. continue to run in the foreground, but will still manage the pid and
  103. user/group switching. To detach a process and send it to the background you
  104. simply pass the ``--daemon`` option at command line.
  105. .. code-block:: text
  106. $ python example.py --daemon
  107. $ ps -x | grep example
  108. 37421 ?? 0:00.01 python example2.py --daemon
  109. 37452 ttys000 0:00.00 grep example
  110. """
  111. import os
  112. import sys
  113. import io
  114. import pwd
  115. import grp
  116. from ..core import exc
  117. from ..utils.misc import minimal_logger
  118. LOG = minimal_logger(__name__)
  119. LOG = minimal_logger(__name__)
  120. CEMENT_DAEMON_ENV = None
  121. CEMENT_DAEMON_APP = None
  122. class Environment(object):
  123. """
  124. This class provides a mechanism for altering the running processes
  125. environment.
  126. Optional Arguments:
  127. :keyword stdin: A file to read STDIN from. Default: ``/dev/null``
  128. :keyword stdout: A file to write STDOUT to. Default: ``/dev/null``
  129. :keyword stderr: A file to write STDERR to. Default: ``/dev/null``
  130. :keyword dir: The directory to run the process in.
  131. :keyword pid_file: The filesystem path to where the PID (Process ID)
  132. should be written to. Default: None
  133. :keyword user: The user name to run the process as.
  134. Default: ``os.getlogin()``
  135. :keyword group: The group name to run the process as.
  136. Default: The primary group of ``os.getlogin()``.
  137. :keyword umask: The umask to pass to os.umask(). Default: ``0``
  138. """
  139. def __init__(self, **kw):
  140. self.stdin = kw.get('stdin', '/dev/null')
  141. self.stdout = kw.get('stdout', '/dev/null')
  142. self.stderr = kw.get('stderr', '/dev/null')
  143. self.dir = kw.get('dir', os.curdir)
  144. self.pid_file = kw.get('pid_file', None)
  145. self.umask = kw.get('umask', 0)
  146. self.user = kw.get('user', os.getlogin())
  147. # clean up
  148. self.dir = os.path.abspath(os.path.expanduser(self.dir))
  149. if self.pid_file:
  150. self.pid_file = os.path.abspath(os.path.expanduser(self.pid_file))
  151. try:
  152. self.user = pwd.getpwnam(self.user)
  153. except KeyError as e:
  154. raise exc.FrameworkError("Daemon user '%s' doesn't exist." %
  155. self.user)
  156. try:
  157. self.group = kw.get('group',
  158. grp.getgrgid(self.user.pw_gid).gr_name)
  159. self.group = grp.getgrnam(self.group)
  160. except KeyError as e:
  161. raise exc.FrameworkError("Daemon group '%s' doesn't exist." %
  162. self.group)
  163. def _write_pid_file(self):
  164. """
  165. Writes ``os.getpid()`` out to ``self.pid_file``.
  166. """
  167. pid = str(os.getpid())
  168. LOG.debug('writing pid (%s) out to %s' % (pid, self.pid_file))
  169. # setup pid
  170. if self.pid_file:
  171. f = open(self.pid_file, 'w')
  172. f.write(pid)
  173. f.close()
  174. os.chown(self.pid_file, self.user.pw_uid, self.group.gr_gid)
  175. def switch(self):
  176. """
  177. Switch the current process's user/group to ``self.user``, and
  178. ``self.group``. Change directory to ``self.dir``, and write the
  179. current pid out to ``self.pid_file``.
  180. """
  181. # set the running uid/gid
  182. LOG.debug('setting process uid(%s) and gid(%s)' %
  183. (self.user.pw_uid, self.group.gr_gid))
  184. os.setgid(self.group.gr_gid)
  185. os.setuid(self.user.pw_uid)
  186. os.environ['HOME'] = self.user.pw_dir
  187. os.chdir(self.dir)
  188. if self.pid_file and os.path.exists(self.pid_file):
  189. raise exc.FrameworkError("Process already running (%s)" %
  190. self.pid_file)
  191. else:
  192. self._write_pid_file()
  193. def daemonize(self): # pragma: no cover
  194. """
  195. Fork the current process into a daemon.
  196. References:
  197. UNIX Programming FAQ:
  198. 1.7 How do I get my program to act like a daemon?
  199. http://www.unixguide.net/unix/programming/1.7.shtml
  200. http://www.faqs.org/faqs/unix-faq/programmer/faq/
  201. Advanced Programming in the Unix Environment
  202. W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7.
  203. """
  204. LOG.debug('attempting to daemonize the current process')
  205. # Do first fork.
  206. try:
  207. pid = os.fork()
  208. if pid > 0:
  209. LOG.debug('successfully detached from first parent')
  210. os._exit(os.EX_OK)
  211. except OSError as e:
  212. sys.stderr.write("Fork #1 failed: (%d) %s\n" %
  213. (e.errno, e.strerror))
  214. sys.exit(1)
  215. # Decouple from parent environment.
  216. os.chdir(self.dir)
  217. os.umask(int(self.umask))
  218. os.setsid()
  219. # Do second fork.
  220. try:
  221. pid = os.fork()
  222. if pid > 0:
  223. LOG.debug('successfully detached from second parent')
  224. os._exit(os.EX_OK)
  225. except OSError as e:
  226. sys.stderr.write("Fork #2 failed: (%d) %s\n" %
  227. (e.errno, e.strerror))
  228. sys.exit(1)
  229. # Redirect standard file descriptors.
  230. stdin = open(self.stdin, 'r')
  231. stdout = open(self.stdout, 'a+')
  232. stderr = open(self.stderr, 'a+')
  233. if hasattr(sys.stdin, 'fileno'):
  234. try:
  235. os.dup2(stdin.fileno(), sys.stdin.fileno())
  236. except io.UnsupportedOperation as e:
  237. # FIXME: ?
  238. pass
  239. if hasattr(sys.stdout, 'fileno'):
  240. try:
  241. os.dup2(stdout.fileno(), sys.stdout.fileno())
  242. except io.UnsupportedOperation as e:
  243. # FIXME: ?
  244. pass
  245. if hasattr(sys.stderr, 'fileno'):
  246. try:
  247. os.dup2(stderr.fileno(), sys.stderr.fileno())
  248. except io.UnsupportedOperation as e:
  249. # FIXME: ?
  250. pass
  251. # Update our pid file
  252. self._write_pid_file()
  253. def daemonize(): # pragma: no cover
  254. """
  255. This function switches the running user/group to that configured in
  256. ``config['daemon']['user']`` and ``config['daemon']['group']``. The
  257. default user is ``os.getlogin()`` and the default group is that user's
  258. primary group. A pid_file and directory to run in is also passed to the
  259. environment.
  260. It is important to note that with the daemon extension enabled, the
  261. environment will switch user/group/set pid/etc regardless of whether
  262. the ``--daemon`` option was passed at command line or not. However, the
  263. process will only 'daemonize' if the option is passed to do so. This
  264. allows the program to run exactly the same in forground or background.
  265. """
  266. # We want to honor the runtime user/group/etc even if --daemon is not
  267. # passed... but only daemonize if it is.
  268. global CEMENT_DAEMON_ENV
  269. global CEMENT_DAEMON_APP
  270. app = CEMENT_DAEMON_APP
  271. CEMENT_DAEMON_ENV = Environment(
  272. user=app.config.get('daemon', 'user'),
  273. group=app.config.get('daemon', 'group'),
  274. pid_file=app.config.get('daemon', 'pid_file'),
  275. dir=app.config.get('daemon', 'dir'),
  276. umask=app.config.get('daemon', 'umask'),
  277. )
  278. CEMENT_DAEMON_ENV.switch()
  279. if '--daemon' in app.argv:
  280. CEMENT_DAEMON_ENV.daemonize()
  281. def extend_app(app):
  282. """
  283. Adds the ``--daemon`` argument to the argument object, and sets the
  284. default ``[daemon]`` config section options.
  285. """
  286. global CEMENT_DAEMON_APP
  287. CEMENT_DAEMON_APP = app
  288. app.args.add_argument('--daemon', dest='daemon',
  289. action='store_true', help='daemonize the process')
  290. # Add default config
  291. user = pwd.getpwnam(os.getlogin())
  292. group = grp.getgrgid(user.pw_gid)
  293. defaults = dict()
  294. defaults['daemon'] = dict()
  295. defaults['daemon']['user'] = user.pw_name
  296. defaults['daemon']['group'] = group.gr_name
  297. defaults['daemon']['pid_file'] = None
  298. defaults['daemon']['dir'] = '/'
  299. defaults['daemon']['umask'] = 0
  300. app.config.merge(defaults, override=False)
  301. app.extend('daemonize', daemonize)
  302. def cleanup(app): # pragma: no cover
  303. """
  304. After application run time, this hook just attempts to clean up the
  305. pid_file if one was set, and exists.
  306. """
  307. global CEMENT_DAEMON_ENV
  308. if CEMENT_DAEMON_ENV and CEMENT_DAEMON_ENV.pid_file:
  309. if os.path.exists(CEMENT_DAEMON_ENV.pid_file):
  310. LOG.debug('Cleaning up pid_file...')
  311. pid = open(CEMENT_DAEMON_ENV.pid_file, 'r').read().strip()
  312. # only remove it if we created it.
  313. if int(pid) == int(os.getpid()):
  314. os.remove(CEMENT_DAEMON_ENV.pid_file)
  315. def load(app):
  316. app.hook.register('post_setup', extend_app)
  317. app.hook.register('pre_close', cleanup)