Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 106 additions & 90 deletions runs/remoteSupport/remoteSupport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -110,97 +126,88 @@ 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
elif msg.topic == REMOTE_PARTNER_IDS_TOPIC:
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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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")