Merge tag 'unstable/0.12.0_rc1' into pkg/debian_rc

This commit is contained in:
2026-08-06 11:59:32 +02:00
6 changed files with 231 additions and 48 deletions
+2 -1
View File
@@ -27,7 +27,8 @@ watchdog = 1
[XMLRPC] [XMLRPC]
xmlrpc = 1 xmlrpc = 1
aclfile = /etc/revpipyload/aclxmlrpc.conf aclfile = /etc/revpipyload/aclxmlrpc.conf
bindip = 127.0.0.1 bindip = socket
unixgroup = picontrol
[MQTT] [MQTT]
mqtt = 0 mqtt = 0
+1 -1
View File
@@ -4,4 +4,4 @@ __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv2" __license__ = "GPLv2"
__package__ = "revpipyload" __package__ = "revpipyload"
__version__ = "0.11.0rc2" __version__ = "0.12.0"
+5 -1
View File
@@ -9,6 +9,7 @@ import os
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from configparser import ConfigParser from configparser import ConfigParser
from os import getpid
from . import __version__ from . import __version__
@@ -55,7 +56,7 @@ def configure():
) )
parser.add_argument( parser.add_argument(
"-c", "--conffile", dest="conffile", "-c", "--conffile", dest="conffile",
default="revpipyload.conf", default="/etc/revpipyload/revpipyload.conf",
help="Application configuration file" help="Application configuration file"
) )
parser.add_argument( parser.add_argument(
@@ -186,3 +187,6 @@ def configure():
else: else:
loglevel = logging.DEBUG loglevel = logging.DEBUG
logger.setLevel(loglevel) logger.setLevel(loglevel)
# Log PID for development purposes
logger.debug("Running with PID {}".format(getpid()))
+90 -24
View File
@@ -31,6 +31,7 @@ __license__ = "GPLv2"
import gzip import gzip
import os import os
import signal import signal
import socket
import tarfile import tarfile
import zipfile import zipfile
from configparser import ConfigParser from configparser import ConfigParser
@@ -51,7 +52,7 @@ from . import proginit
from .helper import get_revpiled_address, pi_control_reset, refullmatch from .helper import get_revpiled_address, pi_control_reset, refullmatch
from .shared.ipaclmanager import IpAclManager from .shared.ipaclmanager import IpAclManager
from .watchdogs import ResetDriverWatchdog from .watchdogs import ResetDriverWatchdog
from .xrpcserver import SaveXMLRPCServer from .xrpcserver import SaveXMLRPCServer, UnixStreamXMLRPCServer
min_revpimodio = "2.5.0" min_revpimodio = "2.5.0"
@@ -69,7 +70,8 @@ class RevPiPyLoad:
proginit.logger.debug("enter RevPiPyLoad.__init__()") proginit.logger.debug("enter RevPiPyLoad.__init__()")
# Klassenattribute # Klassenattribute
self._exit = True self._evt_exit = Event()
self._evt_exit.set()
self.evt_loadconfig = Event() self.evt_loadconfig = Event()
self.globalconfig = ConfigParser() self.globalconfig = ConfigParser()
proginit.conf = self.globalconfig proginit.conf = self.globalconfig
@@ -209,7 +211,9 @@ class RevPiPyLoad:
proginit.logger.info( proginit.logger.info(
"loading config file: {0}".format(proginit.globalconffile) "loading config file: {0}".format(proginit.globalconffile)
) )
self.globalconfig.read(proginit.globalconffile) if not self.globalconfig.read(proginit.globalconffile):
raise RuntimeError("can not access config file '{0}'".format(proginit.globalconffile))
self.__translate_config() self.__translate_config()
proginit.conf = self.globalconfig proginit.conf = self.globalconfig
@@ -313,12 +317,23 @@ class RevPiPyLoad:
# Bind IP lesen und anpassen # Bind IP lesen und anpassen
self.xmlrpcbindip = \ self.xmlrpcbindip = \
self.globalconfig.get("XMLRPC", "bindip", fallback="127.0.0.1") self.globalconfig.get("XMLRPC", "bindip", fallback="127.0.0.1")
if self.xmlrpcbindip.lower() == "socket":
# Unix Domain Socket mit festem Pfad
self.xmlrpcbindip = "/run/revpipyload/xmlrpc.socket"
self.xmlrpcisunix = True
elif self.xmlrpcbindip.startswith("/"):
# Unix Domain Socket
self.xmlrpcisunix = True
else:
self.xmlrpcisunix = False
if self.xmlrpcbindip == "*": if self.xmlrpcbindip == "*":
self.xmlrpcbindip = "" self.xmlrpcbindip = ""
elif self.xmlrpcbindip == "": elif self.xmlrpcbindip == "":
self.xmlrpcbindip = "127.0.0.1" self.xmlrpcbindip = "127.0.0.1"
self.xmlrpcport = self.globalconfig.getint("XMLRPC", "port", fallback=55123) self.xmlrpcport = self.globalconfig.getint("XMLRPC", "port", fallback=55123)
self.xmlrpcunixgroup = self.globalconfig.get("XMLRPC", "unixgroup", fallback="picontrol")
# Workdirectory wechseln # Workdirectory wechseln
if not os.access(self.plcworkdir, os.R_OK | os.W_OK | os.X_OK): if not os.access(self.plcworkdir, os.R_OK | os.W_OK | os.X_OK):
@@ -343,7 +358,7 @@ class RevPiPyLoad:
self.stop_plcmqtt() self.stop_plcmqtt()
self.th_plcmqtt = self._plcmqtt() self.th_plcmqtt = self._plcmqtt()
if not self._exit and self.th_plcmqtt is not None: if not self._evt_exit.is_set() and self.th_plcmqtt is not None:
proginit.logger.info("restart mqtt publisher after reload") proginit.logger.info("restart mqtt publisher after reload")
self.th_plcmqtt.start() self.th_plcmqtt.start()
@@ -352,7 +367,7 @@ class RevPiPyLoad:
self.stop_plcprogram() self.stop_plcprogram()
self.plc = self._plcthread() self.plc = self._plcthread()
if not self._exit and self.plc is not None and self.autostart: if not self._evt_exit.is_set() and self.plc is not None and self.autostart:
proginit.logger.info("restart plc program after reload") proginit.logger.info("restart plc program after reload")
self.plc.start() self.plc.start()
@@ -372,7 +387,7 @@ class RevPiPyLoad:
self.stop_plcserver() self.stop_plcserver()
self.th_plcserver = self._plcserver() self.th_plcserver = self._plcserver()
if not self._exit and self.th_plcserver is not None: if not self._evt_exit.is_set() and self.th_plcserver is not None:
proginit.logger.info("restart plc server after reload") proginit.logger.info("restart plc server after reload")
self.th_plcserver.start() self.th_plcserver.start()
@@ -387,12 +402,35 @@ class RevPiPyLoad:
self.xsrv = None self.xsrv = None
else: else:
proginit.logger.debug("create xmlrpc server") proginit.logger.debug("create xmlrpc server")
if self.xmlrpcisunix:
# Unix Domain Socket Server
proginit.logger.info(
"starting xmlrpc unix server on {0}".format(self.xmlrpcbindip)
)
# Vorherige Socket-Datei löschen
try:
os.unlink(self.xmlrpcbindip)
except FileNotFoundError:
pass
self.xsrv = UnixStreamXMLRPCServer(
self.xmlrpcbindip,
logRequests=False,
allow_none=True,
unixgroup=self.xmlrpcunixgroup
)
else:
# Standard IP Server
self.xsrv = SaveXMLRPCServer( self.xsrv = SaveXMLRPCServer(
(self.xmlrpcbindip, self.xmlrpcport), (self.xmlrpcbindip, self.xmlrpcport),
logRequests=False, logRequests=False,
allow_none=True, allow_none=True,
ipacl=self.xmlrpcacl ipacl=self.xmlrpcacl
) )
self.xsrv.register_introspection_functions() self.xsrv.register_introspection_functions()
self.xsrv.register_multicall_functions() self.xsrv.register_multicall_functions()
@@ -485,7 +523,7 @@ class RevPiPyLoad:
proginit.logger.debug("created xmlrpc server") proginit.logger.debug("created xmlrpc server")
# Neustart bei reload # Neustart bei reload
if not self._exit: if not self._evt_exit.is_set():
proginit.logger.info("bind xmlrpc-server") proginit.logger.info("bind xmlrpc-server")
self.xsrv.server_bind() self.xsrv.server_bind()
self.xsrv.server_activate() self.xsrv.server_activate()
@@ -762,7 +800,7 @@ class RevPiPyLoad:
proginit.logger.debug("enter RevPiPyLoad.start()") proginit.logger.debug("enter RevPiPyLoad.start()")
proginit.logger.info("starting revpipyload") proginit.logger.info("starting revpipyload")
self._exit = False self._evt_exit.clear()
if self.xmlrpc and self.xsrv is not None: if self.xmlrpc and self.xsrv is not None:
proginit.logger.info("bind xmlrpc-server") proginit.logger.info("bind xmlrpc-server")
@@ -786,7 +824,7 @@ class RevPiPyLoad:
pictory_reset_driver.register_call(self.xml_psstop) pictory_reset_driver.register_call(self.xml_psstop)
# mainloop # mainloop
while not self._exit: while not self._evt_exit.is_set():
# Neue Konfiguration laden # Neue Konfiguration laden
if self.evt_loadconfig.is_set(): if self.evt_loadconfig.is_set():
proginit.logger.info("got reqeust to reload config") proginit.logger.info("got reqeust to reload config")
@@ -887,7 +925,7 @@ class RevPiPyLoad:
def stop(self): def stop(self):
"""Stop revpipyload.""" """Stop revpipyload."""
proginit.logger.debug("enter RevPiPyLoad.stop()") proginit.logger.debug("enter RevPiPyLoad.stop()")
self._exit = True self._evt_exit.set()
proginit.logger.debug("leave RevPiPyLoad.stop()") proginit.logger.debug("leave RevPiPyLoad.stop()")
def stop_plcmqtt(self): def stop_plcmqtt(self):
@@ -937,6 +975,14 @@ class RevPiPyLoad:
proginit.logger.info("close xmlrpc-server") proginit.logger.info("close xmlrpc-server")
self.xsrv.server_close() self.xsrv.server_close()
# Unix Socket aufräumen
if hasattr(self.xsrv, "address_family") and \
self.xsrv.address_family == socket.AF_UNIX:
try:
os.unlink(self.xsrv.server_address)
except (FileNotFoundError, AttributeError):
pass
proginit.logger.debug("leave RevPiPyLoad.stop_xmlrpcserver()") proginit.logger.debug("leave RevPiPyLoad.stop_xmlrpcserver()")
def xml_getconfig(self): def xml_getconfig(self):
@@ -1064,11 +1110,19 @@ class RevPiPyLoad:
:param file_name: File with full path relative to work directory :param file_name: File with full path relative to work directory
:return: True on success :return: True on success
""" """
file_name = os.path.join(self.plcworkdir, file_name) plcworkdir = os.path.realpath(self.plcworkdir)
if os.path.exists(file_name): file_path = os.path.realpath(os.path.join(plcworkdir, file_name))
os.remove(file_name)
dirname = os.path.dirname(file_name) if os.path.commonpath([plcworkdir, file_path]) != plcworkdir:
if dirname != self.plcworkdir: proginit.logger.warning(
"file path is not in plc working directory"
)
return False
if os.path.exists(file_path):
os.remove(file_path)
dirname = os.path.dirname(file_path)
if dirname != plcworkdir:
try: try:
# Try to remove directory, which will work if it is empty # Try to remove directory, which will work if it is empty
os.rmdir(dirname) os.rmdir(dirname)
@@ -1104,9 +1158,17 @@ class RevPiPyLoad:
:param file_name: File with full path relative to work directory :param file_name: File with full path relative to work directory
:return: Binary object in gzip format :return: Binary object in gzip format
""" """
file_name = os.path.join(self.plcworkdir, file_name) plcworkdir = os.path.realpath(self.plcworkdir)
if os.path.exists(file_name): file_path = os.path.realpath(os.path.join(plcworkdir, file_name))
with open(file_name, "rb") as fh:
if os.path.commonpath([plcworkdir, file_path]) != plcworkdir:
proginit.logger.warning(
"file path is not in plc working directory"
)
return Binary()
if os.path.exists(file_path):
with open(file_path, "rb") as fh:
xmldata = Binary(gzip.compress(fh.read())) xmldata = Binary(gzip.compress(fh.read()))
return xmldata return xmldata
return Binary() return Binary()
@@ -1169,7 +1231,7 @@ class RevPiPyLoad:
else: else:
return -1 return -1
def xml_plcupload(self, filedata, filename): def xml_plcupload(self, filedata: Binary, filename:str):
"""Empfaengt Dateien fuer das PLC Programm einzeln. """Empfaengt Dateien fuer das PLC Programm einzeln.
@param filedata GZIP Binary data der Datei @param filedata GZIP Binary data der Datei
@@ -1185,14 +1247,18 @@ class RevPiPyLoad:
# Windowszeichen prüfen # Windowszeichen prüfen
filename = filename.replace("\\", "/") filename = filename.replace("\\", "/")
# Build absolut path, join will return last element, if absolute
dirname = os.path.join(self.plcworkdir, os.path.dirname(filename)) plcworkdir = os.path.realpath(self.plcworkdir)
if os.path.abspath(dirname).find(self.plcworkdir) != 0: file_path = os.path.realpath(os.path.join(plcworkdir, filename))
if os.path.commonpath([plcworkdir, file_path]) != plcworkdir:
proginit.logger.warning( proginit.logger.warning(
"file path is not in plc working directory" "file path is not in plc working directory"
) )
return False return False
dirname = os.path.dirname(file_path)
set_uid = self.plcuid if self.plcworkdir_set_uid else 0 set_uid = self.plcuid if self.plcworkdir_set_uid else 0
set_gid = self.plcgid if self.plcworkdir_set_uid else 0 set_gid = self.plcgid if self.plcworkdir_set_uid else 0
@@ -1210,9 +1276,9 @@ class RevPiPyLoad:
# Datei erzeugen # Datei erzeugen
try: try:
with open(filename, "wb") as fh: with open(file_path, "wb") as fh:
fh.write(gzip.decompress(filedata.data)) fh.write(gzip.decompress(filedata.data))
os.chown(filename, set_uid, set_gid) os.chown(file_path, set_uid, set_gid)
return True return True
except Exception: except Exception:
return False return False
+27 -7
View File
@@ -18,6 +18,7 @@ def main() -> int:
"""Secure installation script to use on Revolution Pi.""" """Secure installation script to use on Revolution Pi."""
from configparser import ConfigParser from configparser import ConfigParser
from os import R_OK, access, getuid, system from os import R_OK, access, getuid, system
from shutil import which
from re import match from re import match
from sys import stderr, stdout from sys import stderr, stdout
@@ -42,7 +43,9 @@ def main() -> int:
# Prepare variables # Prepare variables
xmlrpcacl = IpAclManager(minlevel=0, maxlevel=4) xmlrpcacl = IpAclManager(minlevel=0, maxlevel=4)
xmlrpcacl.loadaclfile(aclxmlrpc) xmlrpcacl.loadaclfile(aclxmlrpc)
xmlrpc_only_localhost = xmlrpcbindip.find("127.") == 0 or xmlrpcbindip == "" xmlrpc_unix_socket = xmlrpcbindip == "socket" or xmlrpcbindip.startswith("/")
xmlrpc_only_localhost = xmlrpcbindip.find("127.") == 0 or xmlrpcbindip == "" or \
xmlrpc_unix_socket
# ----- Print summary of actual configuration # ----- Print summary of actual configuration
stdout.write(""" stdout.write("""
@@ -58,7 +61,8 @@ def main() -> int:
aclxmlrpc=aclxmlrpc, aclxmlrpc=aclxmlrpc,
xmlrpc="" if xmlrpc else "NOT ", xmlrpc="" if xmlrpc else "NOT ",
source="" if not xmlrpc source="" if not xmlrpc
else " from this computer only (localhost)." if xmlrpc_only_localhost else " from this computer / SSH only (unix socket)." if xmlrpcbindip == "socket" or xmlrpcbindip.startswith("/")
else " from this computer / SSH only (localhost)." if xmlrpc_only_localhost
else " from ACL listed remote computers!" else " from ACL listed remote computers!"
)) ))
@@ -81,7 +85,7 @@ def main() -> int:
stdout.write("{0:15} - Level: {1:2}".format(ip, level)) stdout.write("{0:15} - Level: {1:2}".format(ip, level))
counter += 1 counter += 1
stdout.write("\n") stdout.write("\n")
else: elif not xmlrpc_unix_socket:
stderr.write( stderr.write(
"\nWARNING: NO IP addresses defined in ACL!\n You will " "\nWARNING: NO IP addresses defined in ACL!\n You will "
"not be able to connect with RevPiPyControl at this moment!\n" "not be able to connect with RevPiPyControl at this moment!\n"
@@ -99,8 +103,14 @@ def main() -> int:
stderr.write("\nYou need root permissions to change values (sudo).\n") stderr.write("\nYou need root permissions to change values (sudo).\n")
return 4 return 4
cmd = input("\nDo you want to allow connections from remote hosts? (y/N) ").lower() cmd = input("\nDo you want to allow connections ONLY via SSH/Unix-Socket? (y/N) ").lower()
if cmd == "y": if cmd == "y":
conf.set("XMLRPC", "xmlrpc", "1")
conf.set("XMLRPC", "bindip", "socket")
xmlrpcacl.acl = ""
save_xmlrpcacls()
elif input("\nDo you want to allow connections from remote hosts? (y/N) ").lower() == "y":
conf.set("XMLRPC", "xmlrpc", "1") conf.set("XMLRPC", "xmlrpc", "1")
conf.set("XMLRPC", "bindip", "*") conf.set("XMLRPC", "bindip", "*")
@@ -169,15 +179,25 @@ def main() -> int:
stdout.write("\n\nWe did no changes!\n") stdout.write("\n\nWe did no changes!\n")
return 2 return 2
if system("/etc/init.d/revpipyload status > /dev/null") == 0: # Check for service manager
if which("systemctl"):
cmd_status = "systemctl status revpipyload > /dev/null"
cmd_reload = "systemctl reload revpipyload"
hint_reload = "sudo systemctl reload revpipyload"
else:
cmd_status = "/etc/init.d/revpipyload status > /dev/null"
cmd_reload = "/etc/init.d/revpipyload reload"
hint_reload = "sudo /etc/init.d/revpipyload reload"
if system(cmd_status) == 0:
try: try:
cmd = input("\nDo you want to apply the new settings now? (Y/n) ").lower() cmd = input("\nDo you want to apply the new settings now? (Y/n) ").lower()
if cmd in ("", "y"): if cmd in ("", "y"):
system("/etc/init.d/revpipyload reload") system(cmd_reload)
else: else:
stderr.write( stderr.write(
"\nYou have to activate the new settings for RevPiPyLoad!\n" "\nYou have to activate the new settings for RevPiPyLoad!\n"
" sudo /etc/init.d/revpipyload reload\n" " {0}\n".format(hint_reload)
) )
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
+92
View File
@@ -4,6 +4,9 @@ __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv2" __license__ = "GPLv2"
import grp
import os
import socket
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from . import proginit from . import proginit
@@ -102,3 +105,92 @@ class SaveXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
) )
return False return False
class UnixStreamXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
"""XML-RPC Request-Handler fuer Unix Domain Sockets."""
timeout = 2.0
def setup(self):
"""Initialisiert die Verbindung ohne TCP-spezifische Socket-Optionen."""
self.connection = self.request
if self.timeout is not None:
self.connection.settimeout(self.timeout)
self.rfile = self.connection.makefile("rb", self.rbufsize)
self.wfile = self.connection.makefile("wb", self.wbufsize)
def address_string(self):
"""Liefert einen Namen fuer Logging bei Unix Domain Sockets."""
return "localhost"
class UnixStreamXMLRPCServer(SimpleXMLRPCServer):
"""XML-RPC Server fuer Unix Domain Sockets."""
address_family = socket.AF_UNIX
def __init__(
self, addr, logRequests=True, allow_none=False, unixgroup="picontrol"):
"""Init UnixStreamXMLRPCServer class."""
proginit.logger.debug("enter UnixStreamXMLRPCServer.__init__()")
self.timeout = 0.5
self.unixgroup = unixgroup
# Create subdirectories for Unix Domain Socket
socket_dir = os.path.dirname(addr)
if socket_dir:
os.makedirs(socket_dir, exist_ok=True)
super().__init__(
addr=addr,
requestHandler=UnixStreamXMLRPCRequestHandler,
logRequests=logRequests,
allow_none=allow_none,
encoding="utf-8",
bind_and_activate=False,
)
proginit.logger.debug("leave UnixStreamXMLRPCServer.__init__()")
def server_bind(self):
"""Ueberschreibt server_bind um Berechtigungen zu setzen."""
super().server_bind()
# Gruppe setzen
try:
gid = grp.getgrnam(self.unixgroup).gr_gid
# -1 will leave the owner unchanged
os.chown(self.server_address, -1, gid)
except (KeyError, PermissionError):
proginit.logger.warning(
"can not set group of socket {0} to '{1}'"
"".format(self.server_address, self.unixgroup)
)
# Berechtigungen fuer restliche Benutzer entziehen
try:
os.chmod(self.server_address, 0o660)
except PermissionError:
proginit.logger.warning(
"can not set permissions of socket {0}"
"".format(self.server_address)
)
def _dispatch(self, method, params):
"""Prueft ACL Level fuer angeforderte Methode.
@param method Angeforderte Methode
@param params Argumente fuer Methode
@return Dispatched data
"""
# ACL on a socket is always max level
if method == "xmlmodus":
params = (4,)
return super()._dispatch(method, params)
def register_function(self, acl_level, function, name=None):
"""Override register_function to ignore acl_level for unix sockets."""
return super().register_function(function, name)