Micro framework for fast building daemon app writen over pep3143daemon in python.
Shestakov Dmitriy 6reduk@gmail.com
MIT
pandanas can be installed via pip like this:
pip install pandanas
Concrete daemon realisation located at pandanas.daemon.generic module. All concrete daemon class implements interfaces that provides features for startup daemon and app configure, logging, handle process signals and run app logic.
All daemon child processes must set own signal handler for interact with parent or avoid signal propagation from parent process.
Build concrete multiprocess daemon. App logic make via multiprocessing.Process:
import multiprocessing as mp
import logging
# app logic wrapped at decorator for inject to Process object
def app():
def f():
logger = configure_logging()
# worker can be killed only by parent or SIGKILL|SIGSTOP
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
counter = 60
while counter > 0:
time.sleep(1)
logger.info('Now: {}'.format(time.time()))
counter -= 1
return f
# fabric for create multiprocessing.Process instances
def unit(*args, **kwargs):
return mp.Process(target=app(), **kwargs)
from pandanas.daemon.generic import MultiprocessDaemon
# define
class Worker(MultiprocessDaemon):
# Here define worker that will doing all business logic
worker_class = unit
# Path to pid file
default_pidfile = 'test.pid'
def __init__(self, *args, **kwargs):
super(Worker, self).__init__(*args, **kwargs)
if __name__ == '__main__':
daemon = Worker()
daemon.run()
For run daemon:
python daemon.py -d start
Stop daemon:
python daemon.py stop
For get all predefined cli startup options:
python daemon.py -h
Most usable options:
- Run as daemon use -d option
- Set config path --config, if not set will be used pandanas.daemon.core.mixins.DaemonStartupConfigureMixin.default_config_path value
Now all configure logic located at pandanas.pandanas.daemon.core.mixins.DaemonStartupConfigureMixin class For change configurable behavior need override class attributes or appropriate class method
Attributes:
- default_work_directory - set work directory for daemon after fork. Try code logic with relative paths that lets do chrooting
- default_pidfile - set path for daemon pidfile
- default_config_path - set path for config file
- default_log_file - set path for daemon config file. Keep attention - this file uses only for internal daemon events, for logging app logic use separate logger.
For getting config options uses two steps workflow - get cli options, get config options and merge them together. Cli options have higher priority.
For getting cli options uses argparse module.
DaemonStartupConfigureMixin.get_argparser() return ArgumentParser object. You can add your own options to parser, see argparse module documentation.
Example:
class Worker(MultiprocessDaemon):
def get_argparser(self):
argparser = super(Worker, self).get_argparser()
argparser.add_argument('-o', '--some-option', dest='my', help='My own argument')
return argparser
For fix option collision use conflict_handler argument at ArgumentParser.add_argument() method
After configure argument parser we will processing daemon startup arguments and got Namespace object.
DaemonStartupConfigureMixin.parse_cli_options() return cli config dict. Next step we extract values from Namespace and put to result config dict. Before we can add our own options we must populate config dict by parent.
Example:
class Worker(MultiprocessDaemon):
def parse_cli_options(self, args):
options = super(Worker, self).parse_cli_options(args)
# Here custom logic for define cli result config
options['my'] = args.my
return options
Config use windows like ini files notation. For parse ini files taken configparser. Definition user config options seems as setting options for cli. First step is setting options at ini file in your own called section. Second step, read and parse file. This step no need any action cause configparser parse all section by default. Third step, your need override DaemonStartupConfigureMixin.parse_config_options() for adding needed values
Example:
class Worker(MultiprocessDaemon):
def parse_config_options(self, conf_parser):
options = super(Worker, self).parse_config_options(conf_parser)
if not conf_parser.has_section('my_own_section'):
return options
if conf_parser.has_option('my_own_section', 'some_options'):
options['some_option'] = conf_parser.get('my_own_section', 'some_option')
return options