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
+6 -5
View File
@@ -7,7 +7,7 @@ plcworkdir_set_uid = 1
plcprogram = program.py
plcprogram_stop_timeout = 5
plcprogram_watchdog = 0
plcarguments =
plcarguments =
plcuid = 1000
plcgid = 1000
pythonversion = 3
@@ -27,7 +27,8 @@ watchdog = 1
[XMLRPC]
xmlrpc = 1
aclfile = /etc/revpipyload/aclxmlrpc.conf
bindip = 127.0.0.1
bindip = socket
unixgroup = picontrol
[MQTT]
mqtt = 0
@@ -38,6 +39,6 @@ write_outputs = 0
broker_address = localhost
port = 1883
tls_set = 0
username =
password =
client_id =
username =
password =
client_id =
+1 -1
View File
@@ -4,4 +4,4 @@ __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv2"
__package__ = "revpipyload"
__version__ = "0.11.0rc2"
__version__ = "0.12.0"
+5 -1
View File
@@ -9,6 +9,7 @@ import os
import sys
from argparse import ArgumentParser
from configparser import ConfigParser
from os import getpid
from . import __version__
@@ -55,7 +56,7 @@ def configure():
)
parser.add_argument(
"-c", "--conffile", dest="conffile",
default="revpipyload.conf",
default="/etc/revpipyload/revpipyload.conf",
help="Application configuration file"
)
parser.add_argument(
@@ -186,3 +187,6 @@ def configure():
else:
loglevel = logging.DEBUG
logger.setLevel(loglevel)
# Log PID for development purposes
logger.debug("Running with PID {}".format(getpid()))
+100 -34
View File
@@ -31,6 +31,7 @@ __license__ = "GPLv2"
import gzip
import os
import signal
import socket
import tarfile
import zipfile
from configparser import ConfigParser
@@ -51,7 +52,7 @@ from . import proginit
from .helper import get_revpiled_address, pi_control_reset, refullmatch
from .shared.ipaclmanager import IpAclManager
from .watchdogs import ResetDriverWatchdog
from .xrpcserver import SaveXMLRPCServer
from .xrpcserver import SaveXMLRPCServer, UnixStreamXMLRPCServer
min_revpimodio = "2.5.0"
@@ -69,7 +70,8 @@ class RevPiPyLoad:
proginit.logger.debug("enter RevPiPyLoad.__init__()")
# Klassenattribute
self._exit = True
self._evt_exit = Event()
self._evt_exit.set()
self.evt_loadconfig = Event()
self.globalconfig = ConfigParser()
proginit.conf = self.globalconfig
@@ -209,7 +211,9 @@ class RevPiPyLoad:
proginit.logger.info(
"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()
proginit.conf = self.globalconfig
@@ -313,12 +317,23 @@ class RevPiPyLoad:
# Bind IP lesen und anpassen
self.xmlrpcbindip = \
self.globalconfig.get("XMLRPC", "bindip", fallback="127.0.0.1")
if self.xmlrpcbindip == "*":
self.xmlrpcbindip = ""
elif self.xmlrpcbindip == "":
self.xmlrpcbindip = "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 == "*":
self.xmlrpcbindip = ""
elif self.xmlrpcbindip == "":
self.xmlrpcbindip = "127.0.0.1"
self.xmlrpcport = self.globalconfig.getint("XMLRPC", "port", fallback=55123)
self.xmlrpcunixgroup = self.globalconfig.get("XMLRPC", "unixgroup", fallback="picontrol")
# Workdirectory wechseln
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.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")
self.th_plcmqtt.start()
@@ -352,7 +367,7 @@ class RevPiPyLoad:
self.stop_plcprogram()
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")
self.plc.start()
@@ -372,7 +387,7 @@ class RevPiPyLoad:
self.stop_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")
self.th_plcserver.start()
@@ -387,12 +402,35 @@ class RevPiPyLoad:
self.xsrv = None
else:
proginit.logger.debug("create xmlrpc server")
self.xsrv = SaveXMLRPCServer(
(self.xmlrpcbindip, self.xmlrpcport),
logRequests=False,
allow_none=True,
ipacl=self.xmlrpcacl
)
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.xmlrpcbindip, self.xmlrpcport),
logRequests=False,
allow_none=True,
ipacl=self.xmlrpcacl
)
self.xsrv.register_introspection_functions()
self.xsrv.register_multicall_functions()
@@ -485,7 +523,7 @@ class RevPiPyLoad:
proginit.logger.debug("created xmlrpc server")
# Neustart bei reload
if not self._exit:
if not self._evt_exit.is_set():
proginit.logger.info("bind xmlrpc-server")
self.xsrv.server_bind()
self.xsrv.server_activate()
@@ -762,7 +800,7 @@ class RevPiPyLoad:
proginit.logger.debug("enter RevPiPyLoad.start()")
proginit.logger.info("starting revpipyload")
self._exit = False
self._evt_exit.clear()
if self.xmlrpc and self.xsrv is not None:
proginit.logger.info("bind xmlrpc-server")
@@ -786,7 +824,7 @@ class RevPiPyLoad:
pictory_reset_driver.register_call(self.xml_psstop)
# mainloop
while not self._exit:
while not self._evt_exit.is_set():
# Neue Konfiguration laden
if self.evt_loadconfig.is_set():
proginit.logger.info("got reqeust to reload config")
@@ -887,7 +925,7 @@ class RevPiPyLoad:
def stop(self):
"""Stop revpipyload."""
proginit.logger.debug("enter RevPiPyLoad.stop()")
self._exit = True
self._evt_exit.set()
proginit.logger.debug("leave RevPiPyLoad.stop()")
def stop_plcmqtt(self):
@@ -937,6 +975,14 @@ class RevPiPyLoad:
proginit.logger.info("close xmlrpc-server")
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()")
def xml_getconfig(self):
@@ -1064,11 +1110,19 @@ class RevPiPyLoad:
:param file_name: File with full path relative to work directory
:return: True on success
"""
file_name = os.path.join(self.plcworkdir, file_name)
if os.path.exists(file_name):
os.remove(file_name)
dirname = os.path.dirname(file_name)
if dirname != self.plcworkdir:
plcworkdir = os.path.realpath(self.plcworkdir)
file_path = os.path.realpath(os.path.join(plcworkdir, file_name))
if os.path.commonpath([plcworkdir, file_path]) != 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 to remove directory, which will work if it is empty
os.rmdir(dirname)
@@ -1104,9 +1158,17 @@ class RevPiPyLoad:
:param file_name: File with full path relative to work directory
:return: Binary object in gzip format
"""
file_name = os.path.join(self.plcworkdir, file_name)
if os.path.exists(file_name):
with open(file_name, "rb") as fh:
plcworkdir = os.path.realpath(self.plcworkdir)
file_path = os.path.realpath(os.path.join(plcworkdir, file_name))
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()))
return xmldata
return Binary()
@@ -1169,7 +1231,7 @@ class RevPiPyLoad:
else:
return -1
def xml_plcupload(self, filedata, filename):
def xml_plcupload(self, filedata: Binary, filename:str):
"""Empfaengt Dateien fuer das PLC Programm einzeln.
@param filedata GZIP Binary data der Datei
@@ -1185,14 +1247,18 @@ class RevPiPyLoad:
# Windowszeichen prüfen
filename = filename.replace("\\", "/")
# Build absolut path, join will return last element, if absolute
dirname = os.path.join(self.plcworkdir, os.path.dirname(filename))
if os.path.abspath(dirname).find(self.plcworkdir) != 0:
plcworkdir = os.path.realpath(self.plcworkdir)
file_path = os.path.realpath(os.path.join(plcworkdir, filename))
if os.path.commonpath([plcworkdir, file_path]) != plcworkdir:
proginit.logger.warning(
"file path is not in plc working directory"
)
return False
dirname = os.path.dirname(file_path)
set_uid = self.plcuid 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
try:
with open(filename, "wb") as fh:
with open(file_path, "wb") as fh:
fh.write(gzip.decompress(filedata.data))
os.chown(filename, set_uid, set_gid)
os.chown(file_path, set_uid, set_gid)
return True
except Exception:
return False
+27 -7
View File
@@ -18,6 +18,7 @@ def main() -> int:
"""Secure installation script to use on Revolution Pi."""
from configparser import ConfigParser
from os import R_OK, access, getuid, system
from shutil import which
from re import match
from sys import stderr, stdout
@@ -42,7 +43,9 @@ def main() -> int:
# Prepare variables
xmlrpcacl = IpAclManager(minlevel=0, maxlevel=4)
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
stdout.write("""
@@ -58,7 +61,8 @@ def main() -> int:
aclxmlrpc=aclxmlrpc,
xmlrpc="" if xmlrpc else "NOT ",
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!"
))
@@ -81,7 +85,7 @@ def main() -> int:
stdout.write("{0:15} - Level: {1:2}".format(ip, level))
counter += 1
stdout.write("\n")
else:
elif not xmlrpc_unix_socket:
stderr.write(
"\nWARNING: NO IP addresses defined in ACL!\n You will "
"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")
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":
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", "bindip", "*")
@@ -169,15 +179,25 @@ def main() -> int:
stdout.write("\n\nWe did no changes!\n")
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:
cmd = input("\nDo you want to apply the new settings now? (Y/n) ").lower()
if cmd in ("", "y"):
system("/etc/init.d/revpipyload reload")
system(cmd_reload)
else:
stderr.write(
"\nYou have to activate the new settings for RevPiPyLoad!\n"
" sudo /etc/init.d/revpipyload reload\n"
" {0}\n".format(hint_reload)
)
except KeyboardInterrupt:
pass
+92
View File
@@ -4,6 +4,9 @@ __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "GPLv2"
import grp
import os
import socket
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from . import proginit
@@ -102,3 +105,92 @@ class SaveXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
)
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)