"""ScoreFX Studio — Desktop engine launcher (PyInstaller entry point). Chay FastAPI backend duoi dang tien trinh nen (sidecar) cho ban cai Standalone Windows (Tauri v2 shell). Tu chon port 8000-8010 (fallback neu bi chiem), tu thoat khi tien trinh cha (Tauri) ket thuc — Graceful Shutdown. """ import logging import os import socket import sys import threading import time PORT_RANGE = (8000, 8010) APP_DATA_DIR_NAME = "SonicForgeDAW" def _pick_port(): for port in range(PORT_RANGE[0], PORT_RANGE[1] + 1): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: # KHONG dung SO_REUSEADDR: tren Windows no CHO PHEP bind trung port # (2 engine cung 8000 -> request roi vao engine ngau nhien -> UI loi). # Listen socket khong can SO_REUSEADDR (TIME_WAIT chi ap dung cho # connection socket, khong phai listen socket). # Bind 0.0.0.0 (GIONG uvicorn) — kiem tra dung port ma server se # dung, truong hop port bi chiem tren interface khac khong sot. try: s.bind(("0.0.0.0", port)) return port except OSError: continue return PORT_RANGE[0] def _lan_ips(): """IPv4 cua may tren LAN (bo loopback) — client browser o may khac mo app.""" ips = [] try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] if ip and not ip.startswith("127.") and ip not in ips: ips.append(ip) except OSError: pass try: for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): ip = info[4][0] if ip and not ip.startswith("127.") and ip not in ips: ips.append(ip) except OSError: pass return ips def _parent_alive(pid): # Windows: os.kill(pid, 0) KHONG kiem tra ton tai — no goi # TerminateProcess (giai thich: moi sig khac CTRL_C/BREAK deu terminate). # => watchdog se GIET LUON tien trinh cha (Tauri) sau 1s dau tien. # Dung OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION): tra ve NULL khi # pid khong con ton tai. if os.name == "nt": import ctypes # OpenProcess chi bao NULL khi process object da bi reap (moi handle # dong het). Process vua bi kill (taskkill/crash) con "zombie" mot # thoi gian -> OpenProcess van thanh cong -> watchdog tuong cha con # song -> engine khong bao gio tu thoat. WaitForSingleObject tren # handle co PROCESS_SYNCHRONIZE phan biet dung: object da terminate # thi SIGNALED ngay, du handle con ton tai. PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 PROCESS_SYNCHRONIZE = 0x00100000 h = ctypes.windll.kernel32.OpenProcess( PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE, False, pid) if not h: return False # process object da mat -> cha da chet terminated = ctypes.windll.kernel32.WaitForSingleObject(h, 0) == 0 ctypes.windll.kernel32.CloseHandle(h) return not terminated try: os.kill(pid, 0) return True except OSError: return False def main(): # PHONG THU (fix ban 1.1.2): neu exe bi goi nhu 'python -c ...' — vd code # cu kiem tra thu vien bang subprocess.run([sys.executable, '-c', 'import X']) # thi frozen sys.executable = daw_engine.exe -> se chay CA ENGINE o day. # Thoat ngay, KHONG khoi dong server, tranh de quy spawn vo han. if len(sys.argv) > 1 and sys.argv[1] == "-c": return os.environ.setdefault("SF_DESKTOP", "1") # Log dir tao SOM de stderr co the tro vao file (xem duoi). log_dir = os.path.join( os.environ.get("APPDATA") or os.path.expanduser("~"), APP_DATA_DIR_NAME, "logs", ) try: os.makedirs(log_dir, exist_ok=True) except OSError: log_dir = os.path.expanduser("~") # Windowed app (engine.spec console=False): Windows khong tao console -> # sys.stdout/sys.stderr = None -> uvicorn logging crash # ('NoneType' object has no attribute 'isatty'). Tro stdout/stderr vao # FILE (KHONG phai devnull) — neu devnull thi moi exception bi nuot im # lang (trieu chung 'stuck khong logs, khong loi'). if sys.stdout is None: sys.stdout = open(os.path.join(log_dir, "stdout.log"), "w", encoding="utf-8") if sys.stderr is None: sys.stderr = open(os.path.join(log_dir, "stderr.log"), "w", encoding="utf-8") port = _pick_port() os.environ["SF_PORT"] = str(port) # Log ra file — khi dong goi console=False, stdout khong nhin thay duoc. try: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", handlers=[ logging.FileHandler(os.path.join(log_dir, "engine.log"), encoding="utf-8"), logging.StreamHandler(), ], ) except Exception: logging.basicConfig(level=logging.INFO) import uvicorn import app.main # noqa: F401 — import tuong minh de PyInstaller bundle du package app # Bind 0.0.0.0: client browser o may khac tren LAN mo app qua dia chi IP # (http://:port) — lam viec realtime voi may chay standalone. # Cua so Tauri (shell) van mo http://127.0.0.1:port nhu cu. config = uvicorn.Config(app.main.app, host="0.0.0.0", port=port, log_level="info", access_log=False) server = uvicorn.Server(config) parent_pid = os.environ.get("SF_PARENT_PID") if parent_pid: def _watchdog(): pid = int(parent_pid) while True: time.sleep(1.0) if not _parent_alive(pid): logging.getLogger("desktop_engine").info( "Parent process exited — shutting down engine") server.should_exit = True # Graceful shutdown co the TREO vo han: uvicorn # _wait_tasks_to_complete cho open connection (client LAN # dang mo browser) va timeout_graceful_shutdown=None. # => engine song dai khi app chet dot ngot (crash/Task # Manager). Hard-exit fallback sau 5s dam bao thoat het. def _force_exit(): time.sleep(5.0) logging.getLogger("desktop_engine").info( "Graceful shutdown hung — forcing exit") os._exit(0) threading.Thread(target=_force_exit, daemon=True).start() return threading.Thread(target=_watchdog, daemon=True).start() logger = logging.getLogger("desktop_engine") logger.info("ScoreFX engine listening on 0.0.0.0:%d", port) lan_ips = _lan_ips() if lan_ips: logger.info("LAN access — mo browser o may khac: %s", " | ".join("http://%s:%d" % (ip, port) for ip in lan_ips)) else: logger.warning("Khong phat hien dia chi LAN — client may khac khong truy cap duoc") server.run() if __name__ == "__main__": main()