Merge tag '0.12.0' into pkg/debian

Release version 0.12.0
This commit is contained in:
2026-08-07 08:30:46 +02:00
6 changed files with 205 additions and 36 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.1" __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()))
+56 -10
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):
+33 -9
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,10 +103,18 @@ 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")
xmlrpc_unix_socket = True
xmlrpcacl.acl = ""
save_xmlrpcacls()
elif input("\nDo you want to allow native TCP 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", "*")
xmlrpc_unix_socket = False
cmd = input("Reset the ACL file to allow all private networks? (y/N) ").lower() cmd = input("Reset the ACL file to allow all private networks? (y/N) ").lower()
if cmd == "y": if cmd == "y":
@@ -135,12 +147,13 @@ def main() -> int:
stderr.write("Wrong format (0.0.0.0)\n") stderr.write("Wrong format (0.0.0.0)\n")
else: else:
cmd = input("Do you want to allow connections from localhost ONLY? (y/N) ").lower() cmd = input("Do you want to allow TCP connections from localhost ONLY? (y/N) ").lower()
if cmd == "y": if cmd == "y":
conf.set("XMLRPC", "xmlrpc", "1") conf.set("XMLRPC", "xmlrpc", "1")
conf.set("XMLRPC", "bindip", "127.0.0.1") conf.set("XMLRPC", "bindip", "127.0.0.1")
xmlrpc_unix_socket = False
cmd = input("Reset the ACL file to allow localhost connections only? (y/N) ").lower() cmd = input("Reset the ACL file to allow localhost TCP connections only? (y/N) ").lower()
if cmd == "y": if cmd == "y":
xmlrpcacl.acl = "127.*.*.*,4 " xmlrpcacl.acl = "127.*.*.*,4 "
save_xmlrpcacls() save_xmlrpcacls()
@@ -153,6 +166,7 @@ def main() -> int:
if cmd == "y": if cmd == "y":
conf.set("XMLRPC", "xmlrpc", "0") conf.set("XMLRPC", "xmlrpc", "0")
conf.set("XMLRPC", "bindip", "127.0.0.1") conf.set("XMLRPC", "bindip", "127.0.0.1")
xmlrpc_unix_socket = False
xmlrpcacl.acl = "" xmlrpcacl.acl = ""
save_xmlrpcacls() save_xmlrpcacls()
else: else:
@@ -169,15 +183,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
+94
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,94 @@ 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.warning("RevPi Commander 0.12.0 or greater is required to connect via Unix Domain Socket")
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)