Files

179 lines
7.1 KiB
Python

"""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://<LAN-IP>: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()