Skip to content

k3daemonize

Action-CI Documentation Status Package

Create daemon processes with CLI for start/stop/restart. Identifies daemons by PID file to prevent duplicate processes.

k3daemonize is a component of pykit3 project: a python3 toolkit set.

Installation

pip install k3daemonize

Quick Start

import time
import k3daemonize

def run():
    for i in range(100):
        print(i)
        time.sleep(1)

# python foo.py start
# python foo.py stop
# python foo.py restart
if __name__ == '__main__':
    k3daemonize.daemonize_cli(run, '/var/run/pid')

API Reference

k3daemonize

Help to create daemon process. It supplies a command line interface API to start/stop/restart a daemon.

daemonize identifies a daemon by the pid file. Thus two processes those are set up with the same pid file can not run at the same time.

Daemon

Bases: object

Source code in k3daemonize/daemonize.py
 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
class Daemon(object):
    def __init__(self, pidfile=None, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null", close_fds=False):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile or _default_pid_file()
        # NOTE: We need to open another separate file to avoid the file
        #       being reopened again.
        #       In which case, process loses file lock.
        #
        # From "man fcntl":
        # As well as being removed by an explicit F_UNLCK, record locks are
        # automatically released when the process terminates or if it
        # closes any file descriptor referring to a file on which locks
        # are held. This is bad: it means that a process can lose the locks
        # on a file like /etc/passwd or /etc/mtab when for some reason a
        # library function decides to open, read and close it.
        self.lockfile = self.pidfile + ".lock"
        self.lockfp = None
        self.close_fds = close_fds

    def daemonize(self):
        """
        do the UNIX double-fork magic, see Stevens' "Advanced
        Programming in the UNIX Environment" for details (ISBN 0201563177)
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """

        try:
            pid = os.fork()
            if pid > 0:
                # exit first parent
                _close_std_io()
                sys.exit(0)

        except OSError as e:
            logger.error("fork #1 failed: " + repr(e))
            sys.exit(1)

        # decouple from parent environment
        os.setsid()
        os.umask(0)

        # do second fork
        try:
            pid = os.fork()
            if pid > 0:
                # exit from second parent
                _close_std_io()
                sys.exit(0)

        except OSError as e:
            logger.error("fork #2 failed: " + repr(e))
            sys.exit(1)

        if self.close_fds:
            _close_fds()

        # redirect standard file descriptors
        sys.stdout.flush()
        sys.stderr.flush()
        si = open(self.stdin, "r")
        so = open(self.stdout, "a+")
        se = open(self.stderr, "a+")
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        logger.info("OK daemonized")

    def trylock_or_exit(self, timeout=10):
        interval = 0.1
        n = int(timeout / interval) + 1
        flag = fcntl.LOCK_EX | fcntl.LOCK_NB

        for ii in range(n):
            fd = os.open(self.lockfile, os.O_RDWR | os.O_CREAT)

            fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD, 0) | fcntl.FD_CLOEXEC)

            try:
                fcntl.lockf(fd, flag)

                self.lockfp = os.fdopen(fd, "w+")
                break

            except IOError as e:
                os.close(fd)
                if e[0] == errno.EAGAIN:
                    time.sleep(interval)
                else:
                    raise

        else:
            logger.info("Failure acquiring lock %s" % (self.lockfile,))
            sys.exit(1)

        logger.info("OK acquired lock %s" % (self.lockfile))

    def unlock(self):
        if self.lockfp is None:
            return

        fd = self.lockfp.fileno()
        fcntl.lockf(fd, fcntl.LOCK_UN)
        self.lockfp.close()
        self.lockfp = None

    def start(self):
        self.daemonize()
        self.init_proc()

    def init_proc(self):
        self.trylock_or_exit()
        self.write_pid_or_exit()

    def write_pid_or_exit(self):
        self.pf = open(self.pidfile, "w+")
        pf = self.pf

        fd = pf.fileno()
        fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD, 0) | fcntl.FD_CLOEXEC)

        try:
            pid = os.getpid()
            logger.debug("write pid:" + str(pid))

            pf.truncate(0)
            pf.write(str(pid))
            pf.flush()
        except Exception as e:
            logger.exception("write pid failed." + repr(e))
            sys.exit(0)

    def stop(self):
        pid = None

        if not os.path.exists(self.pidfile):
            logger.debug("pidfile not exist:" + self.pidfile)
            return

        try:
            pid = _read_file(self.pidfile)
            pid = int(pid)
            os.kill(pid, signal.SIGTERM)
            return

        except Exception as e:
            logger.warn("{e} while get and kill pid={pid}".format(e=repr(e), pid=pid))

daemonize()

do the UNIX double-fork magic, see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177) http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16

Source code in k3daemonize/daemonize.py
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
def daemonize(self):
    """
    do the UNIX double-fork magic, see Stevens' "Advanced
    Programming in the UNIX Environment" for details (ISBN 0201563177)
    http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
    """

    try:
        pid = os.fork()
        if pid > 0:
            # exit first parent
            _close_std_io()
            sys.exit(0)

    except OSError as e:
        logger.error("fork #1 failed: " + repr(e))
        sys.exit(1)

    # decouple from parent environment
    os.setsid()
    os.umask(0)

    # do second fork
    try:
        pid = os.fork()
        if pid > 0:
            # exit from second parent
            _close_std_io()
            sys.exit(0)

    except OSError as e:
        logger.error("fork #2 failed: " + repr(e))
        sys.exit(1)

    if self.close_fds:
        _close_fds()

    # redirect standard file descriptors
    sys.stdout.flush()
    sys.stderr.flush()
    si = open(self.stdin, "r")
    so = open(self.stdout, "a+")
    se = open(self.stderr, "a+")
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

    logger.info("OK daemonized")

daemonize_cli(run_func, pidfn, close_fds=False)

Read command line arguments and then start, stop or restart a daemon process. :param run_func: a callable object such as a function or lambda to run after the daemon process is created. :param pidfn: abosolute path of pid file. It is used to identify a daemon process. Thus two processes those are with the same pid file can not run at the same time. :param close_fds: If it is True, besides stdin, stdout and stderr, all other file descriptors will also be closed. :return: None

Source code in k3daemonize/daemonize.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def daemonize_cli(run_func, pidfn, close_fds=False):
    """
    Read command line arguments and then start, stop or restart a daemon process.
    :param run_func: a callable object such as a `function` or `lambda` to run after the daemon
    process is created.
    :param pidfn: abosolute path of `pid` file. It is used to identify a daemon process.
    Thus two processes those are with the same `pid` file can not run at the same time.
    :param close_fds: If it is `True`, besides `stdin`, `stdout` and `stderr`, all other file descriptors
    will also be closed.
    :return: None
    """
    logging.basicConfig(stream=sys.stderr)
    logging.getLogger(__name__).setLevel(logging.DEBUG)

    d = Daemon(pidfile=pidfn, close_fds=close_fds)

    logger.info("sys.argv: " + repr(sys.argv))
    try:
        if len(sys.argv) == 1:
            d.init_proc()
            run_func()

        elif len(sys.argv) == 2:
            if "start" == sys.argv[1]:
                d.start()
                run_func()

            elif "stop" == sys.argv[1]:
                d.stop()

            elif "restart" == sys.argv[1]:
                d.stop()
                d.start()
                run_func()

            else:
                logger.error("Unknown command: %s" % (sys.argv[1]))
                print("Unknown command")
                sys.exit(2)

            sys.exit(0)
        else:
            print("usage: %s start|stop|restart" % sys.argv[0])
            sys.exit(2)

    except Exception as e:
        logger.exception(repr(e))

License

The MIT License (MIT) - Copyright (c) 2015 Zhang Yanpo (张炎泼)