__init__.py
1 # Reticulum License 2 # 3 # Copyright (c) 2016-2025 Mark Qvist 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # - The Software shall not be used in any kind of system which includes amongst 13 # its functions the ability to purposefully do harm to human beings. 14 # 15 # - The Software shall not be used, directly or indirectly, in the creation of 16 # an artificial intelligence, machine learning or language model training 17 # dataset, including but not limited to any use that contributes to the 18 # training or development of such a model or algorithm. 19 # 20 # - The above copyright notice and this permission notice shall be included in 21 # all copies or substantial portions of the Software. 22 # 23 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 # SOFTWARE. 30 31 import os 32 import sys 33 import glob 34 import time 35 import datetime 36 import random 37 import threading 38 39 from ._version import __version__ 40 41 from .Reticulum import Reticulum 42 from .Identity import Identity 43 from .Link import Link, RequestReceipt 44 from .Channel import MessageBase 45 from .Buffer import Buffer, RawChannelReader, RawChannelWriter 46 from .Transport import Transport 47 from .Destination import Destination 48 from .Packet import Packet 49 from .Packet import PacketReceipt 50 from .Resolver import Resolver 51 from .Resource import Resource, ResourceAdvertisement 52 from .Cryptography import HKDF 53 from .Cryptography import Hashes 54 55 py_modules = glob.glob(os.path.dirname(__file__)+"/*.py") 56 pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc") 57 modules = py_modules+pyc_modules 58 __all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))])) 59 60 import importlib.util 61 if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled 62 else: compiled = False 63 64 LOG_NONE = -1 65 LOG_CRITICAL = 0 66 LOG_ERROR = 1 67 LOG_WARNING = 2 68 LOG_NOTICE = 3 69 LOG_INFO = 4 70 LOG_VERBOSE = 5 71 LOG_DEBUG = 6 72 LOG_EXTREME = 7 73 74 LOG_STDOUT = 0x91 75 LOG_FILE = 0x92 76 LOG_CALLBACK = 0x93 77 78 LOG_MAXSIZE = 5*1024*1024 79 80 loglevel = LOG_NOTICE 81 logfile = None 82 logdest = LOG_STDOUT 83 logcall = None 84 logtimefmt = "%Y-%m-%d %H:%M:%S" 85 logtimefmt_p = "%H:%M:%S.%f" 86 compact_log_fmt = False 87 88 instance_random = random.Random() 89 instance_random.seed(os.urandom(10)) 90 91 _always_override_destination = False 92 93 logging_lock = threading.Lock() 94 95 def loglevelname(level): 96 if (level == LOG_CRITICAL): 97 return "[Critical]" 98 if (level == LOG_ERROR): 99 return "[Error] " 100 if (level == LOG_WARNING): 101 return "[Warning] " 102 if (level == LOG_NOTICE): 103 return "[Notice] " 104 if (level == LOG_INFO): 105 return "[Info] " 106 if (level == LOG_VERBOSE): 107 return "[Verbose] " 108 if (level == LOG_DEBUG): 109 return "[Debug] " 110 if (level == LOG_EXTREME): 111 return "[Extra] " 112 113 return "Unknown" 114 115 def version(): 116 return __version__ 117 118 def host_os(): 119 from .vendor.platformutils import get_platform 120 return get_platform() 121 122 def timestamp_str(time_s): 123 timestamp = time.localtime(time_s) 124 return time.strftime(logtimefmt, timestamp) 125 126 def precise_timestamp_str(time_s): 127 return datetime.datetime.now().strftime(logtimefmt_p)[:-3] 128 129 def log(msg, level=3, _override_destination = False, pt=False): 130 if loglevel == LOG_NONE: return 131 global _always_override_destination, compact_log_fmt 132 msg = str(msg) 133 if loglevel >= level: 134 if pt: 135 logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg 136 else: 137 if not compact_log_fmt: 138 logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg 139 else: 140 logstring = "["+timestamp_str(time.time())+"] "+msg 141 142 with logging_lock: 143 if (logdest == LOG_STDOUT or _always_override_destination or _override_destination): 144 if not threading.main_thread().is_alive(): return 145 else: print(logstring) 146 147 elif (logdest == LOG_FILE and logfile != None): 148 try: 149 file = open(logfile, "a") 150 file.write(logstring+"\n") 151 file.close() 152 153 if os.path.getsize(logfile) > LOG_MAXSIZE: 154 prevfile = logfile+".1" 155 if os.path.isfile(prevfile): 156 os.unlink(prevfile) 157 os.rename(logfile, prevfile) 158 159 except Exception as e: 160 _always_override_destination = True 161 log("Exception occurred while writing log message to log file: "+str(e), LOG_CRITICAL) 162 log("Dumping future log events to console!", LOG_CRITICAL) 163 log(msg, level) 164 165 elif logdest == LOG_CALLBACK: 166 try: 167 logcall(logstring) 168 except Exception as e: 169 _always_override_destination = True 170 log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL) 171 log("Dumping future log events to console!", LOG_CRITICAL) 172 log(msg, level) 173 174 175 def rand(): 176 result = instance_random.random() 177 return result 178 179 def trace_exception(e): 180 import traceback 181 exception_info = "".join(traceback.TracebackException.from_exception(e).format()) 182 log(f"An unhandled {str(type(e))} exception occurred: {str(e)}", LOG_ERROR) 183 log(exception_info, LOG_ERROR) 184 185 def hexrep(data, delimit=True): 186 try: 187 iter(data) 188 except TypeError: 189 data = [data] 190 191 delimiter = ":" 192 if not delimit: 193 delimiter = "" 194 hexrep = delimiter.join("{:02x}".format(c) for c in data) 195 return hexrep 196 197 def prettyhexrep(data): 198 delimiter = "" 199 hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">" 200 return hexrep 201 202 def prettyspeed(num, suffix="b"): 203 return prettysize(num/8, suffix=suffix)+"ps" 204 205 def prettysize(num, suffix='B'): 206 units = ['','K','M','G','T','P','E','Z'] 207 last_unit = 'Y' 208 209 if suffix == 'b': 210 num *= 8 211 units = ['','K','M','G','T','P','E','Z'] 212 last_unit = 'Y' 213 214 for unit in units: 215 if abs(num) < 1000.0: 216 if unit == "": 217 return "%.0f %s%s" % (num, unit, suffix) 218 else: 219 return "%.2f %s%s" % (num, unit, suffix) 220 num /= 1000.0 221 222 return "%.2f%s%s" % (num, last_unit, suffix) 223 224 def prettyfrequency(hz, suffix="Hz"): 225 num = hz*1e6 226 units = ["µ", "m", "", "K","M","G","T","P","E","Z"] 227 last_unit = "Y" 228 229 for unit in units: 230 if abs(num) < 1000.0: 231 return "%.2f %s%s" % (num, unit, suffix) 232 num /= 1000.0 233 234 return "%.2f%s%s" % (num, last_unit, suffix) 235 236 def prettydistance(m, suffix="m"): 237 num = m*1e6 238 units = ["µ", "m", "c", ""] 239 last_unit = "K" 240 241 for unit in units: 242 divisor = 1000.0 243 if unit == "m": divisor = 10 244 if unit == "c": divisor = 100 245 246 if abs(num) < divisor: 247 return "%.2f %s%s" % (num, unit, suffix) 248 num /= divisor 249 250 return "%.2f %s%s" % (num, last_unit, suffix) 251 252 def prettytime(time, verbose=False, compact=False): 253 neg = False 254 if time < 0: 255 time = abs(time) 256 neg = True 257 258 days = int(time // (24 * 3600)) 259 time = time % (24 * 3600) 260 hours = int(time // 3600) 261 time %= 3600 262 minutes = int(time // 60) 263 time %= 60 264 if compact: 265 seconds = int(time) 266 else: 267 seconds = round(time, 2) 268 269 ss = "" if seconds == 1 else "s" 270 sm = "" if minutes == 1 else "s" 271 sh = "" if hours == 1 else "s" 272 sd = "" if days == 1 else "s" 273 274 displayed = 0 275 components = [] 276 if days > 0 and ((not compact) or displayed < 2): 277 components.append(str(days)+" day"+sd if verbose else str(days)+"d") 278 displayed += 1 279 280 if hours > 0 and ((not compact) or displayed < 2): 281 components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h") 282 displayed += 1 283 284 if minutes > 0 and ((not compact) or displayed < 2): 285 components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m") 286 displayed += 1 287 288 if seconds > 0 and ((not compact) or displayed < 2): 289 components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s") 290 displayed += 1 291 292 i = 0 293 tstr = "" 294 for c in components: 295 i += 1 296 if i == 1: 297 pass 298 elif i < len(components): 299 tstr += ", " 300 elif i == len(components): 301 tstr += " and " 302 303 tstr += c 304 305 if tstr == "": 306 return "0s" 307 else: 308 if not neg: 309 return tstr 310 else: 311 return f"-{tstr}" 312 313 def prettyshorttime(time, verbose=False, compact=False): 314 neg = False 315 time = time*1e6 316 if time < 0: 317 time = abs(time) 318 neg = True 319 320 seconds = int(time // 1e6); time %= 1e6 321 milliseconds = int(time // 1e3); time %= 1e3 322 323 if compact: 324 microseconds = int(time) 325 else: 326 microseconds = round(time, 2) 327 328 ss = "" if seconds == 1 else "s" 329 sms = "" if milliseconds == 1 else "s" 330 sus = "" if microseconds == 1 else "s" 331 332 displayed = 0 333 components = [] 334 if seconds > 0 and ((not compact) or displayed < 2): 335 components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s") 336 displayed += 1 337 338 if milliseconds > 0 and ((not compact) or displayed < 2): 339 components.append(str(milliseconds)+" millisecond"+sms if verbose else str(milliseconds)+"ms") 340 displayed += 1 341 342 if microseconds > 0 and ((not compact) or displayed < 2): 343 components.append(str(microseconds)+" microsecond"+sus if verbose else str(microseconds)+"µs") 344 displayed += 1 345 346 i = 0 347 tstr = "" 348 for c in components: 349 i += 1 350 if i == 1: 351 pass 352 elif i < len(components): 353 tstr += ", " 354 elif i == len(components): 355 tstr += " and " 356 357 tstr += c 358 359 if tstr == "": 360 return "0us" 361 else: 362 if not neg: 363 return tstr 364 else: 365 return f"-{tstr}" 366 367 def phyparams(): 368 print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes") 369 print("Plaintext Packet MDU : "+str(Packet.PLAIN_MDU)+" bytes") 370 print("Encrypted Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes") 371 print("Link Curve : "+str(Link.CURVE)) 372 print("Link Packet MDU : "+str(Link.MDU)+" bytes") 373 print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits") 374 print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits") 375 376 def panic(): 377 os._exit(255) 378 379 exit_called = False 380 def exit(code=0): 381 global exit_called 382 if not exit_called: 383 exit_called = True 384 Reticulum.exit_handler() 385 os._exit(code) 386 387 class Profiler: 388 _ran = False 389 profilers = {} 390 tags = {} 391 392 @staticmethod 393 def get_profiler(tag=None, super_tag=None): 394 if tag in Profiler.profilers: 395 return Profiler.profilers[tag] 396 else: 397 profiler = Profiler(tag, super_tag) 398 Profiler.profilers[tag] = profiler 399 return profiler 400 401 def __init__(self, tag=None, super_tag=None): 402 self.paused = False 403 self.pause_time = 0 404 self.pause_started = None 405 self.tag = tag 406 self.super_tag = super_tag 407 if self.super_tag in Profiler.profilers: 408 self.super_profiler = Profiler.profilers[self.super_tag] 409 self.pause_super = self.super_profiler.pause 410 self.resume_super = self.super_profiler.resume 411 else: 412 def noop(self=None): 413 pass 414 self.super_profiler = None 415 self.pause_super = noop 416 self.resume_super = noop 417 418 def __enter__(self): 419 self.pause_super() 420 tag = self.tag 421 super_tag = self.super_tag 422 thread_ident = threading.get_ident() 423 if not tag in Profiler.tags: 424 Profiler.tags[tag] = {"threads": {}, "super": super_tag} 425 if not thread_ident in Profiler.tags[tag]["threads"]: 426 Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []} 427 428 Profiler.tags[tag]["threads"][thread_ident]["current_start"] = time.perf_counter() 429 self.resume_super() 430 431 def __exit__(self, exc_type, exc_value, traceback): 432 self.pause_super() 433 tag = self.tag 434 super_tag = self.super_tag 435 end = time.perf_counter() - self.pause_time 436 self.pause_time = 0 437 thread_ident = threading.get_ident() 438 if tag in Profiler.tags and thread_ident in Profiler.tags[tag]["threads"]: 439 if Profiler.tags[tag]["threads"][thread_ident]["current_start"] != None: 440 begin = Profiler.tags[tag]["threads"][thread_ident]["current_start"] 441 Profiler.tags[tag]["threads"][thread_ident]["current_start"] = None 442 Profiler.tags[tag]["threads"][thread_ident]["captures"].append(end-begin) 443 if not Profiler._ran: 444 Profiler._ran = True 445 self.resume_super() 446 447 def pause(self, pause_started=None): 448 if not self.paused: 449 self.paused = True 450 self.pause_started = pause_started or time.perf_counter() 451 self.pause_super(self.pause_started) 452 453 def resume(self): 454 if self.paused: 455 self.pause_time += time.perf_counter() - self.pause_started 456 self.paused = False 457 self.resume_super() 458 459 @staticmethod 460 def ran(): 461 return Profiler._ran 462 463 @staticmethod 464 def results(): 465 from statistics import mean, median, stdev 466 results = {} 467 468 for tag in Profiler.tags: 469 tag_captures = [] 470 tag_entry = Profiler.tags[tag] 471 472 for thread_ident in tag_entry["threads"]: 473 thread_entry = tag_entry["threads"][thread_ident] 474 thread_captures = thread_entry["captures"] 475 sample_count = len(thread_captures) 476 477 if sample_count > 1: 478 thread_results = { 479 "count": sample_count, 480 "mean": mean(thread_captures), 481 "median": median(thread_captures), 482 "stdev": stdev(thread_captures) 483 } 484 elif sample_count == 1: 485 thread_results = { 486 "count": sample_count, 487 "mean": mean(thread_captures), 488 "median": median(thread_captures), 489 "stdev": None 490 } 491 492 tag_captures.extend(thread_captures) 493 494 sample_count = len(tag_captures) 495 if sample_count > 1: 496 tag_results = { 497 "name": tag, 498 "super": tag_entry["super"], 499 "count": len(tag_captures), 500 "mean": mean(tag_captures), 501 "median": median(tag_captures), 502 "stdev": stdev(tag_captures) 503 } 504 elif sample_count == 1: 505 tag_results = { 506 "name": tag, 507 "super": tag_entry["super"], 508 "count": len(tag_captures), 509 "mean": mean(tag_captures), 510 "median": median(tag_captures), 511 "stdev": None 512 } 513 514 results[tag] = tag_results 515 516 def print_results_recursive(tag, results, level=0): 517 print_tag_results(tag, level+1) 518 519 for tag_name in results: 520 sub_tag = results[tag_name] 521 if sub_tag["super"] == tag["name"]: 522 print_results_recursive(sub_tag, results, level=level+1) 523 524 525 def print_tag_results(tag, level): 526 ind = " "*level 527 name = tag["name"]; count = tag["count"] 528 mean = tag["mean"]; median = tag["median"]; stdev = tag["stdev"] 529 print( f"{ind}{name}") 530 print( f"{ind} Samples : {count}") 531 if stdev != None: 532 print(f"{ind} Mean : {prettyshorttime(mean)}") 533 print(f"{ind} Median : {prettyshorttime(median)}") 534 print(f"{ind} St.dev. : {prettyshorttime(stdev)}") 535 print( f"{ind} Total : {prettyshorttime(mean*count)}") 536 print("") 537 538 print("\nProfiler results:\n") 539 for tag_name in results: 540 tag = results[tag_name] 541 if tag["super"] == None: 542 print_results_recursive(tag, results) 543 544 profile = Profiler.get_profiler