forked from wess09/AzurPilot
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgui.py
More file actions
197 lines (173 loc) · 6.59 KB
/
Copy pathgui.py
File metadata and controls
197 lines (173 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import os
import sys
import threading
from multiprocessing import Event, Process, set_start_method
from typing import Optional
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
from module.logger import logger
from module.webui.setting import State
def func(ev: Optional[Event]):
"""
主函数:运行Web服务。
Args:
ev: 可选的重启事件,用于热重载功能
"""
import argparse
import asyncio
import uvicorn
# 平台特定的asyncio配置
if sys.platform == "darwin":
# macOS: 禁用fork安全检查以避免Mach端口冲突
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
elif sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
State.restart_event = ev
# 解析命令行参数
parser = argparse.ArgumentParser(description="AzurPilot Web 服务")
parser.add_argument(
"--host",
type=str,
help="监听主机。默认使用部署设置中的WebuiHost",
)
parser.add_argument(
"-p",
"--port",
type=int,
help="监听端口。默认使用部署设置中的WebuiPort",
)
parser.add_argument(
"-k", "--key", type=str, help="AzurPilot密码。默认无密码"
)
parser.add_argument(
"--cdn",
action="store_true",
help="使用jsdelivr CDN获取pywebio静态文件(css, js)。默认使用自托管CDN",
)
parser.add_argument(
"--electron", action="store_true", help="由Electron客户端运行"
)
parser.add_argument(
"--ssl-key", dest="ssl_key", type=str, help="SSL密钥文件路径,用于HTTPS支持"
)
parser.add_argument(
"--ssl-cert", type=str, help="SSL证书文件路径,用于HTTPS支持"
)
parser.add_argument(
"--run",
nargs="+",
type=str,
help="启动时运行指定配置的AzurPilot",
)
args, _ = parser.parse_known_args()
# 配置服务器设置
host = args.host or State.deploy_config.WebuiHost or "0.0.0.0"
port = args.port or int(State.deploy_config.WebuiPort) or 22267
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
State.electron = args.electron
State.webui_host = host
# 记录启动器配置
logger.hr("Launcher config")
logger.attr("Host", host)
logger.attr("Port", port)
logger.attr("SSL", ssl)
logger.attr("Electron", args.electron)
logger.attr("Reload", ev is not None)
# Electron客户端特定处理
if State.electron:
# https://github.com/LmeSzinc/AzurLaneAutoScript/issues/2051
logger.info("检测到 Electron,移除标准输出日志处理器")
from module.logger import console_hdlr
logger.removeHandler(console_hdlr)
# 验证SSL配置
if ssl_cert is None and ssl_key is not None:
logger.error("提供了SSL密钥但未提供证书。请同时提供SSL密钥和证书。")
elif ssl_key is None and ssl_cert is not None:
logger.error("提供了SSL证书但未提供密钥。请同时提供SSL密钥和证书。")
# 启动uvicorn服务器
try:
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服务崩溃: {str(e)}")
raise
def _stop_process(process, timeout=5):
"""
安全停止子进程,采用逐级升级的终止策略。
先尝试 terminate(),超时后升级为 kill() 强制终止。
Args:
process: 待停止的 multiprocessing.Process 实例
timeout: 等待进程优雅退出的超时时间(秒),默认 5
"""
if not process or not process.is_alive():
return
logger.info(f"正在停止服务进程 (PID: {process.pid})...")
process.terminate()
process.join(timeout=timeout)
if process.is_alive():
logger.warning(f"服务进程 (PID: {process.pid}) 超时未退出,强制终止...")
process.kill()
process.join(timeout=3)
if __name__ == "__main__":
# 设置multiprocessing启动方式为spawn(macOS兼容性要求)
try:
set_start_method("spawn", force=True)
# 额外的macOS环境配置
if os.name == "posix" and sys.platform == "darwin":
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
except RuntimeError:
logger.warning("无法设置spawn启动方式,可能使用fork(macOS上不推荐)")
# 启用热重载模式
if State.deploy_config.EnableReload:
should_exit = False
while not should_exit:
event = Event()
process = Process(target=func, args=(event,), name="gui")
process.start()
logger.info(f"启动AzurPilot Web服务 (PID: {process.pid})")
while not should_exit:
try:
# 等待重启事件,超时1秒
restart_triggered = event.wait(1)
except KeyboardInterrupt:
logger.info("收到KeyboardInterrupt,退出中...")
should_exit = True
break
except Exception as e:
logger.error(f"等待重启事件时出错: {str(e)}")
should_exit = True
break
if restart_triggered:
logger.info("重启事件触发,终止当前服务...")
_stop_process(process)
break
elif not process.is_alive():
logger.error("AzurPilot Web服务意外退出")
should_exit = True
# 确保子进程完全退出
_stop_process(process)
# 最终清理(预防性)
_stop_process(process)
logger.info("AzurPilot Web服务已成功退出")
else:
# 非重载模式:直接运行
func(None)