From: Andrew Date: Thu, 17 Jun 2021 14:53:50 +0000 (+0100) Subject: Added generic_python_deamon X-Git-Url: https://git.liquid.me.uk/?a=commitdiff_plain;h=a21597ba889a703d83ba1d03bfba46f9584e65ee;p=stuff.git Added generic_python_deamon An ugly to read yet robust python daemon template/class, 'cos I bored writing it from scratch. Includes openrc and systemd startup scripts/.service files. --- diff --git a/generic_python_daemon/etc_init.d_genpyd b/generic_python_daemon/etc_init.d_genpyd new file mode 100755 index 0000000..580b0fb --- /dev/null +++ b/generic_python_daemon/etc_init.d_genpyd @@ -0,0 +1,27 @@ +#!/sbin/openrc-run + +DAEMON_NAME="generic_python_daemon" + +#depend() { +# some depends like 'after net.eth0' or 'before display-manager' etc... +#} + +start() { + ebegin "Starting ${DAEMON_NAME//_/ }" + start-stop-daemon --start --exec /usr/bin/${DAEMON_NAME}.py \ + --pidfile /var/run/${DAEMON_NAME}.pid -- start + eend $? +} + +stop() { + ebegin "Stopping ${DAEMON_NAME//_/ }" + start-stop-daemon --stop --exec /usr/bin/${DAEMON_NAME}.py \ + --pidfile /var/run/${DAEMON_NAME}.pid -- stop + eend $? +} + +status() { + ebegin "Checking ${DAEMON_NAME//_/ } status" + /usr/bin/${DAEMON_NAME}.py status + eend $? +} diff --git a/generic_python_daemon/generic_python_daemon.py b/generic_python_daemon/generic_python_daemon.py new file mode 100755 index 0000000..a6905d2 --- /dev/null +++ b/generic_python_daemon/generic_python_daemon.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +import os +from sys import exit,argv,stdout,stderr +from signal import signal,Signals,SIGINT,SIGTERM +from psutil import process_iter,Process,TimeoutExpired +from time import sleep,strftime,gmtime +from getpass import getuser + +class pydaemon: + def __init__(self,name): + self.name=name + self.user=getuser() + uid=os.getuid() + if uid > 0:uid='user/%s/'%uid + else: uid = '' + self.pid_filename='/var/run/%s%s.pid'%(uid,self.name) + self.debug=5 + self.loop_timer=1 + self.exit_timer=5 + + def echo(self,message,**kws): + d={'flush':False,'end':'\n','sep':' ','file':stdout,'level':0} + for key in kws:d[key]=kws[key] + if self.debug >= d['level']: + if d['level'] > 4:d['file']=stderr + if self.debug > 1: + print ('%s %s: '%(strftime('%Y-%m-%d %H:%M:%S',gmtime()),self.name),end='',file=d['file']) + d['flush']=True;d['end']='\n' + print(message,end=d['end'],sep=d['sep'],file=d['file'],flush=d['flush']) + + def exit(self,sig,frame): + self.echo('Starting exit...',level=2) + sig_name=str(Signals(sig))[8:] + self.echo('%s: Received %s, stopping gracefully ... ' %(self.name,sig_name),end='',level=1,flush=True) + self.clean_up() + self.echo('done.',level=1) + exit(0) + + def start(self): + self.echo('Starting start',level=2) + signal(SIGTERM,self.exit) + try: + pid_file=open(self.pid_filename,'x') + except FileExistsError: + self.echo('PID file already exists.',level=2) + if self.alive(self.readpid()):self.echo('%s: Daemon already running, exiting!'%self.name,level=-1);exit(1) + self.echo('%s: Removing stale PID file: %s'%(self.name,self.pid_filename),level=1) + os.remove(self.pid_filename) + self.start() + self.echo('Writing to PID file %s'%self.pid_filename,level=2) + pid_file.write(str(os.getpid())) + pid_file.close() + self.echo('Starting main_loop',level=2) + while True: + self.main_loop() + sleep(self.loop_timer) + + def main_loop(self): + self.echo('Daemon running as PID: %s'%os.getpid(),level=2) + sleep(15) + + def clean_up(self): + return + + def readpid(self): + self.echo('Reading from pid file: %s'%self.pid_filename,level=2) + pf=open(self.pid_filename,'r');rv=pf.read();pf.close() + return int(rv) + + def getpid(self): + try: + rv=self.readpid() + except ValueError or FileNotFoundError: + self.echo('PID file missing or corrupt: %s'%self.pid_filename,level=2) + rv=0;match=Process().cmdline()[:2];match.append('start') + self.echo('Gleaning PID from running processes ... ',level=2) + for ps in process_iter(): + if ps.cmdline()==match and ps.username()==self.user: + rv=ps.pid + self.echo('Gleaned PID as: %s'%rv,level=2) + break + if not rv:self.echo('Gleaning PID from running processes ... ',level=2) + return int(rv) + + def simple_exit(self,sig,*frame): + exit(0) + + def stop(self): + self.echo ('Stopping %s ... '%self.name, end='', flush=True) + try: + child=Process(self.getpid()) + child.terminate() + except: + self.echo ('daemon not running, stop failed!') + try:os.remove(self.pid_filename) + except FileNotFoundError:pass + exit(1) + try: + child.wait(timeout=self.exit_timer) + except TimeoutExpired: + self.echo ('timeout waiting for clean exit, killing PID:%s ...'%child.pid,end='',flush=True) + child.kill() + try:os.remove(self.pid_filename) + except FileNotFoundError:pass + self.echo('stopped.') + exit(0) + + def alive(self,pid=None): + try: + if pid: return Process(pid).is_running() + else: return Process(self.getpid()).is_running() + except: + return False + + def status(self): + signal(SIGINT,self.simple_exit) + try: interval=argv[2] + except:interval=0 + run=True + while run: + if self.alive(): isRunning='Yes, PID:%s'%self.getpid() + else: isRunning='Not running...' + self.echo(' Running : %s'%isRunning) + sleep(int(interval)) + run=interval + + +#import your_deps + +class daemon(pydaemon): + def __init__(self,name): + super().__init__(name) + self.debug=-1 # -1:suppress all output 0:default 1:daemon prints to terminal 2:time stamped debugging logs + self.loop_timer=1 # seconds between main_loop() runs + self.exit_timer=5 # seconds clean_up() needs to finish + #initialization code goes here + + def main_loop(self): + #main code goes here + pass + + def clean_up(self): + #cleanup code goes here + pass + + #def your_function(self,arg): + #check 'your_function' doesn't conflict with any pydaemon.function names + #endef + + def status(self): + if self.alive():self.echo('%s status: Running'%self.name);exit(0) + else: self.echo('%s status: Stopped'%self.name);exit(1) + +def usage(name): + print('Usage: %s start|status|stop|test'%name) + exit(0) + +name=argv[0][argv[0].rfind('/')+1:] + +def run_main(): + try: + action=argv[1] + except IndexError: + usage(name) + if action == 'start': + if os.fork():exit(0) + main=daemon(name) + main.start() + elif action == 'test': #don't daemonise + main=daemon(name) + main.start() + elif action == 'stop': + main=daemon(name) + main.stop() + elif action == 'status': + main=daemon(name) + main.status() + else: + usage(name) + +if __name__=='__main__': + run_main() diff --git a/generic_python_daemon/generic_python_daemon.service b/generic_python_daemon/generic_python_daemon.service new file mode 100644 index 0000000..2072f9e --- /dev/null +++ b/generic_python_daemon/generic_python_daemon.service @@ -0,0 +1,10 @@ +[Unit] +Description=Generic Python Daemon + +[Service] +Type=simple +ExecStart=/usr/bin/generic_python.daemon.py start +ExecStop=/usr/bin/generic_python_daemon.py stop + +[Install] +WantedBy=multi-user.target