import os import subprocess import sys from importlib.util import find_spec from pathlib import Path from tkinter import TclError BASE_DIR = Path(__file__).resolve().parent REQUIREMENTS_FILE = BASE_DIR / "requirements.txt" PROJECT_VENV_DIR = BASE_DIR / ".runtime-venv" REQUIRED_IMPORTS = { "customtkinter": "customtkinter", "cryptography": "cryptography", "pyserial": "serial", "arabic-reshaper": "arabic_reshaper", "python-bidi": "bidi", } def _project_venv_python() -> Path: if os.name == "nt": return PROJECT_VENV_DIR / "Scripts" / "python.exe" return PROJECT_VENV_DIR / "bin" / "python" def _running_inside_virtualenv() -> bool: return sys.prefix != getattr(sys, "base_prefix", sys.prefix) or hasattr(sys, "real_prefix") def _missing_requirements() -> list[str]: missing = [] for package_name, import_name in REQUIRED_IMPORTS.items(): if find_spec(import_name) is None: missing.append(package_name) return missing def _missing_requirements_for_python(python_executable: str) -> tuple[list[str] | None, str | None]: check_script = "\n".join( [ "from importlib.util import find_spec", f"required = {REQUIRED_IMPORTS!r}", "missing = [name for name, import_name in required.items() if find_spec(import_name) is None]", "print('\\n'.join(missing))", ] ) ok, output = _run_subprocess( [python_executable, "-c", check_script], "Checking project virtual environment", ) if not ok: return None, output return [line.strip() for line in output.splitlines() if line.strip()], None def _run_subprocess(command: list[str], description: str) -> tuple[bool, str]: try: completed = subprocess.run( command, check=False, capture_output=True, text=True, ) except Exception as exc: return False, f"{description} failed to start: {exc}" if completed.returncode == 0: return True, completed.stdout.strip() details = completed.stderr.strip() or completed.stdout.strip() or "Unknown error" return False, f"{description} failed: {details}" def _ensure_pip_available(python_executable: str) -> tuple[bool, str | None]: ok, message = _run_subprocess( [python_executable, "-m", "pip", "--version"], "Checking pip", ) if ok: return True, None ok, ensure_message = _run_subprocess( [python_executable, "-m", "ensurepip", "--upgrade"], "Bootstrapping pip", ) if ok: return True, None return False, ensure_message or message def _install_requirements_with_python(python_executable: str) -> tuple[bool, str | None]: if not REQUIREMENTS_FILE.exists(): return False, f"Requirements file not found: {REQUIREMENTS_FILE}" pip_ready, pip_message = _ensure_pip_available(python_executable) if not pip_ready: return False, pip_message print("Installing missing Python packages...", flush=True) ok, message = _run_subprocess( [ python_executable, "-m", "pip", "install", "--disable-pip-version-check", "-r", str(REQUIREMENTS_FILE), ], "Installing requirements", ) if not ok: return False, message return True, None def _create_project_venv() -> tuple[bool, str | None]: venv_python = _project_venv_python() if venv_python.exists(): return True, None print(f"Creating local virtual environment in {PROJECT_VENV_DIR}...", flush=True) ok, message = _run_subprocess( [sys.executable, "-m", "venv", str(PROJECT_VENV_DIR)], "Creating virtual environment", ) if ok and venv_python.exists(): return True, None help_message = "\n".join( [ message or "Creating virtual environment failed.", "", "On Raspberry Pi / Debian, install venv support first:", " sudo apt install python3-venv python3-full", ] ) return False, help_message def _restart_inside_project_venv() -> tuple[bool, str | None]: venv_python = _project_venv_python() if not venv_python.exists(): return False, f"Virtual environment Python was not found: {venv_python}" print("Restarting app inside the local virtual environment...", flush=True) argv = [str(venv_python), str(BASE_DIR / "main.py"), *sys.argv[1:]] env = os.environ.copy() env["SECURE_SMS_RUNTIME_VENV"] = str(PROJECT_VENV_DIR) try: os.execve(str(venv_python), argv, env) except Exception as exc: return False, f"Restarting inside virtual environment failed: {exc}" return True, None def _ensure_runtime_dependencies() -> tuple[bool, str | None]: missing = _missing_requirements() if not missing: return True, None print(f"Missing packages detected: {', '.join(missing)}", flush=True) if _running_inside_virtualenv(): ok, message = _install_requirements_with_python(sys.executable) if not ok: return False, message else: venv_python = _project_venv_python() if venv_python.exists(): venv_missing, venv_message = _missing_requirements_for_python(str(venv_python)) if venv_message: return False, venv_message if not venv_missing: ok, message = _restart_inside_project_venv() if not ok: return False, message return False, "Unexpected bootstrap state while switching to the local virtual environment." ok, message = _create_project_venv() if not ok: return False, message ok, message = _install_requirements_with_python(str(venv_python)) if not ok: return False, message ok, message = _restart_inside_project_venv() if not ok: return False, message return False, "Unexpected bootstrap state while switching to the local virtual environment." remaining = _missing_requirements() if remaining: return False, f"Packages are still missing after installation: {', '.join(remaining)}" return True, None def _prepare_linux_display() -> tuple[bool, str | None]: if not sys.platform.startswith("linux"): return True, None display = os.environ.get("DISPLAY") if display: return True, None x11_socket = Path("/tmp/.X11-unix/X0") if x11_socket.exists(): os.environ["DISPLAY"] = ":0" return True, None message = "\n".join( [ "No GUI display was found for Tkinter.", "This app needs a graphical desktop session on Raspberry Pi.", "", "If you are on the Pi screen, start the desktop first and then run the app.", "If you run it from SSH/systemd, usually you need:", " DISPLAY=:0", " XAUTHORITY=/home/pars/.Xauthority", "", "Example:", " export DISPLAY=:0", " export XAUTHORITY=/home/pars/.Xauthority", " python3 main.py", ] ) return False, message def _display_error_message(exc: Exception) -> str: return "\n".join( [ f"GUI startup failed: {exc}", "", "Tkinter could not connect to the Raspberry Pi graphical session.", "Run the app from the Pi desktop session or set DISPLAY/XAUTHORITY correctly.", "", "Typical Raspberry Pi values:", " DISPLAY=:0", " XAUTHORITY=/home/pars/.Xauthority", ] ) def main(): deps_ready, deps_message = _ensure_runtime_dependencies() if not deps_ready: print(deps_message, file=sys.stderr) return 1 ready, message = _prepare_linux_display() if not ready: print(message, file=sys.stderr) return 1 try: from secure_sms.application.controller import AppController from secure_sms.ui import SecureSmsApp controller = AppController() app = SecureSmsApp(controller) controller.bind_ui(app) app.protocol("WM_DELETE_WINDOW", controller.shutdown) app.mainloop() return 0 except TclError as exc: print(_display_error_message(exc), file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())