Saba-python/main.py
2026-03-23 19:29:24 +03:30

277 lines
8.3 KiB
Python

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.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())