From a5719ae44fee881b7d3d11bbe1104c110b027594 Mon Sep 17 00:00:00 2001 From: NaruX Date: Sun, 26 Feb 2017 12:47:17 +0100 Subject: [PATCH] first checkin --- .hgignore | 0 MANIFEST.in | 3 + data/etc/default/revpipyload | 7 + data/etc/init.d/revpipyload | 128 +++++++++++++ data/etc/logrotate.d/revpipyload | 8 + data/etc/revpipyload/revpipyload.conf | 7 + data/revpipyload | 3 + revpipyload.e4p | 119 ++++++++++++ revpipyload/__init__.py | 1 + revpipyload/proginit.py | 94 +++++++++ revpipyload/revpipyload.py | 263 ++++++++++++++++++++++++++ setup.py | 56 ++++++ 12 files changed, 689 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 data/etc/default/revpipyload create mode 100755 data/etc/init.d/revpipyload create mode 100644 data/etc/logrotate.d/revpipyload create mode 100644 data/etc/revpipyload/revpipyload.conf create mode 100755 data/revpipyload create mode 100644 revpipyload.e4p create mode 100644 revpipyload/__init__.py create mode 100644 revpipyload/proginit.py create mode 100755 revpipyload/revpipyload.py create mode 100644 setup.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..e69de29 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..39fbb6a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include data * +recursive-include revpipyload * +global-exclude *.pyc diff --git a/data/etc/default/revpipyload b/data/etc/default/revpipyload new file mode 100644 index 0000000..2b9fbb1 --- /dev/null +++ b/data/etc/default/revpipyload @@ -0,0 +1,7 @@ +# RevPiPyLoader +# +# Verbose logging add a -v or -vv +DAEMON_ARGS="-d" + +# Codepage of files +export LANG=C.UTF-8 \ No newline at end of file diff --git a/data/etc/init.d/revpipyload b/data/etc/init.d/revpipyload new file mode 100755 index 0000000..a11104a --- /dev/null +++ b/data/etc/init.d/revpipyload @@ -0,0 +1,128 @@ +#! /bin/bash +### BEGIN INIT INFO +# Provides: revpipyload +# Required-Start: $remote_fs $syslog $piControl +# Required-Stop: $remote_fs $syslog $piControl +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start RevPiPyLoad to execute python plc program +# Description: This file starts the RevPiPyLoad on system +# boot. The Loader starts your python plc program and +# check whether it is running. +### END INIT INFO + +# Author: Akira Naru Takizawa + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="RevPiPyLoad to run plc program" +NAME=revpipyload +DAEMON=/usr/local/share/revpipyload/revpipyload.py +DAEMON_ARGS="-d" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME.py + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME.py + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + reload) + log_daemon_msg "Reloading $DESC" "$NAME" + do_reload + log_end_msg $? + ;; + restart) + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload}" >&2 + exit 3 + ;; +esac diff --git a/data/etc/logrotate.d/revpipyload b/data/etc/logrotate.d/revpipyload new file mode 100644 index 0000000..6a0343a --- /dev/null +++ b/data/etc/logrotate.d/revpipyload @@ -0,0 +1,8 @@ +/var/log/revpipyload +{ + rotate 6 + monthly + compress + missingok + notifempty +} diff --git a/data/etc/revpipyload/revpipyload.conf b/data/etc/revpipyload/revpipyload.conf new file mode 100644 index 0000000..f76710a --- /dev/null +++ b/data/etc/revpipyload/revpipyload.conf @@ -0,0 +1,7 @@ +[DEFAULT] +autoreload=1 +autostart=1 +plcprogram=test.py +xmlrpc=1 +xmlrpcport=55123 +zeroonexit=1 diff --git a/data/revpipyload b/data/revpipyload new file mode 100755 index 0000000..f3af942 --- /dev/null +++ b/data/revpipyload @@ -0,0 +1,3 @@ +#!/bin/sh + +exec "/usr/local/share/revpipyload/revpipyload.py" "$@" diff --git a/revpipyload.e4p b/revpipyload.e4p new file mode 100644 index 0000000..55bb4c6 --- /dev/null +++ b/revpipyload.e4p @@ -0,0 +1,119 @@ + + + + + + + en_US + 89ddb4e70b339f832ee277085202b38acc6a125c + Python3 + Console + Dieser Loader wird über das Init-System geladen und führt das angegebene Pythonprogramm aus. Es ist für den RevolutionPi gedacht um automatisch das SPS-Programm zu starten. + 0.1.0 + Sven Sager + akira@narux.de + + + revpipyload/__init__.py + revpipyload/proginit.py + setup.py + revpipyload/revpipyload.py + + + + + + + data + MANIFEST.in + + + None + + + + + + + + + + + + + Pep8Checker + + + + + DocstringType + + + pep257 + + + ExcludeFiles + + + + + + ExcludeMessages + + + E123,E226,E24 + + + FixCodes + + + + + + FixIssues + + + False + + + HangClosing + + + False + + + IncludeMessages + + + + + + MaxLineLength + + + 80 + + + NoFixCodes + + + E501 + + + RepeatMessages + + + True + + + ShowIgnored + + + False + + + + + + + diff --git a/revpipyload/__init__.py b/revpipyload/__init__.py new file mode 100644 index 0000000..74d18d5 --- /dev/null +++ b/revpipyload/__init__.py @@ -0,0 +1 @@ +"""just init file.""" diff --git a/revpipyload/proginit.py b/revpipyload/proginit.py new file mode 100644 index 0000000..34adaa3 --- /dev/null +++ b/revpipyload/proginit.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""Main functions of our program.""" +import logging +import sys +from argparse import ArgumentParser +from configparser import ConfigParser +from os import fork as osfork +from os.path import exists as ospexists + + +class ProgInit(): + + """Programmfunktionen fuer Parameter und Logger.""" + + def __del__(self): + """Clean up program.""" + # Logging beenden + logging.shutdown() + + def __init__(self): + """Initialize general program functions.""" + + # Command arguments + parser = ArgumentParser( + description="RevolutionPi Python3 Loader" + ) + parser.add_argument( + "-d", "--daemon", action="store_true", dest="daemon", + help="Run program as a daemon in background" + ) + parser.add_argument( + "-c", "--conffile", dest="conffile", + default="/etc/revpipyload/revpipyload.conf", + help="Application configuration file" + ) + parser.add_argument( + "-f", "--logfile", dest="logfile", + help="Save log entries to this file" + ) + parser.add_argument( + "-v", "--verbose", action="count", dest="verbose", + help="Switch on verbose logging" + ) + self.pargs = parser.parse_args() + + # Prüfen ob als Daemon ausgeführt werden soll + self.pidfile = "/var/run/revpipyload.pid" + self.pid = 0 + if self.pargs.daemon: + # Prüfen ob daemon schon läuft + if ospexists(self.pidfile): + raise SystemError( + "program already running as daemon. check {}".format( + self.pidfile + ) + ) + else: + self.pid = osfork() + if self.pid > 0: + with open(self.pidfile, "w") as f: + f.write(str(self.pid)) + exit(0) + + # Ausgaben umhängen in Logfile + sys.stdout = open("/var/log/revpipyload", "a") + sys.stderr = sys.stdout + + # Initialize configparser globalconfig + self.globalconffile = self.pargs.conffile + self.globalconfig = ConfigParser() + self.globalconfig.read(self.pargs.conffile) + + # Program logger + self.logger = logging.getLogger() + logformat = logging.Formatter( + "{asctime} [{levelname:8}] {message}", + datefmt="%Y-%m-%d %H:%M:%S", style="{" + ) + lhandler = logging.StreamHandler(sys.stdout) + lhandler.setFormatter(logformat) + self.logger.addHandler(lhandler) + if self.pargs.logfile is not None: + lhandler = logging.FileHandler(filename=self.pargs.logfile) + lhandler.setFormatter(logformat) + self.logger.addHandler(lhandler) + + # Loglevel auswerten + if self.pargs.verbose is None: + loglevel = logging.WARNING + elif self.pargs.verbose == 1: + loglevel = logging.INFO + elif self.pargs.verbose > 1: + loglevel = logging.DEBUG + self.logger.setLevel(loglevel) diff --git a/revpipyload/revpipyload.py b/revpipyload/revpipyload.py new file mode 100755 index 0000000..32c25f2 --- /dev/null +++ b/revpipyload/revpipyload.py @@ -0,0 +1,263 @@ +#!/usr/bin/python3 +# +# (c) Sven Sager, License: GPLv3 +# +# -*- coding: utf-8 -*- +import proginit +import shlex +import signal +import subprocess +from concurrent import futures +from threading import Thread, Event +from time import sleep +from xmlrpc.server import SimpleXMLRPCServer + + +class RevPiPlc(Thread): + + def __init__(self, logger, lst_proc): + super().__init__() + self.autoreload = False + self._evt_exit = Event() + self.exitcode = 0 + self._lst_proc = lst_proc + self._logger = logger + self._procplc = None + self.zeroonexit = False + + def run(self): + # Prozess starten + self._logger.info("start plc program") + self._procplc = subprocess.Popen(self._lst_proc) + + while not self._evt_exit.is_set(): + + # Auswerten + self.exitcode = self._procplc.poll() + + if self.exitcode is not None: + + if self.exitcode > 0: + self._logger.error( + "plc program chrashed - exitcode: {}".format( + self.exitcode + ) + ) + if self.zeroonexit: + f = open("/dev/piControl0", "w+b", 0) + f.write(bytes(4096)) + self._logger.warning("set piControl0 to ZERO") + else: + self._logger.info("plc program did a clean exit") + + if self.autoreload: + # Prozess neu starten + self._procplc = subprocess.Popen(self._lst_proc) + if self.exitcode == 0: + self._logger.warning( + "restart plc program after clean exit" + ) + else: + self._logger.warning("restart plc program after crash") + else: + break + + self._evt_exit.wait(1) + + # Prozess beenden + count = 0 + self._logger.info("term plc program") + self._procplc.terminate() + while self._procplc.poll() is None and count < 10: + count += 1 + self._logger.debug( + "wait term plc program {} seconds".format(count * 0.5) + ) + sleep(0.5) + if self._procplc.poll() is None: + self._logger.warning("can not term plc program") + self._procplc.kill() + self._logger.warning("killed plc program") + + self.exitcode = self._procplc.poll() + + def stop(self): + self.evt_exit.set() + + +class RevPiPyLoad(proginit.ProgInit): + + def __init__(self): + super().__init__() + self._exit = True + self.evt_loadconfig = Event() + + self.autoreload = None + self.plc = None + self.plcprog = None + self.tpe = None + self.xmlrpc = None + self.xsrv = None + + # Load config + self._loadconfig() + + # Signal events + signal.signal(signal.SIGINT, self._sigexit) + signal.signal(signal.SIGTERM, self._sigexit) + signal.signal(signal.SIGHUP, self._sigloadconfig) + + def _loadconfig(self): + """Load configuration file and setup modul.""" + self.evt_loadconfig.clear() + pauseproc = False + if not self._exit: + self.logger.info( + "shutdown python plc program while getting new config" + ) + self.stop() + pauseproc = True + + # Konfigurationsdatei laden + self.logger.info( + "loading config file: {}".format(self.globalconffile) + ) + self.globalconfig.read(self.globalconffile) + + # Konfiguration verarbeiten + self.autoreload = int(self.globalconfig["DEFAULT"].get("autoreload", 1)) + self.autostart = int(self.globalconfig["DEFAULT"].get("autostart", 0)) + self.plcprog = self.globalconfig["DEFAULT"].get("plcprogram", None) + self.xmlrpc = int(self.globalconfig["DEFAULT"].get("xmlrpc", 1)) + self.zeroonexit = int(self.globalconfig["DEFAULT"].get("zeroonexit", 1)) + + # PLC Thread konfigurieren + self.logger.debug("create PLC watcher") + self.plc = RevPiPlc( + self.logger, shlex.split("/usr/bin/env python3 " + self.plcprog) + ) + self.plc.autoreload = self.autoreload + self.plc.zeroonexit = self.zeroonexit + self.logger.debug("created PLC watcher") + + # XMLRPC-Server Instantiieren und konfigurieren + if self.xmlrpc: + self.logger.debug("create xmlrpc server") + self.xsrv = SimpleXMLRPCServer( + ( + "", + int(self.globalconfig["DEFAULT"].get("xmlrpcport", 55123)) + ), + logRequests=False, + allow_none=True + ) + self.xsrv.register_introspection_functions() + + self.xsrv.register_function(self.xml_plcexitcode, "plcexitcode") + self.xsrv.register_function(self.xml_plcrestart, "plcrestart") + self.xsrv.register_function(self.xml_plcrunning, "plcrunning") + self.xsrv.register_function(self.xml_plcstart, "plcstart") + self.xsrv.register_function(self.xml_plcstop, "plcstop") + self.xsrv.register_function(self.xml_reload, "reload") + self.logger.debug("created xmlrpc server") + + if pauseproc: + self.logger.info( + "start python plc program after getting new config" + ) + self.start() + + def _sigexit(self, signum, frame): + """Signal handler to clean an exit program.""" + self.logger.info("got exit signal") + self.stop() + + def _sigloadconfig(self, signum, frame): + self.logger.info("got reload config signal") + self.evt_loadconfig.set() + + def start(self): + """Start python program and watching it.""" + self.logger.info("starting revpipyload") + self._exit = False + + if self.xmlrpc: + self.logger.info("start xmlrpc-server") + self.tpe = futures.ThreadPoolExecutor(max_workers=1) + self.tpe.submit(self.xsrv.serve_forever) + + if self.autostart: + self.logger.info("starting plc program {}".format(self.plcprog)) + self.plc.start() + + while not self._exit \ + and not self.evt_loadconfig.is_set() \ + and self.plc.is_alive(): + self.evt_loadconfig.wait(1) + + if not self._exit: + self.logger.info("exit python plc program to reload config") + self._loadconfig() + + def stop(self): + """Stop python program.""" + self.logger.info("stopping revpipyload") + self._exit = True + + self.logger.info("stopping plc program {}".format(self.plcprog)) + self.plc.stop() + self.plc.join() + + if self.xmlrpc: + self.logger.info("shutting down xmlrpc-server") + self.xsrv.shutdown() + self.tpe.shutdown() + self.xsrv.server_close() + + def xml_plcexitcode(self): + self.logger.debug("xmlrpc get plcexitcode") + return -1 if self.plc.is_alive() else self.plc.exitcode + + def xml_plcrestart(self): + self.logger.debug("xmlrpc get plcrestart") + self.plc.stop() + self.plc.join() + exitcode = self.plc.exitcode + self.plc = RevPiPlc( + self.logger, shlex.split("/usr/bin/env python3 " + self.plcprog) + ) + self.plc.autoreload = self.autoreload + self.plc.zeroonexit = self.zeroonexit + self.plc.start() + return (exitcode, self.plc.exitcode) + + def xml_plcrunning(self): + self.logger.debug("xmlrpc get plcrunning") + return self.plc.is_alive() + + def xml_plcstart(self): + if self.plc.is_alive(): + return -1 + else: + self.plc = RevPiPlc( + self.logger, shlex.split("/usr/bin/env python3 " + self.plcprog) + ) + self.plc.autoreload = self.autoreload + self.plc.zeroonexit = self.zeroonexit + self.plc.start() + return self.plc.exitcode + + def xml_plcstop(self): + self.logger.debug("xmlrpc get plcstop") + self.plc.stop() + self.plc.join() + return self.plc.exitcode + + def xml_reload(self): + self.logger.info("xmlrpc reload configuration") + self.evt_loadconfig.set() + + +if __name__ == "__main__": + root = RevPiPyLoad() + root.start() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9045d54 --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +#! /usr/bin/env python3 +# +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +"""Setupscript fuer RevPiPyLoad.""" +import distutils.command.install_egg_info +from distutils.core import setup +from glob import glob + + +class MyEggInfo(distutils.command.install_egg_info.install_egg_info): + + u"""Disable egg_info installation, seems pointless for a non-library.""" + + def run(self): + u"""just pass egg_info.""" + pass + + +setup( + author="Sven Sager", + author_email="akira@narux.de", + url="https://revpimodio.org", + maintainer="Sven Sager", + maintainer_email="akira@revpimodio.org", + + license="LGPLv3", + name="revpipyload", + version="0.1.0", + + scripts=["data/revpipyload"], + + data_files=[ + ("/etc/default", ["data/etc/default/revpipyload"]), + ("/etc/init.d", ["data/etc/init.d/revpipyload"]), + ("/etc/logrotate.d", ["data/etc/logrotate.d/revpipyload"]), + ("/etc/revpipyload", ["data/etc/revpipyload/revpipyload.conf"]), + ("share/revpipyload", glob("revpipyload/*.*")), + ], + + description="PLC Loader für Python-Projekte auf den RevolutionPi", + long_description="" + "Dieses Programm startet beim Systemstart ein angegebenes Python PLC\n" + "Programm. Es überwacht das Programm und startet es im Fehlerfall neu.\n" + "Bei Abstruz kann das gesamte /dev/piControl0 auf 0x00 gesettz werden.\n" + "Außerdem stellt es einen XML-RPC Server bereit, über den die Software\n" + "auf den RevPi geladen werden kann. Das Prozessabbild kann über ein Tool\n" + "zur Laufzeit überwacht werden.", + + classifiers=[ + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: POSIX :: Linux", + ], + cmdclass={"install_egg_info": MyEggInfo}, +)