Commit 66c4558d authored by Nemo Ma's avatar Nemo Ma Committed by GitHub

Merge pull request #219 from amarillonmc/codex/create-bot-control-system-in-bothost-gsrgdp

fix: 修复 revbotservice Web 路径加载失败并增强 bothost 远端 PHP 报错可见性
parents 49fc9681 817bde33
<?php <?php
define('CURSCRIPT', 'revbotservice'); define('CURSCRIPT', 'revbotservice');
include './include/common.inc.php';
include GAME_ROOT . './include/game.func.php'; $gameRoot = dirname(__DIR__).DIRECTORY_SEPARATOR;
include GAME_ROOT . './bot/revbot.func.php'; if(is_dir($gameRoot)) {
chdir($gameRoot);
}
require_once $gameRoot.'include/common.inc.php';
require_once GAME_ROOT.'./include/game.func.php';
require_once GAME_ROOT.'./bot/revbot.func.php';
# 注意:因为进程锁的存在,运行bot脚本时必须确保游戏处于未开始状态 # 注意:因为进程锁的存在,运行bot脚本时必须确保游戏处于未开始状态
# 否则请先中止游戏,并手动清空lock目录下所有文件,然后确保游戏正处于未开始状态下运行脚本 # 否则请先中止游戏,并手动清空lock目录下所有文件,然后确保游戏正处于未开始状态下运行脚本
...@@ -11,6 +18,9 @@ include GAME_ROOT . './bot/revbot.func.php'; ...@@ -11,6 +18,9 @@ include GAME_ROOT . './bot/revbot.func.php';
bot_prepare_flag: bot_prepare_flag:
$id = 0; $id = 0;
$dir = GAME_ROOT.'./bot/lock/'; $dir = GAME_ROOT.'./bot/lock/';
if(!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$scdir = scandir($dir); $scdir = scandir($dir);
# 为进程创建对应编号的进程锁 # 为进程创建对应编号的进程锁
$process_id = $scdir ? count($scdir)+1 : 1; $process_id = $scdir ? count($scdir)+1 : 1;
...@@ -67,7 +77,7 @@ while($id) ...@@ -67,7 +77,7 @@ while($id)
{ {
$flag = bot_acts($id); $flag = bot_acts($id);
if ($flag == 0) { if ($flag == 0) {
unset($gamevars['botid'][array_search($botid, $gamevars['botid'])]); unset($gamevars['botid'][array_search($id, $gamevars['botid'])]);
save_gameinfo(); save_gameinfo();
save_combatinfo(); save_combatinfo();
if (empty($gamevars['botid'])) break; if (empty($gamevars['botid'])) break;
...@@ -81,4 +91,4 @@ while($id) ...@@ -81,4 +91,4 @@ while($id)
{ {
goto bot_prepare_flag; goto bot_prepare_flag;
} }
} }
\ No newline at end of file
...@@ -39,6 +39,8 @@ python bothost/main.py -c bothost/config.json ...@@ -39,6 +39,8 @@ python bothost/main.py -c bothost/config.json
- `connect_timeout_sec`:连接超时。 - `connect_timeout_sec`:连接超时。
- `read_timeout_sec`:读取超时,超时会断开并重连。 - `read_timeout_sec`:读取超时,超时会断开并重连。
- `restart_delay_sec`:重连等待秒数。 - `restart_delay_sec`:重连等待秒数。
- `disable_env_proxy`:是否禁用环境变量中的 HTTP/HTTPS 代理(默认 true,建议保持)。
- `insecure_skip_tls_verify`:是否跳过 TLS 证书校验(默认 false,仅测试环境临时使用)。
- `headers`:额外请求头。 - `headers`:额外请求头。
- `query`:附加查询参数。 - `query`:附加查询参数。
...@@ -47,3 +49,12 @@ python bothost/main.py -c bothost/config.json ...@@ -47,3 +49,12 @@ python bothost/main.py -c bothost/config.json
1. `revbotservice.php` 是长生命周期脚本,受目标 PHP 运行时参数影响(如 `max_execution_time`)。 1. `revbotservice.php` 是长生命周期脚本,受目标 PHP 运行时参数影响(如 `max_execution_time`)。
2. 若目标前置代理(Nginx/CDN)对长连接有限制,需要放宽超时。 2. 若目标前置代理(Nginx/CDN)对长连接有限制,需要放宽超时。
3. bothost 仅负责远程托管与状态监测;BOT 行为逻辑仍由 PHPDTS 原生 `revbot` 代码执行。 3. bothost 仅负责远程托管与状态监测;BOT 行为逻辑仍由 PHPDTS 原生 `revbot` 代码执行。
## 故障诊断
- 状态中 `err` 持续增长时,查看汇总下方 `last_error`,可直接看到最近一次 HTTP/网络错误详情。
- 若出现类似 `Tunnel connection failed: 403 Forbidden`,通常是环境代理劫持导致,请确认 `disable_env_proxy=true`
- 若出现证书错误,可先确认站点证书链;仅在临时测试中可设 `insecure_skip_tls_verify=true`
-`last_error` 中包含 `include(...common.inc.php): failed to open stream` 等报错,通常是目标站 `bot/revbotservice.php` 在 Web 环境下工作目录不正确;本仓库已修复为基于脚本目录计算 GAME_ROOT。
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
"connect_timeout_sec": 10, "connect_timeout_sec": 10,
"read_timeout_sec": 30, "read_timeout_sec": 30,
"restart_delay_sec": 2, "restart_delay_sec": 2,
"disable_env_proxy": true,
"insecure_skip_tls_verify": false,
"headers": { "headers": {
"User-Agent": "bothost/0.1" "User-Agent": "bothost/0.2"
}, },
"query": {} "query": {}
} }
......
...@@ -9,6 +9,7 @@ import signal ...@@ -9,6 +9,7 @@ import signal
import socket import socket
import threading import threading
import time import time
import ssl
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
...@@ -27,6 +28,7 @@ class WorkerState: ...@@ -27,6 +28,7 @@ class WorkerState:
bot_id: Optional[int] = None bot_id: Optional[int] = None
restarts: int = 0 restarts: int = 0
errors: int = 0 errors: int = 0
last_error: str = ""
@dataclass @dataclass
...@@ -39,6 +41,8 @@ class TargetConfig: ...@@ -39,6 +41,8 @@ class TargetConfig:
restart_delay_sec: int = 2 restart_delay_sec: int = 2
headers: Dict[str, str] = field(default_factory=dict) headers: Dict[str, str] = field(default_factory=dict)
query: Dict[str, str] = field(default_factory=dict) query: Dict[str, str] = field(default_factory=dict)
disable_env_proxy: bool = True
insecure_skip_tls_verify: bool = False
class TargetRuntime: class TargetRuntime:
...@@ -94,6 +98,10 @@ class BotHost: ...@@ -94,6 +98,10 @@ class BotHost:
self._set_state(target, worker_id, status="connecting") self._set_state(target, worker_id, status="connecting")
try: try:
self._stream_worker(target, worker_id) self._stream_worker(target, worker_id)
except urllib.error.HTTPError as exc:
self._record_http_error(target, worker_id, exc)
except urllib.error.URLError as exc:
self._record_error(target, worker_id, f"URLError: {exc.reason}")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
self._record_error(target, worker_id, f"{type(exc).__name__}: {exc}") self._record_error(target, worker_id, f"{type(exc).__name__}: {exc}")
...@@ -111,7 +119,18 @@ class BotHost: ...@@ -111,7 +119,18 @@ class BotHost:
full_url += "?" + urllib.parse.urlencode(cfg.query) full_url += "?" + urllib.parse.urlencode(cfg.query)
request = urllib.request.Request(full_url, headers=cfg.headers, method="GET") request = urllib.request.Request(full_url, headers=cfg.headers, method="GET")
with urllib.request.urlopen(request, timeout=cfg.connect_timeout_sec) as response: handlers: List[Any] = []
if cfg.disable_env_proxy:
handlers.append(urllib.request.ProxyHandler({}))
if cfg.insecure_skip_tls_verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
handlers.append(urllib.request.HTTPSHandler(context=ctx))
opener = urllib.request.build_opener(*handlers) if handlers else urllib.request.build_opener()
with opener.open(request, timeout=cfg.connect_timeout_sec) as response:
self._set_state(target, worker_id, status="connected") self._set_state(target, worker_id, status="connected")
# response.fp 是底层文件对象,可设置读超时,防止永远阻塞。 # response.fp 是底层文件对象,可设置读超时,防止永远阻塞。
if hasattr(response, "fp") and hasattr(response.fp, "raw"): if hasattr(response, "fp") and hasattr(response.fp, "raw"):
...@@ -159,6 +178,10 @@ class BotHost: ...@@ -159,6 +178,10 @@ class BotHost:
state.status = "running" state.status = "running"
if "等待中" in line: if "等待中" in line:
state.status = "waiting_lock" state.status = "waiting_lock"
low = line.lower()
if "fatal error" in low or "warning:" in low or "uncaught error" in low:
state.last_error = line
state.status = "remote_php_error"
def _set_state(self, target: TargetRuntime, worker_id: int, status: str) -> None: def _set_state(self, target: TargetRuntime, worker_id: int, status: str) -> None:
with target.lock: with target.lock:
...@@ -170,9 +193,21 @@ class BotHost: ...@@ -170,9 +193,21 @@ class BotHost:
state = target.states[worker_id] state = target.states[worker_id]
state.errors += 1 state.errors += 1
state.last_line = err state.last_line = err
state.last_error = err
state.last_seen_ts = time.time() state.last_seen_ts = time.time()
state.status = "error" state.status = "error"
def _record_http_error(self, target: TargetRuntime, worker_id: int, err: urllib.error.HTTPError) -> None:
body = ""
try:
body = err.read(300).decode("utf-8", errors="ignore").strip()
except Exception: # noqa: BLE001
body = ""
detail = f"HTTPError {err.code}: {err.reason}"
if body:
detail += f" | body={body}"
self._record_error(target, worker_id, detail)
def _increment_restart(self, target: TargetRuntime, worker_id: int) -> None: def _increment_restart(self, target: TargetRuntime, worker_id: int) -> None:
with target.lock: with target.lock:
target.states[worker_id].restarts += 1 target.states[worker_id].restarts += 1
...@@ -195,6 +230,10 @@ class BotHost: ...@@ -195,6 +230,10 @@ class BotHost:
f"bot={st.bot_id}, restart={st.restarts}, err={st.errors}, age={age}s" f"bot={st.bot_id}, restart={st.restarts}, err={st.errors}, age={age}s"
) )
print(f"- {target.config.name}: {', '.join(rows)}") print(f"- {target.config.name}: {', '.join(rows)}")
for worker_id in sorted(target.states):
st = target.states[worker_id]
if st.last_error:
print(f" - w{worker_id} last_error: {st.last_error}")
def load_config(path: str) -> Dict[str, Any]: def load_config(path: str) -> Dict[str, Any]:
...@@ -215,6 +254,8 @@ def parse_targets(config: Dict[str, Any]) -> List[TargetConfig]: ...@@ -215,6 +254,8 @@ def parse_targets(config: Dict[str, Any]) -> List[TargetConfig]:
restart_delay_sec=int(raw.get("restart_delay_sec", 2)), restart_delay_sec=int(raw.get("restart_delay_sec", 2)),
headers=dict(raw.get("headers", {})), headers=dict(raw.get("headers", {})),
query=dict(raw.get("query", {})), query=dict(raw.get("query", {})),
disable_env_proxy=bool(raw.get("disable_env_proxy", True)),
insecure_skip_tls_verify=bool(raw.get("insecure_skip_tls_verify", False)),
) )
) )
return targets return targets
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment