#!/usr/bin/env python3 """ LandPPT Application Runner This script starts the LandPPT FastAPI application with proper configuration. """ import uvicorn import sys import os import inspect import threading import time import webbrowser import socket import traceback from pathlib import Path from dotenv import load_dotenv # Add src to Python path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) # Load environment variables with error handling try: load_dotenv() except PermissionError as e: print(f"Warning: Could not load .env file due to permission error: {e}") print("Continuing with system environment variables...") except Exception as e: print(f"Warning: Could not load .env file: {e}") print("Continuing with system environment variables...") def _log_startup_error(exc: BaseException) -> None: """Write startup errors to local log files for packaged runs.""" log_paths = [] try: log_paths.append(Path.cwd() / "landppt_startup_error.log") except Exception: pass try: log_paths.append(Path(__file__).resolve().with_name("landppt_startup_error.log")) except Exception: pass if getattr(sys, "frozen", False): try: log_paths.append(Path(sys.executable).resolve().with_name("landppt_startup_error.log")) except Exception: pass seen = set() for log_path in log_paths: if not log_path or str(log_path) in seen: continue seen.add(str(log_path)) try: with log_path.open("a", encoding="utf-8") as f: f.write("\n" + "=" * 80 + "\n") f.write(time.strftime("%Y-%m-%d %H:%M:%S") + " Startup error\n") f.write("=" * 80 + "\n") f.write("Executable: " + str(getattr(sys, "executable", "")) + "\n") f.write("CWD: " + os.getcwd() + "\n") f.write("Frozen: " + str(getattr(sys, "frozen", False)) + "\n") f.write("\n") f.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))) f.write("\n") except Exception: pass def _open_browser_later(url: str, host: str, port: int, timeout_seconds: float = 45.0) -> None: """Open the LandPPT workspace only after the local server accepts connections.""" def _open() -> None: deadline = time.time() + timeout_seconds while time.time() < deadline: try: with socket.create_connection((host, port), timeout=1.0): webbrowser.open(url) return except OSError: time.sleep(0.5) print(f"Warning: Server did not become ready within {timeout_seconds:.0f}s; opening browser anyway.") try: webbrowser.open(url) except Exception as e: print(f"Warning: Could not open browser automatically: {e}") threading.Thread(target=_open, daemon=True).start() def main(): """Main entry point for running the application""" # Get configuration from environment variables with defaults host = os.getenv("HOST", "0.0.0.0") port = int(os.getenv("PORT", "7860")) # PyInstaller packaged executables should not use uvicorn reload mode. default_reload = "false" if getattr(sys, "frozen", False) else "true" reload = os.getenv("RELOAD", default_reload).lower() in ("true", "1", "yes", "on") log_level = os.getenv("LOG_LEVEL", "info").lower() workers = int(os.getenv("WORKERS", "1")) # Uvicorn's multi-worker supervisor has a watchdog that can SIGTERM "unresponsive" workers. # Long-running background tasks (like video export) can temporarily block/slow the event loop, # so we use a higher default to avoid killing workers during heavy exports. try: timeout_worker_healthcheck = int(os.getenv("UVICORN_TIMEOUT_WORKER_HEALTHCHECK", "60")) except Exception: timeout_worker_healthcheck = 60 timeout_worker_healthcheck = max(5, timeout_worker_healthcheck) # PyInstaller packaged executables must stay in a single process. Uvicorn reload/workers # require an import string and may exit with SystemExit(3) in frozen apps. if getattr(sys, "frozen", False): reload = False workers = 1 # Workers and reload cannot be combined; prefer workers when explicitly set if workers > 1 and reload: reload = False # Configuration app_target = "landppt.main:app" if getattr(sys, "frozen", False): # In PyInstaller builds, string-based uvicorn imports may fail with SystemExit(3). # Import the ASGI app directly so packaged executables do not depend on uvicorn's import string resolver. from landppt.main import app as app_target config = { "app": app_target, "host": host, "port": port, "reload": reload, "log_level": log_level, "access_log": True, } if not getattr(sys, "frozen", False) and "timeout_worker_healthcheck" in inspect.signature(uvicorn.run).parameters: config["timeout_worker_healthcheck"] = timeout_worker_healthcheck if workers > 1: config["workers"] = workers print("Starting LandPPT Server...") print(f"Host: {config['host']}") print(f"Port: {config['port']}") print(f"Reload: {config['reload']}") print(f"Log Level: {config['log_level']}") print(f"Workers: {config.get('workers', 1)}") browser_url = f"http://127.0.0.1:{config['port']}" print(f"Server will be available at: {browser_url}") print(f"Web Interface: {browser_url}/web") print("=" * 60) if os.getenv("LANDPPT_OPEN_BROWSER", "true").lower() in ("true", "1", "yes", "on"): _open_browser_later(browser_url, "127.0.0.1", config['port']) try: if getattr(sys, "frozen", False): server_config = uvicorn.Config( app=config["app"], host=config["host"], port=config["port"], log_level=config["log_level"], access_log=config["access_log"], ) server = uvicorn.Server(server_config) server.run() else: uvicorn.run(**config) except KeyboardInterrupt: print("\nServer stopped by user") except BaseException as e: if isinstance(e, KeyboardInterrupt): raise _log_startup_error(e) print(f"Error starting server: {e}") print("Detailed traceback has been written to landppt_startup_error.log") if getattr(sys, "frozen", False) or os.getenv("LANDPPT_PAUSE_ON_ERROR", "false").lower() in ("true", "1", "yes", "on"): try: input("Press Enter to exit...") except Exception: pass sys.exit(1) if __name__ == "__main__": main()