Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

4
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__
.venv
*.log
app.zip

1
app/.mtimes.json Normal file
View File

@@ -0,0 +1 @@
{".gitignore": 1765697926.2944038, "config.json": 1766641092.7797186, "main.py": 1757275665.3581765, "requirements.txt": 1757272908.932603, "start.py": 1767345275.3860576, "updater.py": 1756702920.6384296, "utilss.py": 1755864608.0, "BKP\\.gitignore": 1741162465.633492, "BKP\\config.json": 1755418661.1762836, "BKP\\main.py": 1756113491.7506, "BKP\\start.py": 1752932509.1119049, "BKP\\updater.py": 1752758398.9098537, "BKP\\utils.py": 1752930900.679567, "common\\args.py": 1755902334.0, "common\\config.py": 1756632053.520828, "common\\logging.py": 1756701444.0262084, "common\\network_utils.py": 1756111641.1188223, "common\\paths.py": 1756118190.209198, "common\\process.py": 1757143858.1563985, "common\\store.py": 1757582915.4349658, "common\\__pycache__\\args.cpython-311.pyc": 1755902456.0, "common\\__pycache__\\config.cpython-311.pyc": 1755909410.0, "common\\__pycache__\\logging.cpython-311.pyc": 1756014259.7862537, "common\\__pycache__\\paths.cpython-311.pyc": 1756014259.7917824, "common\\__pycache__\\process.cpython-311.pyc": 1755865508.0, "ui\\ui_server.py": 1757275891.3673892, "ui\\static\\main.js": 1757241125.1477273, "ui\\templates\\index.html": 1756627053.1326618, "__pycache__\\updater.cpython-311.pyc": 1755912104.0, "__pycache__\\utils.cpython-311.pyc": 1753049185.946803}

4
app/BKP/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__
.venv
*.log
app.zip

75
app/BKP/config.json Normal file
View File

@@ -0,0 +1,75 @@
{
"app":{
"id":"app",
"checkUpdates": true,
"repositories":["http://error/data"],
"version": 0
},
"main":{
"terminalWindow": true
},
"passwordManager":{
"package": "lockbox",
"client": "lockboxClient"
},
"updater":{
"autoUpdate": true,
"checkUpdates": true,
"repositories":[
"http://n0sys.duckdns.org:40404/libs"
]
},
"packages":[
{
"id":"fspn",
"checkUpdates": true,
"repositories":["http://error/data"],
"version": 0
},
{
"id":"noSys",
"checkUpdates": true
},
{
"id":"fileTransfer",
"checkUpdates": true
},
{
"id":"api",
"checkUpdates": true,
"server": {
"host": "127.0.0.1",
"port": "5050"
}
},
{
"id":"vueNoSys",
"checkUpdates": true,
"server": {
"host": "127.0.0.1",
"port": "3001"
}
},
{
"id":"lockbox",
"checkUpdates": true,
"servers": {
"api": {
"port": "5001"
}
}
},
{
"id":"rendezvous",
"checkUpdates": true,
"server": {
"host": "0.0.0.0",
"port": 40441
}
},
{
"id":"p2post",
"checkUpdates": true
}
]
}

5
app/BKP/info.json Normal file
View File

@@ -0,0 +1,5 @@
{
"id":"app",
"version":0,
"modules":[]
}

70
app/BKP/main.py Normal file
View File

@@ -0,0 +1,70 @@
import os, sys
import time
import logging
import traceback
# TODO NOT SURE IF SYS PATH IS A GOOD PRACTICE
root_dir = os.path.normpath(__file__.split("libs")[0])
sys.path.append(root_dir)
from libs.app.utils import Utils, kargs_to_array, get_logger, is_http_running, config_root_logger
logger = None
try:
config_root_logger()
logger = get_logger()
utils = Utils()
except Exception:
logging.exception("ERROR")
from libs.app.updater import Updater
def main():
log_ascii_art()
logger.debug(f"Readable params: {kargs_to_array()}")
if not is_noSys_running():
updater = Updater(utils)
utils.configs.read_packages()
from libs.noSys.noSysCore import NoSysCore
noSys = NoSysCore(updater)
else:
logger.debug(f"NoSys already running")
# TODO open url frontend
logger.info(f"---------------- Main ended ----------------")
def is_noSys_running():
try:
api_config = utils.configs.packages["api"].config
api_port = api_config["server"]["port"]
api_health_check_url = f"http://localhost:{api_port}/api/health"
logger.debug(f"Checking url: {api_health_check_url}")
return is_http_running(api_health_check_url)
except Exception:
logger.exception("ERROR")
def log_ascii_art():
logger.info("""
_ __ ____
/ |/ /__ / __/_ _____
/ / _ \ _\ \/ // (_-<
/_/|_/\___/ /___/\_, /___/
/___/
Every man must have freedom, must have the scope to form, test, and act upon his own choices, for any sort of development of his own personality to take place. He must, in short, be free in order that he may be fully human.
- Murray Rothbard
""")
if __name__ == '__main__':
try:
main()
time.sleep(30)
except Exception:
logger.exception("ERROR")
for sec in range(30):
logger.info(f"Closing in {30-sec}")
time.sleep(1)

59
app/BKP/start.py Normal file
View File

@@ -0,0 +1,59 @@
import os, sys
import logging
import shutil
import pathlib
import urllib.request
import time
from pathlib import Path
import zipfile
root_dir = pathlib.Path(__file__).parent.resolve()
logger = logging.getLogger("start")
def start():
global logger
logger.debug(f"---------------- START ----------------")
logger.debug(f"Root path: {root_dir}")
pathlib.Path(os.path.join(root_dir, "libs")).mkdir(parents=True, exist_ok=True)
download_app()
from libs.app.utils import Utils, get_logger, create_venv, download_file, kargs_to_array
create_venv()
args = kargs_to_array()
# args.append("updateApp=False") # NEVER SET TRUE
# args.append("updateLibs=True")
utils = Utils()
main_file_path = os.path.join(root_dir, "libs","app", "main.py")
pid = utils.new_python_process(main_file_path, args)
logger.info(f"Main process running. PID {pid} - Args: {args}")
logger.info(f"---------------- END ----------------")
def download_app():
url = f"{default_repository}/app/app.zip"
path = os.path.join(root_dir, "libs", "app.zip")
if(not os.path.exists(path)):
logger.debug(f"Downloading from URL: {url} to {path}")
urllib.request.urlretrieve(url, path)
logger.debug(f"Extracting all from {path}")
with zipfile.ZipFile(path, 'r') as zip_ref:
zip_ref.extractall(Path(path).parent.absolute())
default_repository = "http://n0sys.duckdns.org:30303/libs"
logger.addHandler(logging.StreamHandler())
app_logs_path = os.path.join(root_dir, "logs", "app")
shutil.rmtree(os.path.join(root_dir, "logs"), ignore_errors=True)
pathlib.Path(app_logs_path).mkdir(parents=True, exist_ok=True)
logger.addHandler(logging.FileHandler(os.path.join(app_logs_path,'start.log'), mode="a", encoding="utf-8"))
logger.setLevel(logging.DEBUG)
if __name__ == '__main__':
try:
start()
except Exception as e:
logger.exception("ERROR")
time.sleep(30)

124
app/BKP/updater.py Normal file
View File

@@ -0,0 +1,124 @@
import sys
import subprocess
import urllib.parse
import urllib.request
import os
from pathlib import Path
import pathlib
import time
import json
import logging
import zipfile
from threading import Thread
from libs.app.utils import Utils, Package, get_logger, root_dir
logger = get_logger("updater")
class Updater():
def __init__(self, utils:Utils):
self.utils = utils
self.libs_path = os.path.join(root_dir, 'libs')
self.start()
def start(self):
logger.info(f"---------------- Updater Started ----------------")
# Create better validation code
if("updateApp" in self.utils.kargs):
if(self.utils.kargs["updateApp"]):
self.update_app()
elif(self.utils.configs.data["updater"]["autoUpdate"]):
self.update_app()
if("restart" in self.utils.flags and self.utils.flags["restart"]):
self.utils.kargs["updateApp"] = False
self.utils.restart_app()
if("updateLibs" in self.utils.kargs):
if(self.utils.kargs["updateLibs"]):
self.update_libs()
elif(self.utils.configs.data["updater"]["checkUpdates"]):
self.update_libs()
logger.info(f"---------------- Updater Ended ----------------")
def update_app(self):
latest_version_repository = (self.utils.configs.data["app"]["version"],None)
logger.info(f"App current version: {latest_version_repository[0]}")
logger.info(f"Updating app files")
# TODO FIX THIS
self.check_package_update(Package(config=self.utils.configs.data["app"]))
logger.info(f"App updated to version {latest_version_repository[0]}")
self.utils.flags["restart"] = True
def update_libs(self):
logger.info(f"Checking packages update")
threads:list[Thread] = []
for package in self.utils.configs.packages.values():
# Default is check updates
if(not "checkUpdates" in package.config or package.config["checkUpdates"]):
thread = Thread(target=self.check_package_update, args=(package,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
def check_package_update(self, package:Package):
logger.info(f"Updating package {package.config['id']}")
package_id = package.config['id']
package_path = os.path.join(self.libs_path, package_id)
if(package.info):
latest_version_repository = (package.info["version"],None)
else:
logger.info(f"Creating package directory: {package_path}")
Path(package_path).mkdir(parents=True, exist_ok=True)
latest_version_repository = (-1,None)
repositories = self.utils.configs.data["updater"]["repositories"][:]
if "repositories" in package.config:
for repository in package.config["repositories"]:
repositories.append(repository)
for repository in repositories:
try:
package_url = f"{repository}/{package_id}"
info_url = f'{package_url}/info.json'
logger.info(f"Reading remote info of {package_id} from repository {repository}")
with urllib.request.urlopen(info_url, timeout=2) as data:
remote_info = json.load(data)
logger.debug(f"{package_id} remote info: {remote_info}")
# TODO Remove 'or' condition -> "or remote_info["version"]==0"
if remote_info["version"] > latest_version_repository[0] or remote_info["version"]==0:
latest_version_repository = (remote_info["version"], package_url)
except Exception as e:
logger.error(f"Error reading remote info of {package_id} from repository {repository}: {e}")
if latest_version_repository[1]:
package_zip = f"{latest_version_repository[1]}/{package_id}.zip"
package_local_file = os.path.join(self.libs_path, f"{package_id}.zip")
logger.debug(f"Downloading package from {package_zip} to local file {package_local_file}")
self.download_package(package_zip, package_local_file)
self.pip_install_requirements(package_path)
def download_package(self, url, path):
urllib.request.urlretrieve(url, path)
logger.info(f"Extracting all from {path}")
with zipfile.ZipFile(path, 'r') as zip_ref:
zip_ref.extractall(Path(path).parent.absolute())
def pip_install_requirements(self, package_path):
path = os.path.join(package_path, "requirements.txt")
if(os.path.exists(path)):
logger.info(f"Pip installing requirements in {path}")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", path])
else:
logger.info(f"{package_path} no requirements.txt")
def check_module_requirements(self, package_path):
pass

256
app/BKP/utils.py Normal file
View File

@@ -0,0 +1,256 @@
import os, sys
import uuid
import pathlib
import json
import subprocess
root_dir = os.path.normpath(__file__.split("libs")[0])
node_id = str(uuid.uuid4())
### LOGGER ###
import logging, inspect
class CustomLoggingFormatter(logging.Formatter):
grey = "\x1b[0;37m"
green = "\x1b[1;32m"
yellow = "\x1b[1;33m"
red = "\x1b[1;31m"
purple = "\x1b[1;35m"
blue = "\x1b[1;34m"
light_blue = "\x1b[1;36m"
bold_red = "\x1b[31;1m"
blink_red = "\x1b[5m\x1b[1;31m"
reset = "\x1b[0m"
prefix = light_blue + '%(asctime)s' + reset + ' |'
colored_level = '%(levelname)-8s'
message = '| %(message)s' + ''
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
FORMATS = {
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix
}
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# class FileFilter(logging.Filter):
# def filter(self, record):
# file_path = os.path.normpath(inspect.stack()[1].filename)
# head = os.path.split(file_path)[0]
# tail = os.path.split(file_path)[1]
# if head == os.path.normpath(root_dir):
# name = "app"
# else:
# after_lib = head.replace(os.path.join(root_dir, "libs"), "")
# name = after_lib.split(os.sep)[1]
# record.name = name
# print(name)
# return True
def add_logging_console_handler(logger:logging.Logger, logging_level=logging.DEBUG):
sh = logging.StreamHandler()
sh.setLevel(logging_level)
sh.setFormatter(CustomLoggingFormatter())
logger.addHandler(sh)
def add_logging_file_handler(logger:logging.Logger, directory, file_name, logging_level=logging.DEBUG):
file_path = os.path.join(root_dir, "logs", directory, f"{file_name}.log")
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(file_path)
fh.setLevel(logging_level)
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
logger.addHandler(fh)
def config_root_logger():
logger = get_logger(name="root")
# TODO Get lib that is calling logging
# logger.addFilter(FileFilter())
def get_logger(name=""):
file_path = os.path.normpath(inspect.stack()[1].filename)
head = os.path.split(file_path)[0]
tail = os.path.split(file_path)[1]
if head == os.path.normpath(root_dir):
lib = "root"
else:
after_lib = head.replace(os.path.join(root_dir, "libs"), "")
lib = after_lib.split(os.sep)[1]
if name == "root":
logger_name = name
elif name:
logger_name = f"{lib}.{name}"
else:
logger_name = lib
file_name = name if name else lib
logger = logging.getLogger(logger_name)
logger.propagate = False
if not logger.hasHandlers():
logger.setLevel(logging.DEBUG)
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
return logger
logger = get_logger()
logger.debug(f"Root dir: {root_dir}")
### VENV ###
import venv
def create_venv():
venv_dir = os.path.join(root_dir, ".venv")
if not os.path.exists(venv_dir):
logger.debug(f"Creating python venv: {venv_dir}")
venv.create(venv_dir, with_pip=True)
### DOWNLOADS ###
import urllib.request
def download_file(url, path):
head = os.path.split(path)[0]
if not os.path.exists(head):
logger.debug(f"Creating repository {head}")
pathlib.Path(head).mkdir(parents=True, exist_ok=True)
logger.debug(f"Downloading from URL: {url} to {path}")
urllib.request.urlretrieve(url, path)
### KARGS ###
def read_kargs():
kargs = {}
params = sys.argv
for param in params:
if "=" in param:
split = param.split("=")
key = split[0]
value = split[1]
if value == "true" or value == "True":
value = True
elif value == "false" or value == "False":
value = False
kargs[key] = value
kargs["rootDir"] = root_dir
return kargs
def kargs_to_array(kargs={}):
if not kargs:
kargs = read_kargs()
array = []
for key, value in kargs.items():
array.append(f"{key}={value}")
return array
### HTTP UTILS ###
def is_http_running(url):
try:
u:urllib.request.URLopener = urllib.request.urlopen(url)
u.close()
return True
except:
return False
### UTILS ###
class Utils:
def __init__(self):
self.kargs = read_kargs()
self.configs = Config()
self.flags = {}
# def get_env():
# return os.environ.get('NOSYS_ENV', "PROD")
# def download_file(self, package, file, destination, repository=None):
# if not repository:
# repository = self.default_repository
# url = f"{repository}/{package}/{file}"
# logger.debug(f"Downloading {file} from URL: {url}")
# urllib.request.urlretrieve(url, destination)
# def download_main_file(self, ignoreIfExists=True):
# main_path = os.path.join(self.root_dir, "libs","app", "main.py")
# if (not os.path.exists(main_path)) or (os.path.exists(main_path) and not ignoreIfExists):
# self.download_file(package="app", file="main.py", destination=main_path)
def is_terminal_visible(self):
try:
return self.configs.data["main"]["terminalWindow"]
except Exception as e:
return True
# def _create_log_dir(self):
# logs_path = os.path.join(self.root_dir, "logs")
# if not os.path.exists(logs_path):
# logger.debug(f"Creating logs directory: {logs_path}")
# pathlib.Path(logs_path).mkdir(parents=True, exist_ok=True)
def new_python_process(self, file_path, args):
python_executable = "python" if self.is_terminal_visible() else "pythonw"
args = [f"{root_dir}/.venv/scripts/{python_executable}", file_path] + args
logger.debug(f"Starting a new process: {args}")
process = subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
logger.debug(f"Process PID {process.pid} - {args}")
return process.pid
def restart_app(self):
args = [sys.executable, "start.py"] + kargs_to_array(self.kargs)
subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
logger.info(f"Restarting app: {args}")
self.exit_app()
def exit_app(self, exit_code=1):
os._exit(exit_code)
# sys.exit(exit_code)
class Config:
def __init__(self):
self.data = None
self.packages:dict[str, Package] = {}
self.read_config()
def read_config(self):
config_path = os.path.join(root_dir, "libs", "app", "config.json")
if(os.path.exists(config_path)):
with open(config_path) as f:
data = json.load(f)
self.data = data
self.read_packages()
else:
raise Exception(f"Config file {config_path} not exist")
def read_packages(self):
for package in self.data["packages"]:
try:
with open(os.path.join(root_dir,'libs',package["id"],'info.json')) as f:
self.packages[package["id"]] = Package(config=package, info=json.load(f))
except Exception as e:
self.packages[package["id"]] = Package(config=package, info={})
logger.error(e)
class Package:
def __init__(self, config={}, info={}):
self.config = config
self.info = info
self.modules:dict[str, any] = {}
self.read_modules()
def read_modules(self):
if "modules" in self.info:
for module in self.info["modules"]:
self.modules[module["id"]] = module

51
app/common/args.py Normal file
View File

@@ -0,0 +1,51 @@
import sys
from typing import Dict, List, Any
def read_kargs(argv: List[str] = None) -> Dict[str, Any]:
"""
Parse command-line arguments in the format key=value.
Example:
python start.py updateApp=True port=8080
Returns:
dict: {
"updateApp": True,
"port": "8080"
}
"""
if argv is None:
argv = sys.argv
kargs: Dict[str, Any] = {}
for param in argv:
if "=" in param:
key, value = param.split("=", 1)
# Normalize booleans
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
kargs[key] = value
return kargs
def kargs_to_array(kargs: Dict[str, Any] = None) -> List[str]:
"""
Convert a dictionary of arguments back to an array of key=value.
Example:
{"updateApp": True, "port": 8080}
Returns:
["updateApp=True", "port=8080"]
"""
if not kargs:
kargs = read_kargs()
array: List[str] = []
for key, value in kargs.items():
array.append(f"{key}={value}")
return array

56
app/common/config.py Normal file
View File

@@ -0,0 +1,56 @@
import json
import os
from pathlib import Path
from typing import Dict, Any
from collections import OrderedDict
from .paths import LIBS_DIR
class Config:
def __init__(self):
self.libs: Dict[str, Dict[str, Any]] = {}
self.load_libs_config()
def load_libs_config(self):
for lib_path in LIBS_DIR.iterdir():
if lib_path.is_dir():
info_path = lib_path / "info.json"
config_path = lib_path / "config.json"
lib_id = lib_path.name
info_data = {}
config_data = {}
if info_path.exists():
with open(info_path) as f:
info_data = json.load(f, object_pairs_hook=OrderedDict)
if config_path.exists():
with open(config_path) as f:
config_data = json.load(f, object_pairs_hook=OrderedDict)
config_data["info"] = info_data
self.libs[lib_id] = config_data
def get(self, lib: str, *keys, default=None):
"""
Retrieves values from the config.
Example:
config.get("app", "libs", "app", "update", "version", default="1.0")
Parameters:
- lib: the name of the library
- *keys: a sequence of keys to navigate through nested dictionaries
- default: the value to return if any key in the chain does not exist
Returns:
- The value found at the nested key path, or `default` if a key is missing.
"""
value = self.libs.get(lib, default)
for key in keys:
if isinstance(value, dict) and key in value:
value = value[key]
else:
return default
return value

173
app/common/logging.py Normal file
View File

@@ -0,0 +1,173 @@
import os
import logging
import inspect
from collections import deque
from typing import Deque
from io import StringIO
from pathlib import Path
from .paths import ROOT_DIR, LOGS_DIR
class CustomLoggingFormatter(logging.Formatter):
"""
Custom logging formatter with ANSI colors for console output
and standard format for file output.
"""
grey = "\x1b[0;37m"
green = "\x1b[1;32m"
yellow = "\x1b[1;33m"
red = "\x1b[1;31m"
purple = "\x1b[1;35m"
blue = "\x1b[1;34m"
light_blue = "\x1b[1;36m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
prefix = light_blue + '%(asctime)s' + reset + ' |'
colored_level = '%(levelname)-8s'
message = '| %(message)s'
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
FORMATS = {
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix,
}
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno, self.file_format)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
def add_logging_console_handler(logger: logging.Logger, logging_level=logging.DEBUG):
"""
Add console handler with colored formatter to a logger.
"""
sh = logging.StreamHandler()
sh.setLevel(logging_level)
sh.setFormatter(CustomLoggingFormatter())
logger.addHandler(sh)
def add_logging_file_handler(logger: logging.Logger, directory: str, file_name: str, logging_level=logging.DEBUG):
"""
Add file handler with plain formatter to a logger.
"""
file_path = LOGS_DIR / directory / f"{file_name}.log"
file_path.parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(file_path, encoding="utf-8")
fh.setLevel(logging_level)
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
logger.addHandler(fh)
class BufferHandler(logging.Handler):
"""
Logging handler that stores log records in a limited deque.
Allows multiple listeners to be notified when a new log line is added.
"""
def __init__(self, maxlen: int = 200):
super().__init__()
self.buffer: Deque[str] = deque(maxlen=maxlen)
self.formatter = CustomLoggingFormatter()
self._listeners: list[callable] = []
def emit(self, record: logging.LogRecord):
for listener in self._listeners:
try:
listener(record)
except Exception as e:
print(f"BufferHandler listener error: {e}")
def get_logs(self):
return list(self.buffer)
def clear(self):
self.buffer.clear()
def register_listener(self, listener: callable):
"""
Register a listener callback that will be called with
each new log line.
"""
if listener not in self._listeners:
self._listeners.append(listener)
def unregister_listener(self, listener: callable):
"""Remove a previously registered listener."""
if listener in self._listeners:
self._listeners.remove(listener)
_LOGGER_BUFFERS: dict[str, BufferHandler] = {}
def add_logging_buffer_handler(logger: logging.Logger, logging_level=logging.DEBUG, maxlen: int = 300) -> BufferHandler:
"""
Add buffer handler with plain formatter to a logger.
Returns the buffer so it can be accessed later.
"""
bh = BufferHandler(maxlen=maxlen)
logger.addHandler(bh)
_LOGGER_BUFFERS[logger.name] = bh
def get_logger(name: str = "", buffer: bool = False) -> logging.Logger:
"""
Returns a logger instance with console, file, and optional buffer handlers.
Logger name is automatically derived from caller module path.
"""
file_path = os.path.normpath(inspect.stack()[1].filename)
head = os.path.split(file_path)[0]
# Determine library name
if head == os.path.normpath(ROOT_DIR):
lib = "root"
else:
after_lib = head.replace(os.path.join(ROOT_DIR, "libs"), "")
lib = after_lib.split(os.sep)[1] if len(after_lib.split(os.sep)) > 1 else "unknown"
# Build logger name
if name == "root":
logger_name = name
elif name:
logger_name = f"{lib}.{name}"
else:
logger_name = lib
file_name = name if name else lib
logger = logging.getLogger(logger_name)
logger.propagate = False
if not logger.hasHandlers():
logger.setLevel(logging.DEBUG)
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
if buffer and logger_name not in _LOGGER_BUFFERS:
add_logging_buffer_handler(logger, logging_level=logging.DEBUG)
return logger
def get_logger_buffer(logger_name: str):
"""
Returns the buffer of a previously configured logger.
Raises KeyError if the logger does not have a buffer handler configured or does not exist.
"""
if logger_name in _LOGGER_BUFFERS:
return _LOGGER_BUFFERS[logger_name]
raise KeyError(f"Logger '{logger_name}' does not have a buffer handler configured or does not exist.")
def list_loggers() -> list[str]:
"""
Returns a list of all currently registered logger names.
"""
return sorted([
name for name, obj in logging.Logger.manager.loggerDict.items()
if isinstance(obj, logging.Logger)
])
def config_root_logger() -> logging.Logger:
"""
Configure and return the root logger.
"""
return get_logger(name="root")

View File

@@ -0,0 +1,25 @@
import requests
from requests.exceptions import RequestException, Timeout
def check_url(url: str, timeout: int = 10, retries: int = 3, verify_ssl: bool = False) -> bool:
"""
Check if a given URL is reachable.
Args:
url (str): The URL to check.
timeout (int): Timeout for each request in seconds.
retries (int): Number of retry attempts before failing.
verify_ssl (bool): Verify SSL.
Returns:
bool: True if the URL is reachable, False otherwise.
"""
for attempt in range(retries):
try:
response = requests.head(url, timeout=timeout, allow_redirects=True, verify=verify_ssl)
if 200 <= response.status_code < 400:
return True
except (RequestException, Timeout):
continue
return False

8
app/common/paths.py Normal file
View File

@@ -0,0 +1,8 @@
from pathlib import Path
# Root directory of the project
ROOT_DIR = Path(__file__).resolve().parents[3]
LOGS_DIR = ROOT_DIR / "logs"
LIBS_DIR = ROOT_DIR / "libs"
FILES_DIR = ROOT_DIR / "files"

97
app/common/process.py Normal file
View File

@@ -0,0 +1,97 @@
import subprocess
import sys
import os
import platform
import shutil
from typing import List, Optional
from .paths import ROOT_DIR # centralized project paths
def _find_terminal_emulator() -> Optional[str]:
"""
Try to find an available terminal emulator on Linux.
Returns the command name if found, otherwise None.
"""
candidates = [
"x-terminal-emulator",
"gnome-terminal",
"konsole",
"xfce4-terminal",
"lxterminal",
"mate-terminal",
"tilix",
"terminator",
"xterm"
]
for term in candidates:
if shutil.which(term):
return term
return None
def new_python_process(
script_path: str,
python_exec: str = sys.executable,
args: Optional[List[str]] = None,
cwd: str = str(ROOT_DIR),
wait: bool = False,
new_console: bool = False
) -> int:
"""
Start a new Python process running the given script.
Args:
script_path (str): Path to the Python script to execute.
python_exec (str): Path to the Python executable.
args (List[str], optional): Arguments to pass to the script. Defaults to [].
cwd (str): Working directory for the process. Defaults to project root.
wait (bool): If True, waits for process completion. Defaults to False.
new_console (bool): If True, tries to open process in a new console window.
Returns:
int: PID of the created process.
"""
if not os.path.isabs(python_exec):
python_exec = os.path.normpath(os.path.join(cwd, python_exec))
if not os.path.isabs(script_path):
script_path = os.path.normpath(os.path.join(cwd, script_path))
if args is None:
args = []
command = [python_exec, script_path] + args
creation_flags = 0
shell = False
system = platform.system().lower()
if new_console:
if system == "windows":
creation_flags = subprocess.CREATE_NEW_CONSOLE
elif system == "linux":
term = _find_terminal_emulator()
if term:
command = [term, "-e"] + command
else:
# fallback: run in same terminal
pass
elif system == "darwin": # MacOS
osa_cmd = f'tell app "Terminal" to do script "{python_exec} {script_path} {" ".join(args)}"'
process = subprocess.Popen(["osascript", "-e", osa_cmd], cwd=cwd)
if wait:
process.wait()
return process.pid
process = subprocess.Popen(
command,
cwd=cwd,
creationflags=creation_flags,
shell=shell
)
if wait:
process.wait()
return process.pid

75
app/common/store.py Normal file
View File

@@ -0,0 +1,75 @@
import json
import os
from pathlib import Path
from typing import List, Dict, Any, Optional
from libs.app.common.paths import FILES_DIR
class DataStore:
def __init__(self, path: str = "data/data.json", default_data:Optional[Dict[str, List[Any]]] = None, autosave:bool = True):
if default_data is None:
default_data = {"data": []}
if not os.path.isabs(path):
path = os.path.normpath(os.path.join(FILES_DIR, path))
self.file_path = Path(path)
self.default_data = default_data
self.data = default_data.copy()
self.autosave = autosave
self.load()
def load(self):
if self.file_path.exists():
with open(self.file_path, "r", encoding="utf-8") as f:
self.data = json.load(f)
else:
os.makedirs(self.file_path.parent, exist_ok=True)
self._save()
def save(self):
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.data, f, indent=2)
def _save(self):
if not self.autosave:
return
self.save()
def clear(self):
self.data = self.default_data.copy()
self._save()
def add_item(self, parent:str, data: Dict[str, Any], unique: bool = False, id_field:Optional[str]=None, id: Optional[str]=None):
if unique and id and id_field:
if any(item.get(id_field) == id for item in self.data.get(parent, [])):
raise ValueError(f"Item already exists in {parent}. {id_field}:{id}")
self.data.setdefault(parent, []).append(data)
self._save()
def get_item(self, parent:str, id_field:str, id:str) -> Optional[Dict[str, Any]]:
for item in self.data.get(parent, []):
if item.get(id_field) == id:
return item
return None
def update_item(self, parent: str, id_field: str, id: str, updates: Dict[str, Any]) -> bool:
for item in self.data.get(parent, []):
if item.get(id_field) == id:
item.update(updates)
self._save()
return True
return False
def remove_item(self, parent:str, id_field:str, id:str) -> bool:
items = self.data.get(parent, [])
new_items = [item for item in items if item.get(id_field) != id]
if len(new_items) != len(items):
self.data[parent] = new_items
self._save()
return True
return False
def list_items(self, parent:str) -> List[Dict[str, Any]]:
return self.data.get(parent, [])

44
app/config.json Normal file
View File

@@ -0,0 +1,44 @@
{
"terminalWindow": true,
"repositories":["https://n0sys.duckdns.org/downloads/libs"],
"libs":[
{
"id":"app",
"update": {
"checkUpdates": true,
"version": 0,
"repositories": ["http://custom.repo/rendezvous"]
},
"enabled": true
},
{
"id":"fspn"
},
{
"id":"api"
},
{
"id":"noSys"
},
{
"id":"fileTransfer"
},
{
"id":"vueNoSys"
},
{
"id":"lockbox"
},
{
"id":"rendezvous"
},
{
"id":"p2post"
},
{
"id":"p2private"
}
]
}

5
app/info.json Normal file
View File

@@ -0,0 +1,5 @@
{
"id": "app",
"version": 0.111,
"modules": []
}

117
app/main.py Normal file
View File

@@ -0,0 +1,117 @@
import os, sys
import time
import logging
import traceback
import webbrowser
import webview
import threading
from common.paths import ROOT_DIR
sys.path.insert(0, str(ROOT_DIR))
from libs.app.common.logging import get_logger, get_logger_buffer, list_loggers
from libs.app.common.args import read_kargs, kargs_to_array
from libs.app.common.config import Config
from libs.app.common.network_utils import check_url
from libs.app.updater import Updater
from libs.app.ui.ui_server import UIServer
logger = get_logger(buffer=True)
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
class App:
def __init__(self):
self.config = Config()
self.nosys_api_host = self.config.get("api", "server", "host")
self.nosys_api_port = self.config.get("api", "server", "port")
self.ui_server = UIServer()
self.ui_server.start()
self.updater = Updater(self.config)
self.updater.register_listener(self.on_updater_event)
get_logger_buffer("app").register_listener(self.on_log)
get_logger_buffer("app.updater").register_listener(self.on_log)
def start_frontend(self):
url = "http://127.0.0.1:5000/"
logger.debug("Opening browser...")
webbrowser.open(url)
# logger.debug("Starting webview ...")
# webview.create_window(
# "NoSys",
# url,
# width=800,
# height=800,
# resizable=True,
# confirm_close=False,
# text_select=True,
# frameless=False,
# background_color="#000000",
# )
# webview.start(debug=True)
def update_libraries(self):
"""Update libs and restart if app itself was updated."""
updated = self.updater.update_libs()
if updated.get("app") == "Success updated":
logger.warning("App updated, restarting...")
self.ui_server.emit_event("restarting")
self.ui_server.stop()
os.execv(sys.executable, [sys.executable] + sys.argv)
def start_nosys(self):
url = f"https://{self.nosys_api_host}:{self.nosys_api_port}/api/api/health"
logger.debug(f"Checking NoSys Server {url} ...")
if check_url(url, 3, 1, verify_ssl=True):
logger.debug("NoSys already running")
self.ui_server.emit_event("redirect", {"url": f"https://{self.nosys_api_host}:{self.nosys_api_port}"})
return
logger.debug("Starting NoSys")
self.ui_server.emit_event("nosys_starting")
from libs.noSys.noSysCore import NoSysCore
from libs.noSys.events import Events
self.nosys_core = NoSysCore()
get_logger_buffer("noSys").register_listener(self.on_log)
get_logger_buffer("noSys").register_listener(self.on_log)
get_logger_buffer("noSys.moduleManager").register_listener(self.on_log)
self.nosys_core.subscribe_event(Events.READY, self.on_nosys_ready)
self.nosys_core.start()
def start(self):
self.start_frontend()
self.update_libraries()
self.config.load_libs_config()
self.start_nosys()
def on_updater_event(self, event):
if event.name == "status_lib":
self.ui_server.emit_event(event.name, {"lib":event.lib, "status": event.status})
def on_log(self, record):
self.ui_server.emit_event("log", {"levelname":record.levelname, "message":record.message})
def on_nosys_ready(self, event):
logger.debug(f"Stoping startup frontend server and redirecting to nosys server {self.nosys_api_host}:{self.nosys_api_port} ...")
self.ui_server.emit_event("redirect", {"url": f"https://{self.nosys_api_host}:{self.nosys_api_port}"})
self.ui_server.stop()
def main():
try:
app = App()
app.start()
input("Done\nPress any key to close\n")
except Exception:
logger.exception("Error")
input("Error\nPress any key to close\n")
if __name__ == '__main__':
main()

4
app/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
requests
Flask
flask-socketio
pywebview

142
app/start.py Normal file
View File

@@ -0,0 +1,142 @@
import os
import sys
import logging
from logging.handlers import RotatingFileHandler
import shutil
import pathlib
import urllib.request
import subprocess
import zipfile
import venv
from pathlib import Path
# ==============================
# Configuration constants
# ==============================
ROOT_DIR = Path(__file__).parent.resolve()
LIBS_DIR = ROOT_DIR / "libs"
LOGS_DIR = ROOT_DIR / "logs"
APP_MAIN = LIBS_DIR / "app" / "main.py"
APP_ZIP = LIBS_DIR / "app.zip"
DEFAULT_REPOSITORY = "https://n0sys.duckdns.org/downloads/libs"
ARGS_LIST = ["updateApp=False", "updateLibs=True", "repack=True"]
# ==============================
# Logger setup
# ==============================
def setup_logger() -> logging.Logger:
"""Configure application logger with console and rotating file handlers."""
logger = logging.getLogger("start")
logger.setLevel(logging.DEBUG)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
# Log format
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# File handler
file_handler = RotatingFileHandler(LOGS_DIR / "start.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
logger = setup_logger()
# ==============================
# Functions
# ==============================
def start_app():
"""Entry point for the launcher"""
logger.info("--------- START ---------")
logger.debug(f"Root path: {ROOT_DIR}")
LIBS_DIR.mkdir(parents=True, exist_ok=True)
# TODO: remove in production
# update_libs()
create_venv()
ensure_app()
# Import after app is available
from libs.app.common.process import new_python_process
from libs.app.common.args import read_kargs, kargs_to_array
args = kargs_to_array()
pid = new_python_process(APP_MAIN, str(get_venv_python()), args=args, wait=True, new_console=True)
logger.info(f"Main process running. PID {pid} - Args: {args}")
logger.info("--------- END ---------")
def get_venv_python():
"""Return the path to the venv's Python executable in a cross-platform way"""
if os.name == "nt":
return ROOT_DIR / ".venv" / "Scripts" / "python.exe"
else:
return ROOT_DIR / ".venv" / "bin" / "python"
def update_libs():
"""Development only: repack local libs"""
if ("updateApp=True" in ARGS_LIST or "updateLibs=True" in ARGS_LIST) and "repack=True" in ARGS_LIST:
update_version_script = r"C:\Workspace\utils\updateLibsVersion.py"
try:
subprocess.run(
[str(get_venv_python()), update_version_script],
cwd=ROOT_DIR,
creationflags=subprocess.CREATE_NEW_CONSOLE,
check=True,
)
except subprocess.CalledProcessError as e:
logger.error(f"Error running update libs: {e}")
def ensure_app():
"""Download, extract and install requirements of the application if not already present"""
if APP_MAIN.exists():
logger.debug("App already present, skipping download.")
return
url = f"{DEFAULT_REPOSITORY}/app/app.zip"
logger.info(f"Downloading app from {url} ...")
try:
urllib.request.urlretrieve(url, APP_ZIP)
except Exception as e:
logger.exception(f"Failed to download app")
raise
logger.info(f"Extracting {APP_ZIP}...")
with zipfile.ZipFile(APP_ZIP, 'r') as zip_ref:
zip_ref.extractall(LIBS_DIR)
logger.info(f"Installing app requirements ...")
python_exec = str(get_venv_python())
requirements_path = os.path.join(ROOT_DIR, "libs/app/requirements.txt")
subprocess.check_call([python_exec, "-m", "pip", "install", "-r", requirements_path])
def create_venv():
"""Ensure a local virtual environment exists."""
venv_dir = os.path.join(ROOT_DIR, ".venv")
if not os.path.exists(venv_dir):
logger.debug(f"Creating python venv: {venv_dir}")
venv.create(venv_dir, with_pip=True)
# ==============================
# Main
# ==============================
if __name__ == "__main__":
try:
start_app()
except Exception:
logger.exception("Error starting application")
sys.exit(1)

65
app/ui/static/main.js Normal file
View File

@@ -0,0 +1,65 @@
const socket = io();
// const socket = io("http://127.0.0.1:5000");
socket.on("connect", () => {
console.log("Connected...");
socket.emit("frontend_ready");
});
const logContainer = document.getElementById("log-container");
function appendLog(data) {
const line = document.createElement("div");
line.textContent = data.levelname + " : " + data.message;
logContainer.appendChild(line);
logContainer.scrollTop = logContainer.scrollHeight;
}
const statusTree = document.getElementById("updater-tree");
let statusTreeData = {};
function renderStatusTable() {
let html = `
<table border="1" cellpadding="6">
<tr>
<th>Módulo</th>
<th>Status</th>
</tr>
`;
for (const [module, status] of Object.entries(statusTreeData)) {
html += `
<tr>
<td>${module}</td>
<td>${status}</td>
</tr>
`;
}
html += `</table>`;
statusTree.innerHTML = html;
}
socket.on("status_lib", (data) => {
console.log("lib status", data);
statusTreeData[data.lib] = data.status;
renderStatusTable();
});
socket.on("redirect", (data) => {
console.log("redirect", data);
location.replace(data.url);
});
socket.on("restarting", (data) => {
console.log("restarting", data);
location.reload()
});
socket.on("log", (data) => {
appendLog(data);
});
socket.on("error", (data) => {
appendLog(data);
});

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>NoSys Startup</title>
<style>
body { font-family: sans-serif; margin: 20px; }
ul { list-style-type: none; padding-left: 20px; }
li { margin: 5px 0; }
.status { font-weight: bold; }
</style>
</head>
<body style="background-color:black; color: white;">
<h1>NoSys Startup Status</h1>
<ul id="updater-tree"></ul>
<div id="log-container"></div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- <script src="/static/main.js"></script> -->
<script src="static/main.js"></script>
</body>
</html>

76
app/ui/ui_server.py Normal file
View File

@@ -0,0 +1,76 @@
from libs.app.common.logging import get_logger
from flask import Flask, render_template, request
from flask_socketio import SocketIO
import multiprocessing
import threading
import time
logger = get_logger()
class UIServer:
def __init__(self, host="127.0.0.1", port=5000):
self.host = host
self.port = port
self.process = None
self.queue = multiprocessing.Queue()
self.client_ready = multiprocessing.Value("b", False)
@staticmethod
def run_server(host, port, queue, client_ready):
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
@app.route("/")
def index():
return render_template("index.html")
@socketio.on("frontend_ready")
def frontend_ready():
client_ready.value = True
def queue_listener():
while True:
event, data = queue.get()
while not client_ready.value:
time.sleep(1)
socketio.emit(event, data)
threading.Thread(target=queue_listener, daemon=True).start()
socketio.run(app, host=host, port=port, debug=False, use_reloader=False)
def start(self):
if self.process and self.process.is_alive():
logger.debug("Server is already running")
return
self.process = multiprocessing.Process(
target=UIServer.run_server,
args=(self.host, self.port, self.queue, self.client_ready),
)
self.process.start()
logger.debug(f"Server started on http://{self.host}:{self.port}")
def stop(self, wait_queue=True, time_limit=5):
while wait_queue and not self.queue.empty():
logger.debug(f"Waiting server. {self.queue.qsize()} items in queue")
time.sleep(1)
time_limit-=1
if time_limit <= 0:
break
if self.process and self.process.is_alive():
self.process.terminate()
self.process.join()
logger.debug("Server stopped")
self.process = None
def restart(self):
logger.debug("Restarting server...")
self.stop()
self.start()
def emit_event(self, event, data={}):
self.queue.put((event, data))

135
app/updater.py Normal file
View File

@@ -0,0 +1,135 @@
import sys
import subprocess
import urllib.request
import os
import json
import types
import logging
import time
import zipfile
from pathlib import Path
from threading import Thread
from libs.app.common.config import Config
from libs.app.common.logging import get_logger
from libs.app.common.paths import LIBS_DIR
logger = get_logger("updater", buffer=True)
class Updater:
"""
Updater class responsible for managing library updates.
"""
def __init__(self, config:Config):
self.config = config
self.repositories = self.config.get("app", "repositories", default=[])
self.updated_libs: dict[str, any] = {}
self._event_listeners: list[callable] = []
def update_libs(self):
"""Check and update all configured libraries in parallel."""
logger.debug("Checking libraries updates...")
threads: list[Thread] = []
for lib in self.config.get("app", "libs", default=[]):
lib_id = lib.get("id")
self._update_lib_status(lib_id, "Reading configuration")
if lib.get("update", {"checkUpdates":True}).get("checkUpdates", True):
self._update_lib_status(lib_id, "Checking updates")
thread = Thread(target=self.check_lib_update, args=(lib,))
thread.start()
threads.append(thread)
else:
self._update_lib_status(lib_id, "Checking updates disabled")
for thread in threads:
thread.join()
return self.updated_libs
def check_lib_update(self, lib:dict):
"""
Check for a library update in repositories and install if newer version is available.
"""
lib_id = lib.get("id")
lib_config = self.config.get(lib_id)
lib_repositories = lib.get("update", {"repositories":[]}).get("repositories", [])
lib_path = LIBS_DIR / lib_id
if not lib_config:
Path(lib_path).mkdir(parents=True, exist_ok=True)
current_version = 0
else:
current_version = lib_config.get("info").get("version")
logger.debug(f"Checking updates of lib {lib_id} with current version {current_version}")
repositories = list(self.repositories)
repositories.extend(lib_repositories)
latest_version = (current_version, None)
for repo in repositories:
try:
info_url = f"{repo}/{lib_id}/info.json"
logger.debug(f"Reading remote info of {lib_id} from repository {repo}")
with urllib.request.urlopen(info_url, timeout=2) as response:
remote_info = json.load(response)
logger.debug(f"{lib_id} remote info: {remote_info}")
remote_version = remote_info.get("version")
if remote_version > latest_version[0]:
latest_version = (remote_version, f"{repo}/{lib_id}")
except Exception as e:
logger.error(f"Error reading remote info of {lib_id} from {repo}: {e}")
if latest_version[1]:
lib_url = f"{latest_version[1]}/{lib_id}.zip"
lib_file = LIBS_DIR / f"{lib_id}.zip"
logger.debug(f"Downloading lib from {lib_url} to {lib_file}")
self._update_lib_status(lib_id, f"Downloading lastest lib version {latest_version[0]}")
self.download_lib(lib_url, lib_file)
self._update_lib_status(lib_id, f"Installing lib requirements")
self.pip_install_requirements(lib_path)
self._update_lib_status(lib_id, f"Success updated")
else:
self._update_lib_status(lib_id, f"Current version is the latest")
def download_lib(self, url: str, path: str) -> None:
"""Download and extract a lib from repository."""
urllib.request.urlretrieve(url, path)
logger.info(f"Extracting all from {path}")
with zipfile.ZipFile(path, "r") as zip_ref:
zip_ref.extractall(Path(path).parent.absolute())
def pip_install_requirements(self, lib_path: str) -> None:
"""Run pip install on requirements.txt if present."""
requirements_path = os.path.join(lib_path, "requirements.txt")
if os.path.exists(requirements_path):
logger.info(f"Installing requirements from {requirements_path}")
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", requirements_path])
else:
logger.info(f"No requirements.txt found for {lib_path}")
def register_listener(self, callback: callable):
"""
Register a listener callback that will be called with
each new event.
"""
if callback not in self._event_listeners:
self._event_listeners.append(callback)
def emit_event(self, **kwargs):
event = types.SimpleNamespace()
for k, v in kwargs.items():
setattr(event, k, v)
for listener in self._event_listeners:
listener(event)
def _update_lib_status(self, lib, status):
self.updated_libs[lib] = status
self.emit_event(name="status_lib", lib=lib, status=status)

257
app/utilss.py Normal file
View File

@@ -0,0 +1,257 @@
import os, sys
import uuid
import pathlib
import json
import subprocess
root_dir = os.path.normpath(__file__.split("libs")[0])
node_id = str(uuid.uuid4())
### LOGGER ###
import logging, inspect
class CustomLoggingFormatter(logging.Formatter):
grey = "\x1b[0;37m"
green = "\x1b[1;32m"
yellow = "\x1b[1;33m"
red = "\x1b[1;31m"
purple = "\x1b[1;35m"
blue = "\x1b[1;34m"
light_blue = "\x1b[1;36m"
bold_red = "\x1b[31;1m"
blink_red = "\x1b[5m\x1b[1;31m"
reset = "\x1b[0m"
prefix = light_blue + '%(asctime)s' + reset + ' |'
colored_level = '%(levelname)-8s'
message = '| %(message)s' + ''
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
FORMATS = {
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix
}
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
# TODO Dinamicaly get caller lib path
# class FileFilter(logging.Filter):
# def filter(self, record):
# file_path = os.path.normpath(inspect.stack()[1].filename)
# head = os.path.split(file_path)[0]
# tail = os.path.split(file_path)[1]
# if head == os.path.normpath(root_dir):
# name = "app"
# else:
# after_lib = head.replace(os.path.join(root_dir, "libs"), "")
# name = after_lib.split(os.sep)[1]
# record.name = name
# print(name)
# return True
def add_logging_console_handler(logger:logging.Logger, logging_level=logging.DEBUG):
sh = logging.StreamHandler()
sh.setLevel(logging_level)
sh.setFormatter(CustomLoggingFormatter())
logger.addHandler(sh)
def add_logging_file_handler(logger:logging.Logger, directory, file_name, logging_level=logging.DEBUG):
file_path = os.path.join(root_dir, "logs", directory, f"{file_name}.log")
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
fh = logging.FileHandler(file_path)
fh.setLevel(logging_level)
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
logger.addHandler(fh)
def config_root_logger():
logger = get_logger(name="root")
# TODO Get lib that is calling logging
# logger.addFilter(FileFilter())
def get_logger(name=""):
file_path = os.path.normpath(inspect.stack()[1].filename)
head = os.path.split(file_path)[0]
tail = os.path.split(file_path)[1]
if head == os.path.normpath(root_dir):
lib = "root"
else:
after_lib = head.replace(os.path.join(root_dir, "libs"), "")
lib = after_lib.split(os.sep)[1]
if name == "root":
logger_name = name
elif name:
logger_name = f"{lib}.{name}"
else:
logger_name = lib
file_name = name if name else lib
logger = logging.getLogger(logger_name)
logger.propagate = False
if not logger.hasHandlers():
logger.setLevel(logging.DEBUG)
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
return logger
logger = get_logger()
logger.debug(f"Root dir: {root_dir}")
### VENV ###
import venv
def create_venv():
venv_dir = os.path.join(root_dir, ".venv")
if not os.path.exists(venv_dir):
logger.debug(f"Creating python venv: {venv_dir}")
venv.create(venv_dir, with_pip=True)
### DOWNLOADS ###
import urllib.request
def download_file(url, path):
head = os.path.split(path)[0]
if not os.path.exists(head):
logger.debug(f"Creating repository {head}")
pathlib.Path(head).mkdir(parents=True, exist_ok=True)
logger.debug(f"Downloading from URL: {url} to {path}")
urllib.request.urlretrieve(url, path)
### KARGS ###
def read_kargs():
kargs = {}
params = sys.argv
for param in params:
if "=" in param:
split = param.split("=")
key = split[0]
value = split[1]
if value == "true" or value == "True":
value = True
elif value == "false" or value == "False":
value = False
kargs[key] = value
kargs["rootDir"] = root_dir
return kargs
def kargs_to_array(kargs={}):
if not kargs:
kargs = read_kargs()
array = []
for key, value in kargs.items():
array.append(f"{key}={value}")
return array
### HTTP UTILS ###
def is_http_running(url):
try:
u:urllib.request.URLopener = urllib.request.urlopen(url)
u.close()
return True
except:
return False
### UTILS ###
class Utils:
def __init__(self):
self.kargs = read_kargs()
self.configs = Config()
self.flags = {}
# def get_env():
# return os.environ.get('NOSYS_ENV', "PROD")
# def download_file(self, package, file, destination, repository=None):
# if not repository:
# repository = self.default_repository
# url = f"{repository}/{package}/{file}"
# logger.debug(f"Downloading {file} from URL: {url}")
# urllib.request.urlretrieve(url, destination)
# def download_main_file(self, ignoreIfExists=True):
# main_path = os.path.join(self.root_dir, "libs","app", "main.py")
# if (not os.path.exists(main_path)) or (os.path.exists(main_path) and not ignoreIfExists):
# self.download_file(package="app", file="main.py", destination=main_path)
def is_terminal_visible(self):
try:
return self.configs.data["main"]["terminalWindow"]
except Exception as e:
return True
# def _create_log_dir(self):
# logs_path = os.path.join(self.root_dir, "logs")
# if not os.path.exists(logs_path):
# logger.debug(f"Creating logs directory: {logs_path}")
# pathlib.Path(logs_path).mkdir(parents=True, exist_ok=True)
def new_python_process(self, file_path, args):
python_executable = "python" if self.is_terminal_visible() else "pythonw"
args = [f"{root_dir}/.venv/scripts/{python_executable}", file_path] + args
logger.debug(f"Starting a new process: {args}")
process = subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
logger.debug(f"Process PID {process.pid} - {args}")
return process.pid
def restart_app(self):
args = [sys.executable, "start.py"] + kargs_to_array(self.kargs)
subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
logger.info(f"Restarting app: {args}")
self.exit_app()
def exit_app(self, exit_code=1):
os._exit(exit_code)
# sys.exit(exit_code)
class Config:
def __init__(self):
self.data = None
self.packages:dict[str, Package] = {}
self.read_config()
def read_config(self):
config_path = os.path.join(root_dir, "libs", "app", "config.json")
if(os.path.exists(config_path)):
with open(config_path) as f:
data = json.load(f)
self.data = data
self.read_packages()
else:
raise Exception(f"Config file {config_path} not exist")
def read_packages(self):
for package in self.data["packages"]:
try:
with open(os.path.join(root_dir,'libs',package["id"],'info.json')) as f:
self.packages[package["id"]] = Package(config=package, info=json.load(f))
except Exception as e:
self.packages[package["id"]] = Package(config=package, info={})
logger.error(e)
class Package:
def __init__(self, config={}, info={}):
self.config = config
self.info = info
self.modules:dict[str, any] = {}
self.read_modules()
def read_modules(self):
if "modules" in self.info:
for module in self.info["modules"]:
self.modules[module["id"]] = module