2025-12-28 12:34:28 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import sys # 核心修复:新增sys模块导入
|
2022-06-03 16:00:33 +08:00
|
|
|
|
import threading
|
2025-12-28 12:34:28 +08:00
|
|
|
|
from multiprocessing import Event, Process, set_start_method
|
|
|
|
|
|
from typing import Optional
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2023-02-08 16:37:11 +08:00
|
|
|
|
from module.logger import logger
|
2022-06-04 17:40:26 +08:00
|
|
|
|
from module.webui.setting import State
|
|
|
|
|
|
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2025-12-28 12:34:28 +08:00
|
|
|
|
def func(ev: Optional[Event]): # 修正:改为multiprocessing.Event(Optional适配None传参)
|
2022-06-03 16:00:33 +08:00
|
|
|
|
import argparse
|
2022-06-03 23:00:54 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
|
2022-06-03 16:00:33 +08:00
|
|
|
|
import uvicorn
|
2022-06-03 23:00:54 +08:00
|
|
|
|
|
2025-12-28 12:34:28 +08:00
|
|
|
|
# 修复:macOS下asyncio兼容配置(补充)
|
|
|
|
|
|
if sys.platform == "darwin":
|
|
|
|
|
|
# 禁用fork安全检查,解决Mach端口冲突
|
|
|
|
|
|
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
|
|
|
|
# macOS下asyncio事件循环兼容
|
|
|
|
|
|
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
|
|
|
|
|
|
elif sys.platform.startswith("win"):
|
2022-06-03 23:00:54 +08:00
|
|
|
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
|
|
|
|
|
|
2022-06-25 20:04:45 +08:00
|
|
|
|
State.restart_event = ev
|
2022-06-03 16:00:33 +08:00
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Alas web service")
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--host",
|
|
|
|
|
|
type=str,
|
|
|
|
|
|
help="Host to listen. Default to WebuiHost in deploy setting",
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"--port",
|
|
|
|
|
|
type=int,
|
|
|
|
|
|
help="Port to listen. Default to WebuiPort in deploy setting",
|
|
|
|
|
|
)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"-k", "--key", type=str, help="Password of alas. No password by default"
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--cdn",
|
|
|
|
|
|
action="store_true",
|
|
|
|
|
|
help="Use jsdelivr cdn for pywebio static files (css, js). Self host cdn by default.",
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--electron", action="store_true", help="Runs by electron client."
|
|
|
|
|
|
)
|
2025-06-05 15:36:17 +08:00
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--ssl-key", dest="ssl_key", type=str, help="SSL key file path for HTTPS support"
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--ssl-cert", type=str, help="SSL certificate file path for HTTPS support"
|
|
|
|
|
|
)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--run",
|
|
|
|
|
|
nargs="+",
|
|
|
|
|
|
type=str,
|
|
|
|
|
|
help="Run alas by config names on startup",
|
|
|
|
|
|
)
|
2022-01-08 21:45:05 +08:00
|
|
|
|
args, _ = parser.parse_known_args()
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2022-06-03 21:28:44 +08:00
|
|
|
|
host = args.host or State.deploy_config.WebuiHost or "0.0.0.0"
|
|
|
|
|
|
port = args.port or int(State.deploy_config.WebuiPort) or 22267
|
2025-06-05 15:36:17 +08:00
|
|
|
|
ssl_key = args.ssl_key or State.deploy_config.WebuiSSLKey
|
|
|
|
|
|
ssl_cert = args.ssl_cert or State.deploy_config.WebuiSSLCert
|
|
|
|
|
|
ssl = ssl_key is not None and ssl_cert is not None
|
2023-02-08 16:37:11 +08:00
|
|
|
|
State.electron = args.electron
|
|
|
|
|
|
|
|
|
|
|
|
logger.hr("Launcher config")
|
|
|
|
|
|
logger.attr("Host", host)
|
|
|
|
|
|
logger.attr("Port", port)
|
2025-06-05 15:36:17 +08:00
|
|
|
|
logger.attr("SSL", ssl)
|
2023-02-08 16:37:11 +08:00
|
|
|
|
logger.attr("Electron", args.electron)
|
|
|
|
|
|
logger.attr("Reload", ev is not None)
|
|
|
|
|
|
|
|
|
|
|
|
if State.electron:
|
|
|
|
|
|
# https://github.com/LmeSzinc/AzurLaneAutoScript/issues/2051
|
|
|
|
|
|
logger.info("Electron detected, remove log output to stdout")
|
|
|
|
|
|
from module.logger import console_hdlr
|
|
|
|
|
|
logger.removeHandler(console_hdlr)
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2025-06-05 15:36:17 +08:00
|
|
|
|
if ssl_cert is None and ssl_key is not None:
|
|
|
|
|
|
logger.error("SSL key provided without certificate. Please provide both SSL key and certificate.")
|
|
|
|
|
|
elif ssl_key is None and ssl_cert is not None:
|
|
|
|
|
|
logger.error("SSL certificate provided without key. Please provide both SSL key and certificate.")
|
|
|
|
|
|
|
2025-12-28 12:34:28 +08:00
|
|
|
|
try: # 新增:捕获uvicorn运行时异常,避免子进程崩溃无日志
|
|
|
|
|
|
if ssl:
|
|
|
|
|
|
uvicorn.run(
|
|
|
|
|
|
"module.webui.app:app",
|
|
|
|
|
|
host=host,
|
|
|
|
|
|
port=port,
|
|
|
|
|
|
factory=True,
|
|
|
|
|
|
ssl_keyfile=ssl_key,
|
|
|
|
|
|
ssl_certfile=ssl_cert
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
uvicorn.run("module.webui.app:app", host=host, port=port, factory=True)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Uvicorn service crashed: {str(e)}")
|
|
|
|
|
|
raise # 抛出异常让父进程感知
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2022-06-03 16:00:33 +08:00
|
|
|
|
if __name__ == "__main__":
|
2025-12-28 12:34:28 +08:00
|
|
|
|
# 核心修复:强制设置multiprocessing启动方式为spawn(解决macOS fork导致的Mach端口崩溃)
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 优先设置spawn,兼容多平台
|
|
|
|
|
|
set_start_method("spawn", force=True)
|
|
|
|
|
|
# macOS下额外添加环境变量,禁用fork安全检查
|
|
|
|
|
|
if os.name == "posix" and sys.platform == "darwin":
|
|
|
|
|
|
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
logger.warning("Failed to set spawn start method, may use fork (not recommended on macOS)")
|
|
|
|
|
|
|
2022-06-04 17:40:26 +08:00
|
|
|
|
if State.deploy_config.EnableReload:
|
|
|
|
|
|
should_exit = False
|
2022-06-03 18:54:54 +08:00
|
|
|
|
while not should_exit:
|
2022-06-04 17:40:26 +08:00
|
|
|
|
event = Event()
|
2024-05-04 16:26:56 +08:00
|
|
|
|
process = Process(target=func, args=(event,), name="gui")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
process.start()
|
2025-12-28 12:34:28 +08:00
|
|
|
|
logger.info(f"Started Alas web service (PID: {process.pid})")
|
|
|
|
|
|
|
2022-06-04 17:40:26 +08:00
|
|
|
|
while not should_exit:
|
2022-07-26 15:33:46 +08:00
|
|
|
|
try:
|
2025-12-28 12:34:28 +08:00
|
|
|
|
# 等待重启事件(1秒超时,避免阻塞)
|
|
|
|
|
|
restart_triggered = event.wait(1)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
except KeyboardInterrupt:
|
2025-12-28 12:34:28 +08:00
|
|
|
|
logger.info("Received KeyboardInterrupt, exiting...")
|
|
|
|
|
|
should_exit = True
|
|
|
|
|
|
break
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error waiting for restart event: {str(e)}")
|
2022-07-26 15:33:46 +08:00
|
|
|
|
should_exit = True
|
|
|
|
|
|
break
|
2025-12-28 12:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
if restart_triggered:
|
|
|
|
|
|
logger.info("Restart event triggered, killing current service...")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
process.kill()
|
2025-12-28 12:34:28 +08:00
|
|
|
|
process.join(timeout=5) # 新增:等待子进程退出,避免僵尸进程
|
|
|
|
|
|
if process.is_alive():
|
|
|
|
|
|
logger.warning("Failed to kill service process, force exit")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
break
|
2025-12-28 12:34:28 +08:00
|
|
|
|
elif not process.is_alive():
|
|
|
|
|
|
logger.error("Alas web service exited unexpectedly")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
should_exit = True
|
2025-12-28 12:34:28 +08:00
|
|
|
|
# 进程仍存活则继续循环
|
|
|
|
|
|
|
|
|
|
|
|
# 确保子进程完全退出
|
|
|
|
|
|
if process.is_alive():
|
|
|
|
|
|
process.terminate()
|
|
|
|
|
|
process.join(timeout=3)
|
|
|
|
|
|
|
|
|
|
|
|
# 最终清理:确保子进程退出
|
|
|
|
|
|
if process.is_alive():
|
|
|
|
|
|
process.kill()
|
|
|
|
|
|
process.join()
|
|
|
|
|
|
logger.info("Alas web service exited successfully")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
else:
|
2025-12-28 12:34:28 +08:00
|
|
|
|
# 非重启模式直接运行
|
2022-06-04 17:40:26 +08:00
|
|
|
|
func(None)
|