Added generic_python_deamon
authorAndrew <andrew@liquid.me.uk>
Thu, 17 Jun 2021 14:53:50 +0000 (15:53 +0100)
committerAndrew <andrew@liquid.me.uk>
Thu, 17 Jun 2021 14:53:50 +0000 (15:53 +0100)
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.

generic_python_daemon/etc_init.d_genpyd [new file with mode: 0755]
generic_python_daemon/generic_python_daemon.py [new file with mode: 0755]
generic_python_daemon/generic_python_daemon.service [new file with mode: 0644]

diff --git a/generic_python_daemon/etc_init.d_genpyd b/generic_python_daemon/etc_init.d_genpyd
new file mode 100755 (executable)
index 0000000..580b0fb
--- /dev/null
@@ -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 (executable)
index 0000000..a6905d2
--- /dev/null
@@ -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 (file)
index 0000000..2072f9e
--- /dev/null
@@ -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