pythonpython-3.xdaemonpython-daemon

Python daemon work with the same list of objects


I have a class named People. And this class has a list. I don't want to keep this list on a file or database, all in memory, so the way I thought would work is by creating a Daemon and keep the process open, here is my code:

daemon.py

# coding: utf-8
import os
import sys
import time
import atexit
import signal
from people import People

class Daemon(object):
    """
    A generic daemon class.
    Usage: subclass the Daemon class and override the run() method
    """

    def __init__(self, pidfile, stdin='/dev/null',
                 stdout='/dev/null', stderr='/dev/null'):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile
        self.bc = People()

    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
        """
        # Do first fork
        self.fork()

        # Decouple from parent environment
        self.dettach_env()

        # Do second fork
        self.fork()

        # Flush standart file descriptors
        sys.stdout.flush()
        sys.stderr.flush()

        #
        self.attach_stream('stdin', mode='r')
        self.attach_stream('stdout', mode='a+')
        self.attach_stream('stderr', mode='a+')

        # write pidfile
        self.create_pidfile()

    def attach_stream(self, name, mode):
        """
        Replaces the stream with new one
        """
        stream = open(getattr(self, name), mode)
        os.dup2(stream.fileno(), getattr(sys, name).fileno())

    def dettach_env(self):
        os.chdir("/")
        os.setsid()
        os.umask(0)

    def fork(self):
        """
        Spawn the child process
        """
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError as e:
            sys.stderr.write("Fork failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)

    def create_pidfile(self):
        atexit.register(self.delpid)
        pid = str(os.getpid())
        open(self.pidfile, 'w+').write("%s\n" % pid)

    def delpid(self):
        """
        Removes the pidfile on process exit
        """
        os.remove(self.pidfile)

    def start(self):
        """
        Start the daemon
        """
        # Check for a pidfile to see if the daemon already runs
        pid = self.get_pid()

        if pid:
            message = "pidfile %s already exist. Daemon already running?\n"
            sys.stderr.write(message % self.pidfile)
            sys.exit(1)

        # Start the daemon
        self.daemonize()
        self.run()

    def get_pid(self):
        """
        Returns the PID from pidfile
        """
        try:
            pf = open(self.pidfile, 'r')
            pid = int(pf.read().strip())
            pf.close()
        except (IOError, TypeError):
            pid = None
        return pid

    def stop(self, silent=False):
        """
        Stop the daemon
        """
        # Get the pid from the pidfile
        pid = self.get_pid()

        if not pid:
            if not silent:
                message = "pidfile %s does not exist. Daemon not running?\n"
                sys.stderr.write(message % self.pidfile)
            return  # not an error in a restart

        # Try killing the daemon process
        try:
            while True:
                os.kill(pid, signal.SIGTERM)
                time.sleep(0.1)
        except OSError as err:
            err = str(err)
            if err.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                sys.stdout.write(str(err))
                sys.exit(1)

    def restart(self):
        """
        Restart the daemon
        """
        self.stop(silent=True)
        self.start()

    def run(self):
        """
        You should override this method when you subclass Daemon. It will be called after the process has been
        daemonized by start() or restart().
        """
        raise NotImplementedError

And my main file:

# coding: utf-8
import argparse
import sys
import time
from people import People
import logging
from daemon import Daemon


class MyDaemon(Daemon):
    def run(self):
        while True:
            logging.debug("I'm here...")
            time.sleep(1)

    def get_people(self):
        return self.bc



def main():
    """
    The application entry point
    """
    parser = argparse.ArgumentParser(
        description='Daemon runner',
        epilog="That's all folks"
    )

    parser.add_argument(
        'operation',
        metavar='OPERATION',
        type=str,
        help='Operation with daemon. Accepts any of these values: start, stop, restart, status',
        choices=['start', 'stop', 'restart', 'status', 'printpeople', 'add1', 'add2', 'add3', 'add4']
    )

    args = parser.parse_args()
    operation = args.operation

    # Daemon
    logging.basicConfig(filename="foodaemon.log", level=logging.DEBUG)
    daemon = MyDaemon('/Users/marcosaguayo/dev/luracoin/python.pid')

    if operation == 'start':
        print("Starting daemon")
        daemon.start()
        pid = daemon.get_pid()

        if not pid:
            print("Unable run daemon")
        else:
            print("Daemon is running [PID=%d]" % pid)

    elif operation == 'stop':
        print("Stoping daemon")
        daemon.stop()

    elif operation == 'restart':
        print("Restarting daemon")
        daemon.restart()
    elif operation == 'status':
        print("Viewing daemon status")
        pid = daemon.get_pid()

        if not pid:
            print("Daemon isn't running ;)")
        else:
            print("Daemon is running [PID=%d]" % pid)
    elif operation == 'printpeople':
        bc = daemon.get_people()
        print(bc.get_list())
    elif operation == 'add1':
        bc = daemon.get_people()
        bc.add_people({"people": "1"})
        print(bc.get_list())
    elif operation == 'add2':
        bc = daemon.get_people()
        bc.add_people({"people": "2"})
        print(bc.get_list())
    elif operation == 'add3':
        bc = daemon.get_people()
        bc.add_people({"people": "3"})
        print(bc.get_list())
    elif operation == 'add4':
        bc = daemon.get_people()
        bc.add_people({"people": "4"})
        print(bc.get_list())

    sys.exit(0)


if __name__ == '__main__':
    main()

people.py

class People:
    def __init__(self):
        self.people_list = []

    def get_list(self):
        return self.people_list

    def add_people(self, people):
        self.people_list.append(people)

I do the following:

$ python3 test.py start 
*Starting daemon* 
$ python3 test.py add1
*[{'people': '1'}]* 
$ python3 test.py add2
*[{'people': '2'}]*

The python3 test.py add2 should return [{'people': '1'},{'people': '2'}]

What I think is happening is that each time I use the class, the list restarts. I've tried initializing the class on the __init__ of the daemon but doesn't work.

Anyone know how can I solve this?


Solution

  • I do not understand how this could work even in theory. Fixing it would require a complete rewrite.

    You manage to launch your daemon, but then what? You never talk to it. It is there running and it would probably work as intended, but you do not use it for anything.

    When you call your test.py with parameter add1, it creates a new daemon class (but it does not fork and start another background process as you do not call start() on it) with new data structures. This means an empty People list. Then you add to this list, print the result and exit. Your People list with one entry is gone when your process exits. People list in your daemon process is always empty as the daemon process just sits there, waiting in infinite loop and printing logging messages.

    In contrast, your stop command does work, as it just sends a signal to a running process and kills it.

    I can see no evidence anywhere that a new instance of MyDaemon class would somehow find if there is a daemon already running, and then communicate with it.

    Fixing this is more than I have time to do. You need a communication mechanism. Sockets would do, or you could use ZMQ. Or pipes, but then you need two as you need to get a response back. You would use from your current code the parts where you start and stop your daemon.

    When you instantiate your MyDaemon class, you would check whether there is a daemon running. If not, it would start it. And when a daemon starts, it starts listening to the communications channel. Instead of doing nothing in the while True loop, it would check if there are new requests asking it to actually do something.

    If your daemon is already running and you just want to add to the list or query what is in there, you do not need an instance of MyDaemon class at all. You would instead write your request to the socket and then wait for response from your daemon. Writing an example of this is more than I have time to spend, but I hope this gives you the idea where it goes wrong and how to fix it.

    Or then do not fix it by yourself and install a redis server instead. It would be an in-memory key/value store and it might be suitable for your purposes.