ppt-web / run.py
26fwyzpz6f-max
Clean deploy without binary files
6aecb2e
Raw
History Blame Contribute Delete
6.91 kB
#!/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()