2025-12-28 12:34:28 +08:00
|
|
|
|
import os
|
2026-03-08 22:30:55 +09:00
|
|
|
|
import 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
|
|
|
|
|
2026-04-03 22:23:35 +08:00
|
|
|
|
if sys.platform != "win32":
|
|
|
|
|
|
import resource
|
|
|
|
|
|
try:
|
|
|
|
|
|
_soft, _hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
|
|
|
|
|
_target = 65536 if _hard == resource.RLIM_INFINITY else min(65536, _hard)
|
|
|
|
|
|
if _soft < _target:
|
|
|
|
|
|
resource.setrlimit(resource.RLIMIT_NOFILE, (_target, _hard))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
def func(ev: Optional[Event]):
|
|
|
|
|
|
"""
|
|
|
|
|
|
主函数:运行Web服务。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
ev: 可选的重启事件,用于热重载功能
|
|
|
|
|
|
"""
|
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
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 平台特定的asyncio配置
|
2025-12-28 12:34:28 +08:00
|
|
|
|
if sys.platform == "darwin":
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# macOS: 禁用fork安全检查以避免Mach端口冲突
|
2025-12-28 12:34:28 +08:00
|
|
|
|
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
|
|
|
|
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
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 解析命令行参数
|
2022-06-03 16:00:33 +08:00
|
|
|
|
parser = argparse.ArgumentParser(description="Alas web service")
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--host",
|
|
|
|
|
|
type=str,
|
2026-03-08 22:30:55 +09:00
|
|
|
|
help="监听主机。默认使用部署设置中的WebuiHost",
|
2022-06-03 16:00:33 +08:00
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"-p",
|
|
|
|
|
|
"--port",
|
|
|
|
|
|
type=int,
|
2026-03-08 22:30:55 +09:00
|
|
|
|
help="监听端口。默认使用部署设置中的WebuiPort",
|
2022-06-03 16:00:33 +08:00
|
|
|
|
)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
parser.add_argument(
|
2026-03-08 22:30:55 +09:00
|
|
|
|
"-k", "--key", type=str, help="Alas密码。默认无密码"
|
2022-07-26 15:33:46 +08:00
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--cdn",
|
|
|
|
|
|
action="store_true",
|
2026-03-08 22:30:55 +09:00
|
|
|
|
help="使用jsdelivr CDN获取pywebio静态文件(css, js)。默认使用自托管CDN",
|
2022-07-26 15:33:46 +08:00
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
2026-03-08 22:30:55 +09:00
|
|
|
|
"--electron", action="store_true", help="由Electron客户端运行"
|
2022-07-26 15:33:46 +08:00
|
|
|
|
)
|
2025-06-05 15:36:17 +08:00
|
|
|
|
parser.add_argument(
|
2026-03-08 22:30:55 +09:00
|
|
|
|
"--ssl-key", dest="ssl_key", type=str, help="SSL密钥文件路径,用于HTTPS支持"
|
2025-06-05 15:36:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
2026-03-08 22:30:55 +09:00
|
|
|
|
"--ssl-cert", type=str, help="SSL证书文件路径,用于HTTPS支持"
|
2025-06-05 15:36:17 +08:00
|
|
|
|
)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--run",
|
|
|
|
|
|
nargs="+",
|
|
|
|
|
|
type=str,
|
2026-03-08 22:30:55 +09:00
|
|
|
|
help="启动时运行指定配置的Alas",
|
2022-07-26 15:33:46 +08:00
|
|
|
|
)
|
2022-01-08 21:45:05 +08:00
|
|
|
|
args, _ = parser.parse_known_args()
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2026-03-08 22:30:55 +09: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
|
|
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 记录启动器配置
|
2023-02-08 16:37:11 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# Electron客户端特定处理
|
2023-02-08 16:37:11 +08:00
|
|
|
|
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
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 验证SSL配置
|
2025-06-05 15:36:17 +08:00
|
|
|
|
if ssl_cert is None and ssl_key is not None:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.error("提供了SSL密钥但未提供证书。请同时提供SSL密钥和证书。")
|
2025-06-05 15:36:17 +08:00
|
|
|
|
elif ssl_key is None and ssl_cert is not None:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.error("提供了SSL证书但未提供密钥。请同时提供SSL密钥和证书。")
|
2025-06-05 15:36:17 +08:00
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 启动uvicorn服务器
|
|
|
|
|
|
try:
|
2025-12-28 12:34:28 +08:00
|
|
|
|
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:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.error(f"Uvicorn服务崩溃: {str(e)}")
|
|
|
|
|
|
raise
|
2026-04-12 00:28:08 +08:00
|
|
|
|
def _stop_process(process, timeout=5):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Safely stop a multiprocessing.Process with escalating termination.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not process or not process.is_alive():
|
|
|
|
|
|
return
|
2026-03-08 22:30:55 +09:00
|
|
|
|
|
2026-04-12 00:28:08 +08:00
|
|
|
|
logger.info(f"正在停止服务进程 (PID: {process.pid})...")
|
|
|
|
|
|
process.terminate()
|
|
|
|
|
|
process.join(timeout=timeout)
|
2022-01-07 22:06:28 +08:00
|
|
|
|
|
2026-04-12 00:28:08 +08:00
|
|
|
|
if process.is_alive():
|
|
|
|
|
|
logger.warning(f"服务进程 (PID: {process.pid}) 超时未退出,强制终止...")
|
|
|
|
|
|
process.kill()
|
|
|
|
|
|
process.join(timeout=3)
|
2026-04-11 22:48:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 16:00:33 +08:00
|
|
|
|
if __name__ == "__main__":
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 设置multiprocessing启动方式为spawn(macOS兼容性要求)
|
2025-12-28 12:34:28 +08:00
|
|
|
|
try:
|
|
|
|
|
|
set_start_method("spawn", force=True)
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 额外的macOS环境配置
|
2025-12-28 12:34:28 +08:00
|
|
|
|
if os.name == "posix" and sys.platform == "darwin":
|
|
|
|
|
|
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
|
|
|
|
except RuntimeError:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.warning("无法设置spawn启动方式,可能使用fork(macOS上不推荐)")
|
2025-12-28 12:34:28 +08:00
|
|
|
|
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 启用热重载模式
|
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()
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.info(f"启动Alas Web服务 (PID: {process.pid})")
|
|
|
|
|
|
|
2022-06-04 17:40:26 +08:00
|
|
|
|
while not should_exit:
|
2022-07-26 15:33:46 +08:00
|
|
|
|
try:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 等待重启事件,超时1秒
|
2025-12-28 12:34:28 +08:00
|
|
|
|
restart_triggered = event.wait(1)
|
2022-07-26 15:33:46 +08:00
|
|
|
|
except KeyboardInterrupt:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.info("收到KeyboardInterrupt,退出中...")
|
2025-12-28 12:34:28 +08:00
|
|
|
|
should_exit = True
|
|
|
|
|
|
break
|
|
|
|
|
|
except Exception as e:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.error(f"等待重启事件时出错: {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:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.info("重启事件触发,终止当前服务...")
|
2026-04-12 00:28:08 +08:00
|
|
|
|
_stop_process(process)
|
2022-06-04 17:40:26 +08:00
|
|
|
|
break
|
2025-12-28 12:34:28 +08:00
|
|
|
|
elif not process.is_alive():
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.error("Alas Web服务意外退出")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
should_exit = True
|
2025-12-28 12:34:28 +08:00
|
|
|
|
|
|
|
|
|
|
# 确保子进程完全退出
|
2026-04-12 00:28:08 +08:00
|
|
|
|
_stop_process(process)
|
|
|
|
|
|
|
|
|
|
|
|
# 最终清理(预防性)
|
|
|
|
|
|
_stop_process(process)
|
2026-03-08 22:30:55 +09:00
|
|
|
|
logger.info("Alas Web服务已成功退出")
|
2022-06-04 17:40:26 +08:00
|
|
|
|
else:
|
2026-03-08 22:30:55 +09:00
|
|
|
|
# 非重载模式:直接运行
|
2026-04-12 00:28:08 +08:00
|
|
|
|
func(None)
|