Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions bot/revbotservice.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<?php

define('CURSCRIPT', 'revbotservice');
include './include/common.inc.php';
include GAME_ROOT . './include/game.func.php';
include GAME_ROOT . './bot/revbot.func.php';

$gameRoot = dirname(__DIR__).DIRECTORY_SEPARATOR;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(无法在本次 review 中锚定到新增文件行,先在此备注)bothost/main.py 里拼接查询参数使用 full_url += "?" + urlencode(...),当 revbotservice_url 本身已包含 ? 时会生成无效 URL(...?a=b?c=d),可能导致连接目标不符合配置预期。

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if(is_dir($gameRoot)) {
chdir($gameRoot);
}

require_once $gameRoot.'include/common.inc.php';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(无法在本次 review 中锚定到新增文件行,先在此备注)bothost/main.py 通过 response.fp.raw._sock 这种私有属性设置读超时;在部分 Python/urllib 响应实现下可能拿不到底层 socket,从而 read_timeout_sec 不生效并导致 readline() 长时间阻塞、影响重连/退出。

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

require_once GAME_ROOT.'./include/game.func.php';
require_once GAME_ROOT.'./bot/revbot.func.php';

# 注意:因为进程锁的存在,运行bot脚本时必须确保游戏处于未开始状态
# 否则请先中止游戏,并手动清空lock目录下所有文件,然后确保游戏正处于未开始状态下运行脚本
Expand All @@ -11,6 +18,9 @@
bot_prepare_flag:
$id = 0;
$dir = GAME_ROOT.'./bot/lock/';
if(!is_dir($dir)) {
mkdir($dir, 0777, true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkdir($dir, 0777, true) 的返回值未检查;如果权限不足或并发创建导致失败,后续 scandir() / touch() 仍会执行并产生告警或让锁目录不可用。可以考虑在创建失败时显式记录/退出,以避免“看似继续跑但其实不可用”。

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

}
$scdir = scandir($dir);
# 为进程创建对应编号的进程锁
$process_id = $scdir ? count($scdir)+1 : 1;
Expand Down Expand Up @@ -67,7 +77,7 @@
{
$flag = bot_acts($id);
if ($flag == 0) {
unset($gamevars['botid'][array_search($botid, $gamevars['botid'])]);
unset($gamevars['botid'][array_search($id, $gamevars['botid'])]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里 array_search($id, $gamevars['botid']) 找不到时会返回 false,而 unset($arr[false]) 可能因类型转换误删索引 0 的元素。可以考虑在 unset 前对返回值做 !== false 的判断,避免误删其他 bot id。

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

save_gameinfo();
save_combatinfo();
if (empty($gamevars['botid'])) break;
Expand All @@ -81,4 +91,4 @@
{
goto bot_prepare_flag;
}
}
}
11 changes: 11 additions & 0 deletions bothost/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ python bothost/main.py -c bothost/config.json
- `connect_timeout_sec`:连接超时。
- `read_timeout_sec`:读取超时,超时会断开并重连。
- `restart_delay_sec`:重连等待秒数。
- `disable_env_proxy`:是否禁用环境变量中的 HTTP/HTTPS 代理(默认 true,建议保持)。
- `insecure_skip_tls_verify`:是否跳过 TLS 证书校验(默认 false,仅测试环境临时使用)。
- `headers`:额外请求头。
- `query`:附加查询参数。

Expand All @@ -47,3 +49,12 @@ python bothost/main.py -c bothost/config.json
1. `revbotservice.php` 是长生命周期脚本,受目标 PHP 运行时参数影响(如 `max_execution_time`)。
2. 若目标前置代理(Nginx/CDN)对长连接有限制,需要放宽超时。
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。
4 changes: 3 additions & 1 deletion bothost/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"connect_timeout_sec": 10,
"read_timeout_sec": 30,
"restart_delay_sec": 2,
"disable_env_proxy": true,
"insecure_skip_tls_verify": false,
"headers": {
"User-Agent": "bothost/0.1"
"User-Agent": "bothost/0.2"
},
"query": {}
}
Expand Down
43 changes: 42 additions & 1 deletion bothost/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import socket
import threading
import time
import ssl
import urllib.error
import urllib.parse
import urllib.request
Expand All @@ -27,6 +28,7 @@ class WorkerState:
bot_id: Optional[int] = None
restarts: int = 0
errors: int = 0
last_error: str = ""


@dataclass
Expand All @@ -39,6 +41,8 @@ class TargetConfig:
restart_delay_sec: int = 2
headers: 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:
Expand Down Expand Up @@ -94,6 +98,10 @@ def _worker_loop(self, target: TargetRuntime, worker_id: int) -> None:
self._set_state(target, worker_id, status="connecting")
try:
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
self._record_error(target, worker_id, f"{type(exc).__name__}: {exc}")

Expand All @@ -111,7 +119,18 @@ def _stream_worker(self, target: TargetRuntime, worker_id: int) -> None:
full_url += "?" + urllib.parse.urlencode(cfg.query)

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")
# response.fp 是底层文件对象,可设置读超时,防止永远阻塞。
if hasattr(response, "fp") and hasattr(response.fp, "raw"):
Expand Down Expand Up @@ -159,6 +178,10 @@ def _consume_line(self, target: TargetRuntime, worker_id: int, line: str) -> Non
state.status = "running"
if "等待中" in line:
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:
with target.lock:
Expand All @@ -170,9 +193,21 @@ def _record_error(self, target: TargetRuntime, worker_id: int, err: str) -> None
state = target.states[worker_id]
state.errors += 1
state.last_line = err
state.last_error = err
state.last_seen_ts = time.time()
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:
with target.lock:
target.states[worker_id].restarts += 1
Expand All @@ -195,6 +230,10 @@ def print_report(self) -> None:
f"bot={st.bot_id}, restart={st.restarts}, err={st.errors}, age={age}s"
)
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]:
Expand All @@ -215,6 +254,8 @@ def parse_targets(config: Dict[str, Any]) -> List[TargetConfig]:
restart_delay_sec=int(raw.get("restart_delay_sec", 2)),
headers=dict(raw.get("headers", {})),
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
Expand Down