first checkin

This commit is contained in:
2017-02-26 12:47:17 +01:00
commit a5719ae44f
12 changed files with 689 additions and 0 deletions

0
.hgignore Normal file
View File

3
MANIFEST.in Normal file
View File

@@ -0,0 +1,3 @@
recursive-include data *
recursive-include revpipyload *
global-exclude *.pyc

View File

@@ -0,0 +1,7 @@
# RevPiPyLoader
#
# Verbose logging add a -v or -vv
DAEMON_ARGS="-d"
# Codepage of files
export LANG=C.UTF-8

128
data/etc/init.d/revpipyload Executable file
View File

@@ -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 <akira@narux.de>
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

View File

@@ -0,0 +1,8 @@
/var/log/revpipyload
{
rotate 6
monthly
compress
missingok
notifempty
}

View File

@@ -0,0 +1,7 @@
[DEFAULT]
autoreload=1
autostart=1
plcprogram=test.py
xmlrpc=1
xmlrpcport=55123
zeroonexit=1

3
data/revpipyload Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
exec "/usr/local/share/revpipyload/revpipyload.py" "$@"

119
revpipyload.e4p Normal file
View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Project SYSTEM "Project-5.1.dtd">
<!-- eric project file for project revpipyload -->
<!-- Saved: 2017-02-16, 07:47:17 -->
<!-- Copyright (C) 2017 Sven Sager, akira@narux.de -->
<Project version="5.1">
<Language>en_US</Language>
<Hash>89ddb4e70b339f832ee277085202b38acc6a125c</Hash>
<ProgLanguage mixed="0">Python3</ProgLanguage>
<ProjectType>Console</ProjectType>
<Description>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.</Description>
<Version>0.1.0</Version>
<Author>Sven Sager</Author>
<Email>akira@narux.de</Email>
<Eol index="-1"/>
<Sources>
<Source>revpipyload/__init__.py</Source>
<Source>revpipyload/proginit.py</Source>
<Source>setup.py</Source>
<Source>revpipyload/revpipyload.py</Source>
</Sources>
<Forms/>
<Translations/>
<Resources/>
<Interfaces/>
<Others>
<Other>data</Other>
<Other>MANIFEST.in</Other>
</Others>
<Vcs>
<VcsType>None</VcsType>
</Vcs>
<FiletypeAssociations>
<FiletypeAssociation pattern="*.idl" type="INTERFACES"/>
<FiletypeAssociation pattern="*.py" type="SOURCES"/>
<FiletypeAssociation pattern="*.py3" type="SOURCES"/>
<FiletypeAssociation pattern="*.pyw" type="SOURCES"/>
<FiletypeAssociation pattern="*.pyw3" type="SOURCES"/>
</FiletypeAssociations>
<Checkers>
<CheckersParams>
<dict>
<key>
<string>Pep8Checker</string>
</key>
<value>
<dict>
<key>
<string>DocstringType</string>
</key>
<value>
<string>pep257</string>
</value>
<key>
<string>ExcludeFiles</string>
</key>
<value>
<string></string>
</value>
<key>
<string>ExcludeMessages</string>
</key>
<value>
<string>E123,E226,E24</string>
</value>
<key>
<string>FixCodes</string>
</key>
<value>
<string></string>
</value>
<key>
<string>FixIssues</string>
</key>
<value>
<bool>False</bool>
</value>
<key>
<string>HangClosing</string>
</key>
<value>
<bool>False</bool>
</value>
<key>
<string>IncludeMessages</string>
</key>
<value>
<string></string>
</value>
<key>
<string>MaxLineLength</string>
</key>
<value>
<int>80</int>
</value>
<key>
<string>NoFixCodes</string>
</key>
<value>
<string>E501</string>
</value>
<key>
<string>RepeatMessages</string>
</key>
<value>
<bool>True</bool>
</value>
<key>
<string>ShowIgnored</string>
</key>
<value>
<bool>False</bool>
</value>
</dict>
</value>
</dict>
</CheckersParams>
</Checkers>
</Project>

1
revpipyload/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""just init file."""

94
revpipyload/proginit.py Normal file
View File

@@ -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)

263
revpipyload/revpipyload.py Executable file
View File

@@ -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()

56
setup.py Normal file
View File

@@ -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},
)