#!/usr/bin/env python3 ############################################################################################################## # # Copyright (c) 2022 Sebastian Obele / obele.eu # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # This software uses the following software-parts: # Reticulum, LXMF, NomadNet / Copyright (c) 2016-2022 Mark Qvist / unsigned.io / MIT License # ############################################################################################################## ############################################################################################################## # Include #### System #### import sys import os import time import argparse #### JSON #### import json import pickle #### String #### import string #### Other #### import random import secrets #### Process #### import signal import threading #### Reticulum, LXMF #### # Install: pip3 install rns lxmf # Source: https://markqvist.github.io import RNS import LXMF import RNS.vendor.umsgpack as umsgpack ############################################################################################################## # Globals #### Global Variables - Configuration #### NAME = "LXMF Ping" DESCRIPTION = "Periodically sends pings/messages and evaluates the status" VERSION = "0.0.1 (2022-10-21)" COPYRIGHT = "(c) 2022 Sebastian Obele / obele.eu" PATH = os.path.expanduser("~") + "/." + os.path.splitext(os.path.basename(__file__))[0] PATH_RNS = None #### Global Variables - System (Not changeable) #### DATA = None RNS_CONNECTION = None LXMF_CONNECTION = None ############################################################################################################## # LXMF Class class lxmf_connection: message_received_callback = None message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file self.identity = identity self.destination_name = destination_name self.destination_type = destination_type self.aspect_filter = self.destination_name + "." + self.destination_type self.display_name = display_name self.send_delay = int(send_delay) if desired_method == "propagated" or desired_method == "PROPAGATED": self.desired_method_direct = False else: self.desired_method_direct = True self.propagation_node = propagation_node self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup self.announce_startup_delay = int(announce_startup_delay) self.announce_periodic = announce_periodic self.announce_periodic_interval = int(announce_periodic_interval) self.sync_startup = sync_startup self.sync_startup_delay = int(sync_startup_delay) self.sync_limit = int(sync_limit) self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) log("LXMF - Storage path: " + self.storage_path, LOG_INFO) if self.identity: log("LXMF - Using existing Primary Identity %s" % (str(self.identity))) else: if not self.identity_file: self.identity_file = "identity" self.identity_path = self.storage_path + "/" + self.identity_file if os.path.isfile(self.identity_path): try: self.identity = RNS.Identity.from_file(self.identity_path) if self.identity != None: log("LXMF - Loaded Primary Identity %s from %s" % (str(self.identity), self.identity_path)) else: log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) except Exception as e: log("LXMF - Could not load the Primary Identity from "+self.identity_path, LOG_ERROR) log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) else: try: log("LXMF - No Primary Identity file found, creating new...") self.identity = RNS.Identity() self.identity.to_file(self.identity_path) log("LXMF - Created new Primary Identity %s" % (str(self.identity))) except Exception as e: log("LXMF - Could not create and save a new Primary Identity", LOG_ERROR) log("LXMF - The contained exception was: %s" % (str(e)), LOG_ERROR) self.message_router = LXMF.LXMRouter(identity=self.identity, storagepath=self.storage_path) self.destination = self.message_router.register_delivery_identity(self.identity, display_name=self.display_name) self.message_router.register_delivery_callback(self.process_lxmf_message_propagated) if self.display_name == "": self.display_name = RNS.prettyhexrep(self.destination_hash()) self.destination.set_default_app_data(self.display_name.encode("utf-8")) self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) RNS.Identity.remember(packet_hash=None, destination_hash=self.destination.hash, public_key=self.identity.get_public_key(), app_data=None) log("LXMF - Identity: " + str(self.identity), LOG_INFO) log("LXMF - Destination: " + str(self.destination), LOG_INFO) log("LXMF - Hash: " + RNS.prettyhexrep(self.destination_hash()), LOG_INFO) self.destination.set_link_established_callback(self.client_connected) self.autoselect_propagation_node() if self.announce_startup or self.announce_periodic: self.announce(True) if self.sync_startup or self.sync_periodic: self.sync(True) def register_announce_callback(self, handler_function): self.announce_callback = handler_function(self.aspect_filter) RNS.Transport.register_announce_handler(self.announce_callback) def register_message_received_callback(self, handler_function): self.message_received_callback = handler_function def register_message_notification_callback(self, handler_function): self.message_notification_callback = handler_function def register_message_notification_success_callback(self, handler_function): self.message_notification_success_callback = handler_function def register_message_notification_failed_callback(self, handler_function): self.message_notification_failed_callback = handler_function def destination_hash(self): return self.destination.hash def destination_hash_str(self): return RNS.hexrep(self.destination.hash, False) def destination_check(self, destination): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) return False try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) return False return True def destination_correct(self, destination): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return "" try: destination_bytes = bytes.fromhex(destination) return destination except Exception as e: return "" return "" def send(self, destination, content="", title=None, fields=None, timestamp=None, app_data=""): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] if len(destination) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Destination length is invalid", LOG_ERROR) return try: destination = bytes.fromhex(destination) except Exception as e: log("LXMF - Destination is invalid", LOG_ERROR) return destination_identity = RNS.Identity.recall(destination) destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) def send_message(self, destination, source, content="", title=None, fields=None, timestamp=None, app_data=""): if self.desired_method_direct: desired_method = LXMF.LXMessage.DIRECT else: desired_method = LXMF.LXMessage.PROPAGATED message = LXMF.LXMessage(destination, source, content, title=title, desired_method=desired_method) if fields is not None: message.fields = fields if timestamp is not None: message.timestamp = timestamp message.app_data = app_data self.message_method(message) self.log_message(message, "LXMF - Message send") message.register_delivery_callback(self.message_notification) message.register_failed_callback(self.message_notification) if self.message_router.get_outbound_propagation_node() != None: message.try_propagation_on_fail = self.try_propagation_on_fail try: self.message_router.handle_outbound(message) time.sleep(self.send_delay) except Exception as e: log("LXMF - Could not send message " + str(message), LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) return def message_notification(self, message): self.message_method(message) if self.message_notification_callback is not None: self.message_notification_callback(message) if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: self.log_message(message, "LXMF - Delivery receipt (failed) Retrying as propagated message") message.try_propagation_on_fail = None message.delivery_attempts = 0 del message.next_delivery_attempt message.packed = None message.desired_method = LXMF.LXMessage.PROPAGATED self.message_router.handle_outbound(message) elif message.state == LXMF.LXMessage.FAILED: self.log_message(message, "LXMF - Delivery receipt (failed)") if self.message_notification_failed_callback is not None: self.message_notification_failed_callback(message) else: self.log_message(message, "LXMF - Delivery receipt (success)") if self.message_notification_success_callback is not None: self.message_notification_success_callback(message) def message_method(self, message): if message.desired_method == LXMF.LXMessage.DIRECT: message.desired_method_str = "direct" elif message.desired_method == LXMF.LXMessage.PROPAGATED: message.desired_method_str = "propagated" def announce(self, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) announce_timer.daemon = True announce_timer.start() if initial: if self.announce_startup: if self.announce_startup_delay > 0: if announce_timer is not None: announce_timer.cancel() announce_timer = threading.Timer(self.announce_startup_delay, self.announce) announce_timer.daemon = True announce_timer.start() else: self.announce_now() return self.announce_now() def announce_now(self, app_data=None): if app_data: self.destination.announce(app_data.encode("utf-8")) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ":" + app_data, LOG_DEBUG) else: self.destination.announce() log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) + ": " + self.display_name, LOG_DEBUG) def sync(self, initial=False): sync_timer = None if self.sync_periodic and self.sync_periodic_interval > 0: sync_timer = threading.Timer(self.sync_periodic_interval*60, self.sync) sync_timer.daemon = True sync_timer.start() if initial: if self.sync_startup: if self.sync_startup_delay > 0: if sync_timer is not None: sync_timer.cancel() sync_timer = threading.Timer(self.sync_startup_delay, self.sync) sync_timer.daemon = True sync_timer.start() else: self.sync_now(self.sync_limit) return self.sync_now(self.sync_limit) def sync_now(self, limit=None): if self.message_router.get_outbound_propagation_node() is not None: if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: log("LXMF - Message sync requested from propagation node " + RNS.prettyhexrep(self.message_router.get_outbound_propagation_node()) + " for " + str(self.identity)) self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) return True else: return False else: return False def autoselect_propagation_node(self): if self.propagation_node is not None: if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): log("LXMF - Propagation node length is invalid", LOG_ERROR) else: try: propagation_hash = bytes.fromhex(self.propagation_node) except Exception as e: log("LXMF - Propagation node is invalid", LOG_ERROR) return node_identity = RNS.Identity.recall(propagation_hash) if node_identity != None: log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) self.message_router.set_outbound_propagation_node(propagation_hash) else: log("LXMF - Propagation node identity not known", LOG_ERROR) def client_connected(self, link): log("LXMF - Client connected " + str(link), LOG_EXTREME) link.set_resource_strategy(RNS.Link.ACCEPT_ALL) link.set_resource_concluded_callback(self.resource_concluded) link.set_packet_callback(self.packet_received) def packet_received(self, lxmf_bytes, packet): log("LXMF - Single packet delivered " + str(packet), LOG_EXTREME) self.process_lxmf_message_bytes(lxmf_bytes) def resource_concluded(self, resource): log("LXMF - Resource data transfer (multi packet) delivered " + str(resource.file), LOG_EXTREME) if resource.status == RNS.Resource.COMPLETE: lxmf_bytes = resource.data.read() self.process_lxmf_message_bytes(lxmf_bytes) else: log("LXMF - Received resource message is not complete", LOG_EXTREME) def process_lxmf_message_bytes(self, lxmf_bytes): try: message = LXMF.LXMessage.unpack_from_bytes(lxmf_bytes) except Exception as e: log("LXMF - Could not assemble LXMF message from received data", LOG_ERROR) log("LXMF - The contained exception was: " + str(e), LOG_ERROR) return message.desired_method = LXMF.LXMessage.DIRECT self.message_method(message) self.log_message(message, "LXMF - Message received") if self.message_received_callback is not None: log("LXMF - Call to registered message received callback", LOG_DEBUG) self.message_received_callback(message) else: log("LXMF - No message received callback registered", LOG_DEBUG) def process_lxmf_message_propagated(self, message): message.desired_method = LXMF.LXMessage.PROPAGATED self.message_method(message) self.log_message(message, "LXMF - Message received") if self.message_received_callback is not None: log("LXMF - Call to registered message received callback", LOG_DEBUG) self.message_received_callback(message) else: log("LXMF - No message received callback registered", LOG_DEBUG) def log_message(self, message, message_tag="LXMF - Message log"): if message.signature_validated: signature_string = "Validated" else: if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: signature_string = "Invalid signature" elif message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: signature_string = "Cannot verify, source is unknown" else: signature_string = "Signature is invalid, reason undetermined" title = message.title.decode('utf-8') content = message.content.decode('utf-8') fields = message.fields log(message_tag + ":", LOG_DEBUG) log("- Date/Time: " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)), LOG_DEBUG) log("- Title: " + title, LOG_DEBUG) log("- Content: " + content, LOG_DEBUG) log("- Fields: " + str(fields), LOG_DEBUG) log("- Size: " + str(len(title) + len(content) + len(title) + len(pickle.dumps(fields))) + " bytes", LOG_DEBUG) log("- Source: " + RNS.prettyhexrep(message.source_hash), LOG_DEBUG) log("- Destination: " + RNS.prettyhexrep(message.destination_hash), LOG_DEBUG) log("- Signature: " + signature_string, LOG_DEBUG) log("- Attempts: " + str(message.delivery_attempts), LOG_DEBUG) if hasattr(message, "desired_method_str"): log("- Method: " + message.desired_method_str + " (" + str(message.desired_method) + ")", LOG_DEBUG) else: log("- Method: " + str(message.desired_method), LOG_DEBUG) if hasattr(message, "app_data"): log("- App Data: " + message.app_data, LOG_DEBUG) ############################################################################################################## # LXMF Functions #### LXMF - Success #### def lxmf_success(message): global DATA key = RNS.hexrep(message.destination_hash, False) if not key in DATA: key = "." if not key in DATA: return DATA[key]["count_success"] = DATA[key]["count_success"] + 1 timestamp = round(float(time.time()) - float(message.timestamp), 4) if DATA[key]["time_min"] == 0 or DATA[key]["time_min"] > timestamp: DATA[key]["time_min"] = timestamp if DATA[key]["time_max"] == 0 or DATA[key]["time_max"] < timestamp: DATA[key]["time_max"] = timestamp DATA[key]["time"] = DATA[key]["time"] + timestamp DATA[key]["time_avg"] = round(DATA[key]["time"]/DATA[key]["count_success"], 4) count = str(message.content) if "#" in count: count = count.split("#", 1)[1] count = count.split(" ", 1)[0] else: count = "" print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Success") #### LXMF - Failed #### def lxmf_failed(message): global DATA key = RNS.hexrep(message.destination_hash, False) if not key in DATA: key = "." if not key in DATA: return DATA[key]["count_failed"] = DATA[key]["count_failed"] + 1 count = str(message.content) if "#" in count: count = count.split("#", 1)[1] count = count.split(" ", 1)[0] else: count = "" print("Destination: " + str (key) + " | #: " + str(count) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Failed") ############################################################################################################## # Value convert def val_to_bool(val): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": return True else: return False ############################################################################################################## # Log LOG_FORCE = -1 LOG_CRITICAL = 0 LOG_ERROR = 1 LOG_WARNING = 2 LOG_NOTICE = 3 LOG_INFO = 4 LOG_VERBOSE = 5 LOG_DEBUG = 6 LOG_EXTREME = 7 LOG_LEVEL = LOG_NOTICE LOG_LEVEL_SERVICE = LOG_NOTICE LOG_TIMEFMT = "%Y-%m-%d %H:%M:%S" LOG_MAXSIZE = 5*1024*1024 LOG_PREFIX = "" LOG_SUFFIX = "" LOG_FILE = "" def log(text, level=3, file=None): if not LOG_LEVEL: return if LOG_LEVEL >= level: name = "Unknown" if (level == LOG_FORCE): name = "" if (level == LOG_CRITICAL): name = "Critical" if (level == LOG_ERROR): name = "Error" if (level == LOG_WARNING): name = "Warning" if (level == LOG_NOTICE): name = "Notice" if (level == LOG_INFO): name = "Info" if (level == LOG_VERBOSE): name = "Verbose" if (level == LOG_DEBUG): name = "Debug" if (level == LOG_EXTREME): name = "Extra" if not isinstance(text, str): text = str(text) text = "[" + time.strftime(LOG_TIMEFMT, time.localtime(time.time())) +"] [" + name + "] " + LOG_PREFIX + text + LOG_SUFFIX if file == None and LOG_FILE != "": file = LOG_FILE if file == None: print(text) else: try: file_handle = open(file, "a") file_handle.write(text + "\n") file_handle.close() if os.path.getsize(file) > LOG_MAXSIZE: file_prev = file + ".1" if os.path.isfile(file_prev): os.unlink(file_prev) os.rename(file, file_prev) except: return ############################################################################################################## # System #### Panic ##### def panic(): sys.exit(255) #### Exit ##### def exit(): sys.exit(0) ############################################################################################################## # Setup/Start #### Setup ##### def setup(path=None, path_rns=None, path_log=None, loglevel=None, dest="", interval=1, size=128, count=0, inst=1): global DATA global PATH global PATH_RNS global LOG_LEVEL global LOG_FILE global RNS_CONNECTION global LXMF_CONNECTION if path is not None: if path.endswith("/"): path = path[:-1] PATH = path if path_rns is not None: if path_rns.endswith("/"): path_rns = path_rns[:-1] PATH_RNS = path_rns if loglevel is not None: LOG_LEVEL = loglevel rns_loglevel = loglevel else: rns_loglevel = None RNS_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) print("...............................................................................") print(" Name: " + NAME + " - " + DESCRIPTION) print("Program File: " + __file__) print(" Version: " + VERSION) print(" Copyright: " + COPYRIGHT) print("...............................................................................") log("LXMF - Connecting ...", LOG_DEBUG) if path is None: path = PATH LXMF_CONNECTION = lxmf_connection(storage_path=path) LXMF_CONNECTION.register_message_notification_success_callback(lxmf_success) LXMF_CONNECTION.register_message_notification_failed_callback(lxmf_failed) log("LXMF - Connected", LOG_DEBUG) log("...............................................................................", LOG_FORCE) log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) log("...............................................................................", LOG_FORCE) DATA = {} destinations = dest.split(",") for key in destinations: DATA[key] = {} DATA[key]["count"] = 0 DATA[key]["count_success"] = 0 DATA[key]["count_failed"] = 0 DATA[key]["time"] = 0 DATA[key]["time_min"] = 0 DATA[key]["time_max"] = 0 DATA[key]["time_avg"] = 0 count_current = 0 while True: if count == 0 or count_current < count: count_current = count_current + 1 letters = string.ascii_lowercase content = ''.join(random.choice(letters) for i in range(size)) for key in DATA: DATA[key]["count"] = DATA[key]["count"] + 1 content = "#"+ str(DATA[key]["count"]) + " " + content content = content[:size] if key == ".": LXMF_CONNECTION.send(secrets.token_hex(nbytes=10), content) else: LXMF_CONNECTION.send(key, content) print("Destination: " + str (key) + " | #: " + str(DATA[key]["count"]) + " | Messages delivered: " + str(DATA[key]["count_success"]) + "/" + str(DATA[key]["count"]) + " (" + str(round(100/DATA[key]["count"]*DATA[key]["count_success"], 2)) + "%) | Time (min / max / avg): " + str(DATA[key]["time_min"]) + " / " + str(DATA[key]["time_max"]) + " / " + str(DATA[key]["time_avg"]) + " | Info: Sending/Queued") time.sleep(interval) #### Start #### def main(): try: description = NAME + " - " + DESCRIPTION parser = argparse.ArgumentParser(description=description) parser.add_argument("-p", "--path", action="store", type=str, default=None, help="Path to alternative config directory") parser.add_argument("-pr", "--path_rns", action="store", type=str, default=None, help="Path to alternative Reticulum config directory") parser.add_argument("-pl", "--path_log", action="store", type=str, default=None, help="Path to alternative log directory") parser.add_argument("-l", "--loglevel", action="store", type=int, default=LOG_LEVEL) parser.add_argument("-d", "--dest", action="store", required=True, type=str, default=None, help="Single destination hash or ,-separated list with destination hashs or . for random destination") parser.add_argument("-t", "--time", action="store", type=float, default=1, help="Time between messages in seconds") parser.add_argument("-s", "--size", action="store", type=int, default=128, help="Size (lenght) of the message content") parser.add_argument("-c", "--count", action="store", type=float, default=0, help="Maximum message send count (0=no end)") parser.add_argument("-i", "--inst", action="store", type=int, default=1, help="Parallel instances (different sender addresses)") params = parser.parse_args() setup(path=params.path, path_rns=params.path_rns, path_log=params.path_log, loglevel=params.loglevel, dest=params.dest, interval=params.time, size=params.size, count=params.count, inst=params.inst) except KeyboardInterrupt: print("Terminated by CTRL-C") exit() ############################################################################################################## # Init if __name__ == "__main__": main()