143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
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)
|