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)