diff --git a/.idea/dictionaries/akira.xml b/.idea/dictionaries/akira.xml
index 743ad92..1c702c7 100644
--- a/.idea/dictionaries/akira.xml
+++ b/.idea/dictionaries/akira.xml
@@ -2,6 +2,7 @@
ctory
+ popen
\ No newline at end of file
diff --git a/revpipyload/helper.py b/revpipyload/helper.py
index 915f546..1251295 100644
--- a/revpipyload/helper.py
+++ b/revpipyload/helper.py
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
"""Helperfunktionen fuer das gesamte RevPiPyLoad-System."""
__author__ = "Sven Sager"
-__copyright__ = "Copyright (C) 2018 Sven Sager"
+__copyright__ = "Copyright (C) 2020 Sven Sager"
__license__ = "GPLv3"
+
import os
-import proginit
+from json import load, loads
from re import match as rematch
from subprocess import Popen, PIPE
+import proginit
+
def _setuprt(pid, evt_exit):
"""Konfiguriert Programm fuer den RT-Scheduler.
@@ -117,6 +120,32 @@ def _zeroprocimg():
)
+def get_revpiled_address(configrsc_bytes: bytes) -> int:
+ """
+ Find byte address of revpiled output.
+
+ :return: Address or -1 on error
+ """
+ try:
+ rsc = loads(configrsc_bytes.decode()) # type: dict
+ except Exception:
+ return -1
+
+ byte_address = -1
+ for device in rsc.get("Devices", ()): # type: dict
+ if device.get("type", "") == "BASE":
+ try:
+ byte_address = device["offset"] + int(device["out"]["0"][3])
+ proginit.logger.debug(
+ "found revpi_led_address on {0} byte".format(byte_address)
+ )
+ except Exception:
+ pass
+ break
+
+ return byte_address
+
+
def refullmatch(regex, string):
"""re.fullmatch wegen alter python version aus wheezy nachgebaut.
diff --git a/revpipyload/plcsystem.py b/revpipyload/plcsystem.py
index 8c023d6..9d66eab 100644
--- a/revpipyload/plcsystem.py
+++ b/revpipyload/plcsystem.py
@@ -1,18 +1,20 @@
# -*- coding: utf-8 -*-
"""Modul fuer die Verwaltung der PLC Funktionen."""
__author__ = "Sven Sager"
-__copyright__ = "Copyright (C) 2018 Sven Sager"
+__copyright__ = "Copyright (C) 2020 Sven Sager"
__license__ = "GPLv3"
import os
-import proginit
import shlex
import subprocess
-from logsystem import PipeLogwriter
-from helper import _setuprt, _zeroprocimg
from sys import stdout as sysstdout
from threading import Event, Thread
from time import sleep, asctime
+import proginit
+from helper import _setuprt, _zeroprocimg
+from logsystem import PipeLogwriter
+from watchdogs import SoftwareWatchdog
+
class RevPiPlc(Thread):
@@ -45,6 +47,16 @@ class RevPiPlc(Thread):
self.zeroonerror = False
self.zeroonexit = False
+ # Software watchdog
+ self.softdog = SoftwareWatchdog(0, 0)
+
+ def __exec_rtlevel(self):
+ """RealTime Scheduler nutzen nach 5 Sekunden Programmvorlauf."""
+ if self.rtlevel > 0 \
+ and not self._evt_exit.wait(5) \
+ and self._procplc.poll() is None:
+ _setuprt(self._procplc.pid, self._evt_exit)
+
def __get_autoreloaddelay(self):
"""Getter fuer autoreloaddelay.
@return Delayzeit in Sekunden """
@@ -103,6 +115,11 @@ class RevPiPlc(Thread):
stderr=subprocess.STDOUT
)
proginit.logger.debug("leave RevPiPlc._spopen()")
+
+ # Pass process to watchdog
+ self.softdog.kill_process = sp
+ self.softdog.reset()
+
return sp
def newlogfile(self):
@@ -138,12 +155,7 @@ class RevPiPlc(Thread):
# Prozess erstellen
proginit.logger.info("start plc program {0}".format(self._program))
self._procplc = self._spopen(lst_proc)
-
- # RealTime Scheduler nutzen nach 5 Sekunden Programmvorlauf
- if self.rtlevel > 0 \
- and not self._evt_exit.wait(5) \
- and self._procplc.poll() is None:
- _setuprt(self._procplc.pid, self._evt_exit)
+ self.__exec_rtlevel()
# Überwachung starten
while not self._evt_exit.is_set():
@@ -152,22 +164,11 @@ class RevPiPlc(Thread):
self.exitcode = self._procplc.poll()
if self.exitcode is not None:
+ # Do nothing, if delay for restart is running
if self._delaycounter == self._autoreloaddelay:
- if self.exitcode > 0:
- # PLC Python Programm abgestürzt
- proginit.logger.error(
- "plc program crashed - exitcode: {0}".format(
- self.exitcode
- )
- )
- if self.zeroonerror:
- _zeroprocimg()
- proginit.logger.warning(
- "set piControl0 to ZERO after "
- "PLC program error"
- )
+ self.softdog.stop()
- else:
+ if self.exitcode == 0:
# PLC Python Programm sauber beendet
proginit.logger.info("plc program did a clean exit")
if self.zeroonexit:
@@ -176,6 +177,18 @@ class RevPiPlc(Thread):
"set piControl0 to ZERO after "
"PLC program returns clean exitcode"
)
+ else:
+ # PLC Python Programm abgestürzt
+ proginit.logger.error(
+ "plc program crashed - exitcode: {0}"
+ "".format(self.exitcode)
+ )
+ if self.zeroonerror:
+ _zeroprocimg()
+ proginit.logger.warning(
+ "set piControl0 to ZERO after "
+ "PLC program error"
+ )
if not self._evt_exit.is_set() and self.autoreload:
self._delaycounter -= 1
@@ -192,6 +205,7 @@ class RevPiPlc(Thread):
proginit.logger.warning(
"restart plc program after crash"
)
+ self.__exec_rtlevel()
else:
break
@@ -211,6 +225,7 @@ class RevPiPlc(Thread):
proginit.logger.info("stop revpiplc thread")
self._evt_exit.set()
+ self.softdog.stop()
# Prüfen ob es einen subprocess gibt
if self._procplc is None:
diff --git a/revpipyload/revpipyload.py b/revpipyload/revpipyload.py
index 20f50ad..c3459a3 100755
--- a/revpipyload/revpipyload.py
+++ b/revpipyload/revpipyload.py
@@ -26,7 +26,7 @@ begrenzt werden!
"""
__author__ = "Sven Sager"
-__copyright__ = "Copyright (C) 2018 Sven Sager"
+__copyright__ = "Copyright (C) 2020 Sven Sager"
__license__ = "GPLv3"
__version__ = "0.8.5"
@@ -48,7 +48,7 @@ import logsystem
import picontrolserver
import plcsystem
import proginit
-from helper import refullmatch
+from helper import refullmatch, get_revpiled_address
from shared.ipaclmanager import IpAclManager
from watchdogs import ResetDriverWatchdog
from xrpcserver import SaveXMLRPCServer
@@ -81,6 +81,7 @@ class RevPiPyLoad():
self.pictorymtime = 0
self.replaceiosmtime = 0
self.replaceiofail = False
+ self.revpi_led_address = -1
# Berechtigungsmanger
if proginit.pargs.developermode:
@@ -219,8 +220,8 @@ class RevPiPyLoad():
"plcworkdir", ".")
self.plcprogram = self.globalconfig["DEFAULT"].get(
"plcprogram", "none.py")
- self.plcprogram_watchdog = self.globalconfig["DEFAULT"].getboolean(
- "plcprogram_watchdog", False)
+ self.plcprogram_watchdog = self.globalconfig["DEFAULT"].getint(
+ "plcprogram_watchdog", 0)
self.plcarguments = self.globalconfig["DEFAULT"].get(
"plcarguments", "")
self.plcworkdir_set_uid = self.globalconfig["DEFAULT"].getboolean(
@@ -374,6 +375,7 @@ class RevPiPyLoad():
)
self.plc.autoreload = self.autoreload
self.plc.autoreloaddelay = self.autoreloaddelay
+ self.plc.softdog.timeout = self.plcprogram_watchdog
self.plc.zeroonerror = self.zeroonerror
self.plc.zeroonexit = self.zeroonexit
@@ -547,7 +549,6 @@ class RevPiPyLoad():
"""Konfiguriert den PLC-Thread fuer die Ausfuehrung.
@return PLC-Thread Object or None"""
proginit.logger.debug("enter RevPiPyLoad._plcthread()")
- th_plc = None
# Prüfen ob Programm existiert
if not os.path.exists(os.path.join(self.plcworkdir, self.plcprogram)):
@@ -556,6 +557,16 @@ class RevPiPyLoad():
))
return None
+ # Check software watchdog
+ if self.revpi_led_address < 0 < self.plcprogram_watchdog:
+ proginit.logger.error(
+ "can not start plc program, because watchdog is activated "
+ "but no address was found in piCtory configuration"
+ )
+ return None
+
+ th_plc = None
+
proginit.logger.debug("create PLC program watcher")
th_plc = plcsystem.RevPiPlc(
os.path.join(self.plcworkdir, self.plcprogram),
@@ -567,6 +578,9 @@ class RevPiPyLoad():
th_plc.gid = self.plcgid
th_plc.uid = self.plcuid
th_plc.rtlevel = self.rtlevel
+ th_plc.softdog.address = \
+ 0 if self.revpi_led_address < 0 else self.revpi_led_address
+ th_plc.softdog.timeout = self.plcprogram_watchdog
th_plc.zeroonerror = self.zeroonerror
th_plc.zeroonexit = self.zeroonexit
@@ -628,7 +642,23 @@ class RevPiPyLoad():
# TODO: Nur "Devices" list vergleich da HASH immer neu wegen timestamp
with open(proginit.pargs.configrsc, "rb") as fh:
- file_hash = md5(fh.read()).digest()
+ rsc_buff = fh.read()
+
+ # Check change of RevPiLED address
+ self.revpi_led_address = get_revpiled_address(rsc_buff)
+ if self.plc is not None and self.plc.is_alive():
+ if self.revpi_led_address >= 0:
+ self.plc.softdog.address = self.revpi_led_address
+ elif self.plcprogram_watchdog > 0:
+ # Stop plc program, if watchdog is needed but not found
+ proginit.logger.error(
+ "stop plc program, because watchdog is activated but "
+ "no address was found in piCtory configuration"
+ )
+ self.plc.stop()
+ self.plc.join()
+
+ file_hash = md5(rsc_buff).digest()
if picontrolserver.HASH_PICT == file_hash:
return False
picontrolserver.HASH_PICT = file_hash
@@ -1151,7 +1181,7 @@ class RevPiPyLoad():
"autoreloaddelay": "[0-9]+",
"autostart": "[01]",
"plcprogram": ".+",
- "plcprogram_watchdog": "[01]",
+ "plcprogram_watchdog": "[0-9]+",
"plcarguments": ".*",
"plcworkdir_set_uid": "[01]",
# "plcuid": "[0-9]{,5}",
diff --git a/revpipyload/watchdogs.py b/revpipyload/watchdogs.py
index 5b729a6..06f6601 100644
--- a/revpipyload/watchdogs.py
+++ b/revpipyload/watchdogs.py
@@ -6,11 +6,164 @@ __license__ = "GPLv3"
import os
from fcntl import ioctl
-from threading import Thread
+from random import random
+from struct import pack, unpack
+from subprocess import Popen
+from threading import Thread, Event
+from time import time
import proginit as pi
+class SoftwareWatchdog:
+
+ def __init__(self, address: int, timeout: int, kill_process=None):
+ """
+ Software watchdog thread, which must be recreate if triggered.
+
+ :param address: Byte address of RevPiLED byte
+ :param timeout: Timeout to trigger watchdog on no change of bit
+ :param kill_process: Process to kill on trigger
+ """
+ self.__th = Thread()
+ self._exit = Event()
+ self._ioctl_bytes = b''
+ self._process = None
+ self._timeout = 0.0
+ self.triggered = False
+
+ # Process and check the values
+ self.address = address
+ self.kill_process = kill_process
+
+ # The timeout property will start/stop the thread
+ self.timeout = timeout
+
+ def __th_run(self):
+ """Thread function for watchdog."""
+ pi.logger.debug("enter SoftwareWatchdog.__th_run()")
+
+ # Startup delay to let the python program start and trigger
+ # todo: Is this fix value okay?
+ if self._exit.wait(2.0):
+ pi.logger.debug("leave SoftwareWatchdog.__th_run()")
+ return
+
+ fd = os.open(pi.pargs.procimg, os.O_RDONLY)
+ mrk = self._ioctl_bytes
+ tmr = time()
+
+ # Random wait value 0.0-0.1 to become async to process image
+ while not self._exit.wait(random() / 10):
+ try:
+ # Get SoftWatchdog bit
+ bit_7 = ioctl(fd, 19215, self._ioctl_bytes)
+ except Exception:
+ pass
+ else:
+ if bit_7 != mrk:
+ # Toggling detected, wait the rest of time to free cpu
+ self._exit.wait(self._timeout - (time() - tmr))
+ mrk = bit_7
+ tmr = time()
+ continue
+
+ if time() - tmr >= self._timeout:
+ pi.logger.debug("software watchdog timeout reached")
+ self.triggered = True
+ if self._process is not None:
+ self._process.kill()
+ pi.logger.warning(
+ "process killed by software watchdog"
+ )
+ break
+
+ os.close(fd)
+
+ pi.logger.debug("leave SoftwareWatchdog.__th_run()")
+
+ def reset(self):
+ """Reset watchdog functions after triggered or stopped."""
+ pi.logger.debug("enter SoftwareWatchdog.reset()")
+ self._exit.clear()
+ self.triggered = False
+
+ # The timeout property will start / stop the thread
+ self.timeout = int(self.timeout)
+
+ pi.logger.debug("leave SoftwareWatchdog.reset()")
+
+ def stop(self):
+ """Shut down watchdog task and wait for exit."""
+ pi.logger.debug("enter SoftwareWatchdog.stop()")
+ self._exit.set()
+ if self.__th.is_alive():
+ self.__th.join()
+ pi.logger.debug("leave SoftwareWatchdog.stop()")
+
+ @property
+ def address(self) -> int:
+ """Byte address of RevPiLED byte."""
+ return unpack(" None:
+ """Byte address of RevPiLED byte."""
+ if not isinstance(value, int):
+ raise TypeError("address must be ")
+ if not 0 <= value < 4096:
+ raise ValueError("address must be 0 - 4095")
+
+ # Use Bit 7 of RevPiLED byte (wd of Connect device)
+ self._ioctl_bytes = pack(" Popen:
+ return self._process
+
+ @kill_process.setter
+ def kill_process(self, value: Popen) -> None:
+ if not (value is None or isinstance(value, Popen)):
+ raise TypeError("kill_process must be ")
+ self._process = value
+
+ @property
+ def timeout(self) -> int:
+ """Timeout to trigger watchdog on no change of bit."""
+ return int(self._timeout)
+
+ @timeout.setter
+ def timeout(self, value: int):
+ """
+ Timeout to trigger watchdog on no change of bit.
+
+ Value in seconds, 0 will stop watchdog monitoring.
+ """
+ if not isinstance(value, int):
+ raise TypeError("timeout must be ")
+ if value < 0:
+ raise ValueError(
+ "timeout value must be 0 to disable or a positive number"
+ )
+
+ if value == 0:
+ # A value of 0 will stop the watchdog thread
+ self.stop()
+ self._timeout = 0.0
+ else:
+ self._timeout = float(value)
+ if not (self.triggered or
+ self._exit.is_set() or self.__th.is_alive()):
+ self.__th = Thread(target=self.__th_run)
+ self.__th.start()
+ pi.logger.debug(
+ "set software watchdog timeout to {0} seconds"
+ "".format(value)
+ )
+
+
class ResetDriverWatchdog(Thread):
"""Watchdog to catch a piCtory reset_driver action."""