summaryrefslogtreecommitdiff
path: root/daemon.py
blob: 2eedca56b41a33ce861f7b8237e1b6c349ae533b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
"""A daemonize function (for Unices) and daemon mix-in class"""

__docformat__ = "restructuredtext en"

import os
import errno
import signal
import sys
import time
import warnings

def setugid(user):
    """Change process user and group ID

    Argument is a numeric user id or a user name"""
    try:
        from pwd import getpwuid
        passwd = getpwuid(int(user))
    except ValueError:
        from pwd import getpwnam
        passwd = getpwnam(user)

    if hasattr(os, 'initgroups'): # python >= 2.7
        os.initgroups(passwd.pw_name, passwd.pw_gid)
    else:
        import ctypes
        if ctypes.CDLL(None).initgroups(passwd.pw_name, passwd.pw_gid) < 0:
            err = ctypes.c_int.in_dll(ctypes.pythonapi,"errno").value
            raise OSError(err, os.strerror(err), 'initgroups')
    os.setgid(passwd.pw_gid)
    os.setuid(passwd.pw_uid)


def daemonize(pidfile=None, uid=None, umask=077):
    """daemonize a Unix process. Set paranoid umask by default.

    Return 1 in the original process, 2 in the first fork, and None for the
    second fork (eg daemon process).
    """
    # http://www.faqs.org/faqs/unix-faq/programmer/faq/
    #
    # fork so the parent can exit
    if os.fork():   # launch child and...
        return 1
    # disconnect from tty and create a new session
    os.setsid()
    # fork again so the parent, (the session group leader), can exit.
    # as a non-session group leader, we can never regain a controlling
    # terminal.
    if os.fork():   # launch child again.
        return 2
    # move to the root to avoit mount pb
    os.chdir('/')
    # set umask if specified
    if umask is not None:
        os.umask(umask)
    # redirect standard descriptors
    null = os.open('/dev/null', os.O_RDWR)
    for i in range(3):
        try:
            os.dup2(null, i)
        except OSError, e:
            if e.errno != errno.EBADF:
                raise
    os.close(null)
    # filter warnings
    warnings.filterwarnings('ignore')
    # write pid in a file
    if pidfile:
        # ensure the directory where the pid-file should be set exists (for
        # instance /var/run/cubicweb may be deleted on computer restart)
        piddir = os.path.dirname(pidfile)
        if not os.path.exists(piddir):
            os.makedirs(piddir)
        f = file(pidfile, 'w')
        f.write(str(os.getpid()))
        f.close()
    # change process uid
    if uid:
        setugid(uid)
    return None


class DaemonMixIn:
    """Mixin to make a daemon from watchers/queriers.
    """

    def __init__(self, configmod) :
        self.delay = configmod.DELAY
        self.name = str(self.__class__).split('.')[-1]
        self._pid_file = os.path.join('/tmp', '%s.pid'%self.name)
        if os.path.exists(self._pid_file):
            raise Exception('''Another instance of %s must be running.
If it i not the case, remove the file %s''' % (self.name, self._pid_file))
        self._alive = 1
        self._sleeping = 0
        self.config = configmod

    def _daemonize(self):
        if not self.config.NODETACH:
            if daemonize(self._pid_file) is None:
                # put signal handler
                signal.signal(signal.SIGTERM, self.signal_handler)
                signal.signal(signal.SIGHUP, self.signal_handler)
            else:
                return -1

    def run(self):
        """ optionally go in daemon mode and
        do what concrete class has to do and pauses for delay between runs
        If self.delay is negative, do a pause before starting
        """
        if self._daemonize() == -1:
            return
        if self.delay < 0:
            self.delay = -self.delay
            time.sleep(self.delay)
        while True:
            try:
                self._run()
            except Exception, ex:
                # display for info, sleep, and hope the problem will be solved
                # later.
                self.config.exception('Internal error: %s', ex)
            if not self._alive:
                break
            try:
                self._sleeping = 1
                time.sleep(self.delay)
                self._sleeping = 0
            except SystemExit:
                break
        self.config.info('%s instance exited', self.name)
        # remove pid file
        os.remove(self._pid_file)

    def signal_handler(self, sig_num, stack_frame):
        if sig_num == signal.SIGTERM:
            if self._sleeping:
                # we are sleeping so we can exit without fear
                self.config.debug('exit on SIGTERM')
                sys.exit(0)
            else:
                self.config.debug('exit on SIGTERM (on next turn)')
                self._alive = 0
        elif sig_num == signal.SIGHUP:
            self.config.info('reloading configuration on SIGHUP')
            reload(self.config)

    def _run(self):
        """should be overridden in the mixed class"""
        raise NotImplementedError()


import logging
from logilab.common.logging_ext import set_log_methods
set_log_methods(DaemonMixIn, logging.getLogger('lgc.daemon'))

## command line utilities ######################################################

L_OPTIONS = ["help", "log=", "delay=", 'no-detach']
S_OPTIONS = 'hl:d:n'

def print_help(modconfig):
    print """  --help or -h
    displays this message
  --log <log_level>
    log treshold (7 record everything, 0 record only emergency.)
    Defaults to %s
  --delay <delay>
    the number of seconds between two runs.
    Defaults to %s""" % (modconfig.LOG_TRESHOLD, modconfig.DELAY)

def handle_option(modconfig, opt_name, opt_value, help_meth):
    if opt_name in ('-h', '--help'):
        help_meth()
        sys.exit(0)
    elif opt_name in ('-l', '--log'):
        modconfig.LOG_TRESHOLD = int(opt_value)
    elif opt_name in ('-d', '--delay'):
        modconfig.DELAY = int(opt_value)
    elif opt_name in ('-n', '--no-detach'):
        modconfig.NODETACH = 1