Added libs
This commit is contained in:
4
app/.gitignore
vendored
Normal file
4
app/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.log
|
||||
app.zip
|
||||
1
app/.mtimes.json
Normal file
1
app/.mtimes.json
Normal 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
4
app/BKP/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.log
|
||||
app.zip
|
||||
75
app/BKP/config.json
Normal file
75
app/BKP/config.json
Normal 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
5
app/BKP/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id":"app",
|
||||
"version":0,
|
||||
"modules":[]
|
||||
}
|
||||
70
app/BKP/main.py
Normal file
70
app/BKP/main.py
Normal 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
59
app/BKP/start.py
Normal 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
124
app/BKP/updater.py
Normal 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
256
app/BKP/utils.py
Normal 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
51
app/common/args.py
Normal 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
56
app/common/config.py
Normal 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
173
app/common/logging.py
Normal 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")
|
||||
25
app/common/network_utils.py
Normal file
25
app/common/network_utils.py
Normal 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
8
app/common/paths.py
Normal 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
97
app/common/process.py
Normal 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
75
app/common/store.py
Normal 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
44
app/config.json
Normal 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
5
app/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "app",
|
||||
"version": 0.111,
|
||||
"modules": []
|
||||
}
|
||||
117
app/main.py
Normal file
117
app/main.py
Normal 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
4
app/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
requests
|
||||
Flask
|
||||
flask-socketio
|
||||
pywebview
|
||||
142
app/start.py
Normal file
142
app/start.py
Normal 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
65
app/ui/static/main.js
Normal 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);
|
||||
});
|
||||
22
app/ui/templates/index.html
Normal file
22
app/ui/templates/index.html
Normal 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
76
app/ui/ui_server.py
Normal 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
135
app/updater.py
Normal 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
257
app/utilss.py
Normal 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
|
||||
Reference in New Issue
Block a user