diff --git a/runs/remoteSupport/remoteSupport.py b/runs/remoteSupport/remoteSupport.py index 15913eae09..331b29f92d 100755 --- a/runs/remoteSupport/remoteSupport.py +++ b/runs/remoteSupport/remoteSupport.py @@ -5,12 +5,15 @@ from datetime import datetime from subprocess import Popen from pathlib import Path +import sys +from signal import signal, Signals, SIGTERM, SIGINT from time import sleep from typing import Optional import paho.mqtt.client as mqtt import platform +VERSION = "1.0.0" API_VERSION = "1" BASE_PATH = Path(__file__).resolve().parents[2] RAMDISK_PATH = BASE_PATH / "ramdisk" @@ -26,17 +29,23 @@ mqtt_broker_host = "localhost" mqtt_broker_port = 1886 -support_tunnel: Popen = None -partner_tunnel: Popen = None -cloud_tunnel: Popen = None +support_tunnel: Optional[Popen] = None +partner_tunnel: Optional[Popen] = None +cloud_tunnel: Optional[Popen] = None valid_partner_ids: list[str] = [] logging.basicConfig( filename=str(RAMDISK_PATH / "remote_support.log"), - level=logging.DEBUG, format='%(asctime)s: %(message)s' + level=logging.DEBUG, format='%(asctime)s - {%(name)s:%(lineno)s} - {%(levelname)s:%(threadName)s}: %(message)s' ) log = logging.getLogger("RemoteSupport") +def handle_terminate(signal_number: int, frame: Optional[object]): + signal_name = Signals(signal_number).name + log.info(f"{signal_name} received, shutting down gracefully...") + sys.exit(0) + + def get_serial(): """Extract serial from cpuinfo file""" with open('/proc/cpuinfo', 'r') as f: @@ -72,6 +81,26 @@ def get_lt_executable() -> Optional[Path]: return lt_path +def stop_tunnel(tunnel: Optional[Popen], tunnel_name: str) -> None: + log.debug(f"Stopping tunnel: {tunnel_name}") + if tunnel is not None: + if tunnel.poll() is None: + log.info(f"terminating {tunnel_name} ...") + tunnel.terminate() + try: + tunnel.wait(timeout=3) + except Exception as e: + log.error(f"Error terminating {tunnel_name}: {e}") + else: + # Tunnel process is already terminated, but may not have been collected yet + try: + tunnel.wait(timeout=1) + except Exception: + pass + else: + log.error(f"tunnel {tunnel_name} is not running.") + + def on_connect(client: mqtt.Client, userdata, flags: dict, rc: int): """connect to broker and subscribe to set topics""" log.info("Connected") @@ -87,19 +116,6 @@ def on_connect(client: mqtt.Client, userdata, flags: dict, rc: int): def on_message(client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): """handle incoming messages""" - def is_tunnel_closed(tunnel: Popen) -> bool: - log.debug(str(tunnel)) - is_closed = False - if tunnel is not None: - if tunnel.poll() is None: - log.error("received start tunnel message but tunnel is already running") - else: - is_closed = True - log.info("tunnel was closed by server") - else: - is_closed = True - return is_closed - global support_tunnel global partner_tunnel global cloud_tunnel @@ -110,24 +126,21 @@ def is_tunnel_closed(tunnel: Popen) -> bool: log.debug("Topic: %s, Message: %s", msg.topic, payload) if msg.topic == REMOTE_SUPPORT_TOPIC: if payload == 'stop': - if support_tunnel is None: - log.error("received stop tunnel message but tunnel is not running") - else: - log.info("stop remote support") - support_tunnel.terminate() - support_tunnel.wait(timeout=3) - support_tunnel = None + stop_tunnel(support_tunnel, "support_tunnel") + support_tunnel = None elif re.match(r'^([^;]+)(?:;([1-9][0-9]+)(?:;([a-zA-Z0-9]+))?)?$', payload): - if is_tunnel_closed(support_tunnel): - splitted = payload.split(";") - token = splitted[0] - port = splitted[1] if len(splitted) > 1 else "2223" - user = splitted[2] if len(splitted) > 2 else "getsupport" - log.info("start remote support") - support_tunnel = Popen(["sshpass", "-p", token, "ssh", "-N", "-tt", "-o", - "StrictHostKeyChecking=no", "-o", "ServerAliveInterval 60", "-R", - f"{port}:localhost:22", f"{user}@remotesupport.openwb.de"]) - log.info(f"tunnel running with pid {support_tunnel.pid}") + # Always stop existing tunnel before starting a new one + stop_tunnel(support_tunnel, "support_tunnel") + support_tunnel = None + splitted = payload.split(";") + token = splitted[0] + port = splitted[1] if len(splitted) > 1 else "2223" + user = splitted[2] if len(splitted) > 2 else "getsupport" + log.info("start remote support") + support_tunnel = Popen(["sshpass", "-p", token, "ssh", "-N", "-tt", "-o", + "StrictHostKeyChecking=no", "-o", "ServerAliveInterval 60", "-R", + f"{port}:localhost:22", f"{user}@remotesupport.openwb.de"]) + log.info(f"tunnel running with pid {support_tunnel.pid}") else: log.info("unknown message: " + payload) clear_topic = True @@ -135,72 +148,66 @@ def is_tunnel_closed(tunnel: Popen) -> bool: valid_partner_ids = json.loads(payload) elif msg.topic == REMOTE_PARTNER_TOPIC: if payload == 'stop': - if partner_tunnel is None: - log.error("received stop tunnel message but tunnel is not running") - else: - log.info("stop partner support") - partner_tunnel.terminate() - partner_tunnel.wait(timeout=3) - partner_tunnel = None + stop_tunnel(partner_tunnel, "partner_tunnel") + partner_tunnel = None elif re.match(r'^([^;]+)(?:;((?:cnode)?[0-9]+)(?:;([\wäöüÄÖÜ-]+))?)?$', payload): - if is_tunnel_closed(partner_tunnel): - splitted = payload.split(";") - if len(splitted) != 3: - log.error("invalid number of settings received!") + # Always stop existing tunnel before starting a new one + stop_tunnel(partner_tunnel, "partner_tunnel") + partner_tunnel = None + splitted = payload.split(";") + if len(splitted) != 3: + log.error("invalid number of settings received!") + else: + token = splitted[0] + port_or_node = splitted[1] + user = splitted[2] # not used in v0, partner-id in v1 + if port_or_node.isdecimal(): + # v0 + log.info("start partner support") + partner_tunnel = Popen(["sshpass", "-p", token, "ssh", "-N", "-tt", "-o", + "StrictHostKeyChecking=no", "-o", "ServerAliveInterval 60", "-R", + f"{port_or_node}:localhost:80", f"{user}@partner.openwb.de"]) + log.info(f"tunnel running with pid {partner_tunnel.pid}") else: - token = splitted[0] - port_or_node = splitted[1] - user = splitted[2] # not used in v0, partner-id in v1 - if port_or_node.isdecimal(): - # v0 - log.info("start partner support") - partner_tunnel = Popen(["sshpass", "-p", token, "ssh", "-N", "-tt", "-o", - "StrictHostKeyChecking=no", "-o", "ServerAliveInterval 60", "-R", - f"{port_or_node}:localhost:80", f"{user}@partner.openwb.de"]) - log.info(f"tunnel running with pid {partner_tunnel.pid}") + # v1 + if lt_executable is None: + log.error("start partner tunnel requested but lt executable not found!") else: - # v1 - if lt_executable is None: - log.error("start partner tunnel requested but lt executable not found!") + if user in valid_partner_ids: + log.info("start partner support v1") + if lt_executable is not None: + partner_tunnel = Popen([f"{lt_executable}", "-h", + "https://" + port_or_node + ".openwb.de/", + "-p", "80", "-s", token]) + log.info(f"tunnel running with pid {partner_tunnel.pid}") else: - if user in valid_partner_ids: - log.info("start partner support v1") - if lt_executable is not None: - partner_tunnel = Popen([f"{lt_executable}", "-h", - "https://" + port_or_node + ".openwb.de/", - "-p", "80", "-s", token]) - log.info(f"tunnel running with pid {partner_tunnel.pid}") - else: - log.error(f"invalid partner-id: {user}") + log.error(f"invalid partner-id: {user}") else: log.info("unknown message: " + payload) clear_topic = True elif msg.topic == CLOUD_TOPIC: if payload == 'stop': - if cloud_tunnel is None: - log.error("received stop cloud message but tunnel is not running") - else: - log.info("stop cloud tunnel") - cloud_tunnel.terminate() - cloud_tunnel.wait(timeout=3) - cloud_tunnel = None + stop_tunnel(cloud_tunnel, "cloud_tunnel") + cloud_tunnel = None elif re.match(r'^([^;]+)(?:;([a-zA-Z0-9]+)(?:;([a-zA-Z0-9]+))?)?$', payload): - if is_tunnel_closed(cloud_tunnel): - splitted = payload.split(";") - if len(splitted) != 3: - log.error("invalid number of settings received!") - else: - token = splitted[0] - cloud_node = splitted[1] - user = splitted[2] + # Always stop existing tunnel before starting a new one + stop_tunnel(cloud_tunnel, "cloud_tunnel") + cloud_tunnel = None + splitted = payload.split(";") + if len(splitted) != 3: + log.error("invalid number of settings received!") + else: + token = splitted[0] + cloud_node = splitted[1] + user = splitted[2] - if lt_executable is None: - log.error("start cloud tunnel requested but lt executable not found!") - else: - log.info(f"start cloud tunnel '{token[:4]}...{token[-4:]}' on '{cloud_node}'") - cloud_tunnel = Popen([f"{lt_executable}", "-h", - "https://" + cloud_node + ".openwb.de/", "-p", "80", "-s", token]) - log.info(f"cloud tunnel running with pid {cloud_tunnel.pid}") + if lt_executable is None: + log.error("start cloud tunnel requested but lt executable not found!") + else: + log.info(f"start cloud tunnel '{token[:4]}...{token[-4:]}' on '{cloud_node}'") + cloud_tunnel = Popen([f"{lt_executable}", "-h", + "https://" + cloud_node + ".openwb.de/", "-p", "80", "-s", token]) + log.info(f"cloud tunnel running with pid {cloud_tunnel.pid}") else: log.info("unknown message: " + payload) clear_topic = True @@ -209,6 +216,11 @@ def is_tunnel_closed(tunnel: Popen) -> bool: client.publish(msg.topic, "", qos=2, retain=True) +log.info("Starting remote support client") +log.debug(f"openWB remote support client v{VERSION} (API v{API_VERSION})") +log.debug("registering signal handlers") +signal(SIGTERM, handle_terminate) # Handle SIGTERM from systemctl for graceful shutdown +signal(SIGINT, handle_terminate) # Handle SIGINT from keyboard (Strg+C) for graceful shutdown lt_executable = get_lt_executable() client = mqtt.Client(f"openWB-remote-{get_serial()}-{datetime.today().timestamp()}") client.on_connect = on_connect @@ -222,7 +234,7 @@ def is_tunnel_closed(tunnel: Popen) -> bool: try: while True: sleep(1) -except (Exception, KeyboardInterrupt) as e: +except Exception as e: log.debug(e) log.debug("terminated") finally: @@ -233,4 +245,8 @@ def is_tunnel_closed(tunnel: Popen) -> bool: client.loop_stop() client.disconnect() log.debug("disconnected") + # terminate tunnels + stop_tunnel(support_tunnel, "support_tunnel") + stop_tunnel(partner_tunnel, "partner_tunnel") + stop_tunnel(cloud_tunnel, "cloud_tunnel") log.debug("exit")