mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
v2.1.5
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8dc47d0f48 |
2.1.2:隧道稳定性加固(继续修「网站还是容易挂」)
2.1.1 修掉了 nettune 死锁之后,公网上到 200,但**过一阵又 530**。
这次抓到的是隧道自己:
cloudflared 的 precheck 输出(tunnel.log):
| UDP Connectivity region1.v2.argotunnel.com FAIL QUIC connection failed |
| TCP Connectivity region1.v2.argotunnel.com FAIL HTTP/2 connection is blocked or unreachable |
| Cloudflare API api.cloudflare.com:443 PASS API is reachable |
| ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2. |
也就是 **443 通、7844 不通**,而且是**间歇性**的(过几分钟自己又好了,
然后再次断)。Cloudflare 隧道的边缘连接走 7844(QUIC 走 UDP 7844、
HTTP/2 走 TCP 7844)。这条线路对它是间歇性阻断 —— 属于运营商那一侧,
设备改不了,但可以让 cloudflared 自己去适应。
== 两个改动 ==
1. **协议从写死改成 auto**
lanremote-tunnel --protocol quic -> --protocol auto
lanremote-tunnel-h2 --protocol http2 -> --protocol auto
原来两条实例各赌一个协议:那一刻 7844 的 UDP 和 TCP 都不通就一起死,
没有退路(实测连接数掉到 1/0、公网 530)。改成 auto 之后
cloudflared 自己在 QUIC / HTTP/2 之间挑当前能通的那条,
10 条连接全都有自适应能力。
2. **--retries 5 -> 1000000**
原来失败 5 次就退出、靠 systemd Restart=always 拉起来,
每次退出到重启之间网站都是断的。而这条线路的抖动是分钟级的 ——
实测隧道每 3~10 分钟就被重启一次(19:37 / 19:40 / 19:43 / 19:50 / 19:52),
公网跟着一起断。拉大之后抖动期间连接自己恢复,整条隧道不退出。
3. 顺手加日志轮转(tunnel.log 已经 2.98 MB,这台机器硬盘不大)。
== 实测 ==
改前:连接数 1/0,公网 530
改后:连接数 7+2(接近满配 10),公网连测 5 次全部 200
== 同步 ==
relay/deploy.sh 里的 MAIN_PROTO / ALT_PROTO 也改成 auto、
TUNNEL_BASE 的 --retries 也拉到 1000000,新机器装出来就是加固过的。
全量回归 46 个阶段全部通过。
|
||
|
|
6adf7acbfb |
修「网站容易挂」的真因:nettune 和隧道之间的死锁
用户报「网站还是容易挂」。查到最后是个 systemd 顺序死锁 ——
而且**是我自己引入的**(换掉设备上那份过期 unit 之后才露出来)。
== 死锁链条 ==
lanremote-nettune.service:
Before=lanremote-tunnel.service <- 我必须先于隧道启动完
ExecStart=/bin/bash net-tune.sh
net-tune.sh 第 4 步("让已经建立的隧道用上新参数"):
systemctl restart lanremote-tunnel <- 在这里永久阻塞
nettune 在**自己还没结束**的时候去 restart 一个"必须等自己结束
才能启动"的服务 —— systemd 判定顺序冲突,restart 一直不返回。
证据(设备上抓的):
CGroup: /system.slice/lanremote-nettune.service
├─13388 /bin/bash net-tune.sh
└─13428 systemctl restart lanremote-tunnel
那个 systemctl 的 etime 是 09:32 —— 卡了 9 分半。
后果:nettune 一直 activating -> 隧道被 Before= 挡着起不来 ->
公网 530。而 journalctl 里只看得到 nettune 停在 activating,
没有任何报错,特别难查。
== 为什么以前没暴露 ==
设备上原来那份 unit 的 ExecStart 里有个 iptables ... || ...,
systemd 不支持 ||,所以它**在第 2 条 ExecStart 就报错退出**,
根本走不到"重启隧道"那一步 —— 死锁被那个 bug 遮住了。
我把 unit 换成正确形状之后,死锁才露出来。
另一个迷惑点:**手动跑 net-tune.sh 完全正常**(输出"没有正在跑的
隧道,跳过"),因为手动跑时不在服务上下文里、没有 Before= 约束。
== 改法 ==
第 4 步的本意是"改完 MTU/MSS 后让已建立的隧道用上新参数",
这个功能有用,但**只该在人工重跑时做** —— 开机时顺序本来就是
nettune 先、隧道后,那时隧道还没起来。
所以 unit 里加 Environment=NETTUNE_SERVICE_CALL=1,
脚本看到就跳过第 4 步。三个地方都改了:
relay/net-tune.sh 认这个开关
relay/deploy.sh 生成的 unit 带上它
relay/install.sh 同上(老机器走的安装路径)
没选 systemctl restart --no-block:那只是把阻塞变异步,
开机时仍然做一次无谓的重启(隧道刚起来就被停掉),
也没解决"不该在开机时重启"这个语义问题。
== 另外 ==
setup-monitor.sh / monitor.sh / rescue.sh 这一整套这次也真的
在线上跑起来并验证过了:
18:13:03 紧急预案启动:公网连续 3 次不通
18:13:07 现场已记录 + [1/5] 重启隧道
18:13:59 ✅ 第 1 级救回来了,公网已恢复
18:13:59 已把「第 1 级奏效」记进知识库
死锁这次就是被监测单元抓到的(fails=3 / last_code=530)。
全量回归 46 个阶段全部通过。
|
||
|
|
3f18e6bb40 |
2.1.0:网站自动抢救 + 后台抢救报告分区 + 网络热门音效 + 全量测试后台静默
按用户定的规矩,这一轮有新功能,升第二位:2.0.0 -> 2.1.0。
== 一、网站挂了会自己救 ==
用户原话:「网站还是容易挂,建议继续增加隧道稳定性,并在后台设立
检测单元,实时检测网站状态,一旦检测到网站挂了,立即启动紧急预案」。
新增 relay/monitor.sh + lanremote-monitor.timer:
每 60 秒从**公网**查一次 /v1/health(不是查 127.0.0.1)
为什么不查本机:本机 200 只说明中转进程活着,用户真正关心的是
经过 Cloudflare 边缘 + 隧道 + 本机三段之后能不能打开。实测过
隧道两条实例都掉了、本机 curl 照样 200,只有从公网测才发现 502。
连续 3 次不通才动手(这条 4G 线路本来就有 2~4% 偶发失败),
救完 10 分钟内不再动手(否则预案刚重启完隧道还没连上,
下一次检查又救一遍,来回重启反而永远连不上)。
新增 relay/rescue.sh:五级阶梯,每级做完立刻复验,好了就停
1 重启隧道 不动中转,在线设备不掉线
2 重启中转服务 设备要重连,但很快
3 重新施加网络调优 MSS / MTU 跑偏时用
4 重启无线网卡 4G / WiFi 卡死时用
5 重启整机 最后的底牌,**默认不开**(要 RESCUE_REBOOT=1)
理由:如果问题出在供电或上游线路,重启只会让恢复更慢;
而且半夜自动重启一台没人管的机器风险太大。
== 二、抢救报告 + 后台分区 ==
用户原话:「如果紧急预案未能解决,则将问题打包成 .txt,然后在
中转站设置里面加一个分区专门放还有下载这个 txt。解决后将解决问题
的方法加入紧急预案」。
* 五级都没救回来 -> 把现场(内存/进程/隧道连接数/路由和 MTU、
三份日志尾部、内核异常)整理成 rescue_reports/<时间戳>.txt
* 后台新增「抢救报告」分区,列出报告(带结论摘要),点一下下载
* 下载走单独的 /rescue/<名字>,**不发缓存头**(内容会变,
也不该让 Cloudflare 边缘留着)。和 /download/ 分开是有意的:
那个是给用户的安装包,文件名带版本号、内容永不改变,
所以敢发 604800 长缓存
* **要登录才能下** —— 报告里有内网 IP、进程表、日志尾部,
是运维信息。第一版把这条路由插在 _admin_gate() 之前,
等于匿名可下,自己查出来补上了
* 每次自动抢救成功,把「第几级奏效」写进 rescue_knowledge.md;
监测单元读它,下次同类症状优先试历史上管用的那一级
(这就是用户说的"解决后将解决问题的方法加入紧急预案")
== 三、网络热门音效 ==
用户原话:「再加上近期网络热门音效,什么冰冰冰,啊哈哈哈哈,
各种老牧师音效,可以直接远程播放」。
**版权说明**:网上那些「冰冰冰」「啊哈哈哈」是**有版权的真实录音**
(影视片段 / 别人的作品),不能下载打包进安装包 —— 那既侵权,
也会让这个项目在 Gitee 那边直接踩线(这个仓库已经被判过一次
"内容可能含有违规信息")。
所以:
* 内置 8 个**程序合成**的音效(正弦/方波现场算,零版权风险、
不占安装包体积):冰冰冰 / 啊哈哈哈 / 老牧师 / 叮 / 当当当 /
警报 / 鼓点 / 弹簧。名字对着感觉来,但不是那些录音本身
* 做「自己放进来」的目录:mp3/wav 丢进 data/sfx/ 就自动出现在
列表里,照样能远程播。目录里放了一份说明
* 手表首页新增「音效」按钮,点开选一个,电脑就响
(复用现成的 showChooser,不新建面板)
协议加 sfx_list / sfx_play 两条命令。
== 四、修「安卓设置页一片空白」 ==
用户报的。根因:设置页**建好了但忘了 root.addView()** ——
showOnly() 让它 VISIBLE 也没用,它根本不在视图树里。
这类"漏挂"编译完全不报,只在运行时表现为空白。已加断言
_tests/test_watch_panels_wired.py 盯着:每个面板都必须挂到 root、
并且纳入 showOnly()。
== 五、全量测试改成后台静默 ==
用户原话:「把全量测试改为在后台静默运行不要打开任何窗口或锁屏」。
会开窗口的 6 个阶段(含开**全屏**那个)现在跑在独立的 Windows
隐藏桌面上(_tests/hidden_desktop.py)。窗口是**真的** —— 全屏、
可见、几何、Win32 枚举都真实,测试有效性一点没损失 ——
只是不在用户眼前,全屏也不会锁屏。
顺带修了一个一直在骗人的检测:判断"锁屏"时只看 OpenInputDesktop
成不成功是不对的 —— **锁屏时它同样成功**(那时输入桌面是 Winlogon)。
于是截屏类测试在锁屏时被记成 FAIL 而不是 SKIP,把真问题淹掉了
(用户电脑锁了一夜,我就是被这个误导过)。改成看输入桌面的**名字**。
现在锁屏也能跑全量:该 SKIP 的 SKIP,该跑的照跑。
== 六、顺手修的真 bug ==
* windows/lanremote/server.py 的 sfx_play 分支用了不存在的变量名 body
(应该是 msg)-> **一调就 NameError**。pyflakes 那关抓出来的,
说明"未定义名"这条检查有用。
* _build/package_remote.py 的文件白名单只有 .py/.sh/.ico,
**.service/.timer 收不进服务端 zip** -> 新机器上监测根本不启动,
而且不报错、只是静默不跑。已加。
* protocol.py 被我补丁插入了 LF 行到 CRLF 文件里 -> 行尾混用,编码审计红。
* deploy.sh 的 watchdog 块被我改锚点时吃掉了 start/say 两行
(bash -n 通过、只是少两条执行语句)—— 改完读回来核对才发现。
* _build/CHANGELOG.md 的 2.1.0 说明里写了"管理后台",
触发对外材料清洁度检查。改成不提管理页(用户定的规矩:
对外只能提官网)。
全量回归 46 个阶段全部通过。
|
||
|
|
e611b07fb1 |
纠正一个传出去的错误结论:不是「运营商封了 UDP」
我在给部署脚本的指示里写过:
实测这条线路上运营商对 UDP 有限制 ——
12 个 Cloudflare edge IP:TCP 12/12 通、UDP 0/12 通
它照这个把注释写进了 relay/deploy.sh。
**但这个测量是无效的。** relay-stability 后来自己纠正了:
那个 UDP 探测包是手搓的简化包,格式不对,
Cloudflare 本来就不该回应它 —— 「0/12 无回应」什么都证明不了。
用 cloudflared --loglevel debug 跑临时实例,真相是:
UDP Connectivity region1/region2 PASS QUIC connection successful
SUMMARY: Environment is healthy. cloudflared will use 'quic'
Registered tunnel connection (握手 1 秒)
**UDP 完全通。**
真因是:QUIC 建连要 6+ 个包往返,而 TCP 的 SYN 只有 3 个包、
内核还会按 1s/2s/4s 激进重传。约 5.4%/包 丢包下:
(1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上
所以 QUIC 的弱点在「连不上」,不在「连上之后不好」;
一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。
结论的**用途**没变(还是要两条协议一起跑),但留着错误的根因注释会:
* 误导后人以为 UDP 被封 -> 可能做出「干脆别用 QUIC」的错误决定
* 掩盖真正可优化的方向(QUIC 握手对丢包敏感是链路物理属性,
只能靠 HTTP/2 兜底来绕,不是「运营商限制」)
改动:relay/deploy.sh 两处、_build/test_deploy_sh.sh 一处。
教训记一笔:**手搓协议包去探测可达性非常容易得出错误结论** ——
那个包格式不对,对方根本没义务回应。要么用官方工具的自检
(cloudflared 自己会打 UDP Connectivity PASS/FAIL),
要么别下结论。
|
||
|
|
aac43e559d |
修正 MTU 测试的过时断言(1500 -> 1400)
test_net_tune.py 原来断言 net-tune.sh 里写着 mtu 1500, 依据是更早一次实测「接口 MTU 降到 1400 会掉到 50%」。 那个结论后来被推翻了 —— 当时漏了一件事: **MSS 钳制只管 TCP,QUIC 走 UDP 完全不受保护。** 路径 MTU 实测 1472(1500 的包不通),QUIC 默认发 ~1452 的包会被 中间设备静默丢掉,而 UDP 丢了不回 ICMP,只能等超时。 把接口 MTU 压到 1400 之后,QUIC 建齐 10 条连接从 28 秒缩到 12 秒。 所以现在是**故意**设 1400。断言跟着改, 不然它会永久假失败,还会诱导后人把真正的改进改回 1500。 顺带记录:全量回归 38 个阶段全部通过。 新增的三个阶段(局域网优化 / 手表局域网切换 / 编码与换行 / 管理页设备控制)都是这轮加的。 |
||
|
|
8880e7033d |
修「经中转几次后永久哑掉」——「按一下没反应」的真因
== 现象 ==
用户报:
* 局域网能搜到电脑,IP/端口/配对码都对,连上后**没有任何反应**
* 走中转「已配对」,但点按钮没反应
局域网那一半已经修过(preferLan)。这次抓到的是**经中转**那一半,
而且比预想的严重得多。
== 证据链(插桩复现出来的)==
被控端日志里直接抛了未捕获异常,线程当场死:
Exception in thread client-relay:
server.py:407 session.send(reply)
relay.py:166 self._link.send_to_client(data)
relay.py:357 self._post("/v1/agent/send", ...)
relay.py:766 raise RelayError(f"请求失败: {last_error}")
lanremote.relay.RelayError: 请求失败: Idle
之后的状态:
* client-relay 线程没了 —— 电脑再也不往手表回任何东西
* relay-link 线程还活着、还在长轮询 —— 中转那边看着「设备在线」
* **手表看到的就是「连上了,但按一下毫无反应」**
写入仓库的测试 _tests/test_relay_local.py 一直是这个形状:
前面几项 PASS、跑到经中转发命令就卡死。之前没人深挖。
== 根因 1:一条 HTTP/1.1 连接被两个线程同时用 ==
_post() 里锁只包住「取连接」那一段:
with self._http_lock:
connection = self._http # 锁到这里就放了
try:
connection.request("POST", ...) # 锁外发请求
于是两个线程会同时用同一条 connection:
* relay-link 线程跑 /v1/agent/poll —— 长轮询,一条请求挂 180 秒
* client-relay 线程同时 POST /v1/agent/send
HTTP/1.1 一条连接同一时刻只能有一个在途请求,第二个线程一进来
就撞上 http.client 状态机,抛 CannotSendRequest("Idle")。
_post 的「重试一次」也救不了:它 _drop_http() 之后,另一个线程
手里还攥着那条旧连接对象在发,照样坏。
修法:
* **发命令用独立连接**(_post 加 dedicated 参数)。
长轮询那条常年被占着,发命令不该去挤它,各走各的。
* 两条连接各自一把锁**锁住整个「发请求 + 读响应」周期**,
不再只锁取连接那一下。
* 普通请求若发现缓存连接正忙(长轮询在途),临时另开一条用完即关。
* _drop_http(dedicated=) 区分清哪条;finally 里一定归还占用
(否则那条连接会被永久标记为忙)。
== 根因 2:回包失败会打死整个会话线程 ==
server.py 的 _client_loop 里:
if reply is not None:
session.send(reply) # <- 不在任何 try 里
...
except OSError: # <- 外层只捕获 OSError
pass
而中转链路上 send 抛的是 RelayError,直接冒出去把线程打死。
**死一次就永久哑**,代价太大。
修法:session.send() 单独兜住所有异常,失败就断开这个会话
(让手表重连),绝不让线程消失。外层也补一个兜底 except。
== 修复效果 ==
修复前:pair OK -> ping OK -> ping 卡死 -> 线程崩溃,永久哑
修复后:pair 21ms -> ping 82ms -> ping 4ms -> ping 6ms,零异常
_tests/test_relay_local.py:**之前完全卡死 -> 现在 21/21 全过**
这个测试之前一直是红的,说明「35 个阶段的回归全绿」这件事
本身就不成立过 —— 某个阶段卡住时看起来像环境问题而不是缺陷。
== 同批修掉验证阶段找到的其它问题 ==
1. **TTL 清理 + 关掉接入开关 = 设备永久锁死**(严重)
register() 用 `device_id in self.devices` 判断老设备,但超时清理
把设备从 devices 和 codes 都删了。电脑关机过夜(必然 > 6 小时)
第二天开机就会被自己的开关挡住,而且**重启也救不回来**
(清 codes 时没落盘,重启后又当孤儿清一遍,自我维系)。
管理员在页面上还看不到这台设备,不知道该放开谁。
修法:状态文件里单独存 knownDevices 设备历史,超时清理绝不动它;
register 改查这份历史;清理后补 _save();delete_device 才真正忘记。
2. **管理页 JS 语法错误 -> 所有按钮失效**(严重)
admin.py 里 ADMIN_JS 是普通三引号字符串,我在 Python 源码里写的
`\n` 被变成真实换行,拼进 JS 就是字符串里的裸换行 ->
整段 <script> 不执行 -> 改码/删除/批量删除/全选全变幽灵按键。
元凶是我加改码功能那次提交。改成 `\\n`。
3. **--relay / --relay-status 崩溃**(严重)
RelayLink.start(url) 的 url 是必填参数,我把默认地址改空白时
run_agent.py 两处调用没跟着改。一处被 try 吞成「测试失败:missing
1 required positional argument」(用户以为地址填错了),
一处直接报栈。两处都补上 url。
4. 非法 deviceId(控制字符 / 路径分隔符)现在被拒(白名单:
字母数字 + - _ . :)
5. enable_relay() 第二次调用不再谎报成功(加 RelayLink.running 属性,
返回值反映真实运行状态;不再用「对象建过没有」判断)
6. snapshot() 清孤儿码后补 _save(),磁盘和内存不再脱节
== 局域网全面优化(task-2,另一个智能体完成)==
4 个真问题:
* **网段判断写死 /24** —— 在 /16 网络上 10.8.1.5 和 10.8.2.5
被判成不同网络,明明能直连却去走中转还费流量。
改成按网卡真实掩码比较。
* **广播只发一个包** —— 丢一个就整轮搜不到。改成 5 个时间点重发。
* **只探一个网段** —— 多网卡手表搜不到另一网段的电脑。改成全探。
* **走中转之后再也回不到局域网** —— 出门再回家永远挂中转。
新增定期回切(先便宜地 TCP ping,通了才动真格,
不破坏正在工作的中转连接)。
实测:局域网握手 2ms、50/50 = 100%、中位 0ms。
(跨网络那半之前拿不到数字,就是被上面那个线程崩溃挡住的 ——
现已修复。)
顺带:instance.py 单实例锁端口加 YKXMZ_LOCK_PORT 覆盖(默认不变)。
用户自己那台被控端正跑在这台机器上占着 8767/TCP + 8766/UDP,
是这次排查里好几个「测试怎么都不通」的根因。
== 服务端一键部署脚本(task-1,另一个智能体完成)==
relay/deploy.sh + relay/deploy-tune.sh,随服务端 zip 发布:
* 自动检查并安装 cloudflared
* 部署程序、装 4 个 systemd 服务、网络调优、配隧道、自检打公网地址
* **不传 --hostname 且没有 TUNNEL_TOKEN 时明确提示要填**,
不会静默用一个写死的域名(用户明确说过这个中转站是他自己用的,
不对外开放,绝不能把私人域名烧成默认值)
* 重跑 = 升级,已有数据/配对码/管理密码/凭据都不动
顺带修了发布包一直缺 site_shots/ 的老 bug
(package_remote.py 用 iterdir()+is_file() 收文件,目录进不去,
新机器装完官网截图全是 404)。
== 默认中转地址改成空白 ==
用户要求:软件默认不带任何中转地址,由用户自己填。
四处硬编码全部清空(server.py / MainActivity.java / run_agent.py /
relay.py 的注释和文档示例),全仓已无非注释的硬编码地址。
== 文档同步(task-5,另一个智能体完成)==
README / BEFORE-INSTALL / AFTER-INSTALL / 打包模板:
* 新增「软件里不带任何中转地址」告示,讲清跨网络要自己部署
* 修正 Android 版本(写的是 9.0/API28,实际 API 24 / Android 7.0)
* 删掉「地址已经内置好了」(现在是错的)
* AGENT_README 原来把作者自己的中转地址当成「内置地址」发给用户,已重写
* RELAY_README 原来写「两条隧道同时跑会互相争抢」,
和真机/CHANGELOG 矛盾;按实测改成「两种协议同时在跑(故意的)」
|
||
|
|
facb156aca |
服务端 zip 加一键部署脚本(cloudflared 安装 + 部署 + 公网,全自动)
用户要求:服务器端文件里放一个 .sh,双击就能检查 cloudflared 装没装、
自动部署、配好公网,以后全自动。
== 新增 ==
relay/deploy.sh 一键部署主脚本,7 步全自动:
环境检查 -> 装 cloudflared -> 部署程序 -> 网络调优
-> 写 4 个 systemd 服务 -> 配隧道 -> 启动自检打公网地址
relay/deploy-tune.sh 网络调优,可单独重跑:
keepalive 30/10/3 + 路径 MTU 探测 + MSS 1360
+ 接口 MTU 1400 + mask networkd-wait-online
每一项都写明了实测依据
用法:
bash deploy.sh 复用已有凭据,全自动
TUNNEL_TOKEN=eyJh... bash deploy.sh Token 模式
bash deploy.sh --hostname relay.你的域名 换域名
bash deploy.sh --skip-tunnel 只装中转
bash deploy.sh --help 全部参数
重跑 = 升级;已有数据/配对码/管理密码/凭据/手改过的 config.yml 都不动。
== 自动化到什么程度 ==
* cloudflared 不在就自动下载装上(并校验能执行)
* 隧道支持两种模式:已有凭据文件、或 TUNNEL_TOKEN
* **不传 --hostname 且没有 Token 时明确提示要填**,不会静默用一个写死的值
(用户明确说过:这个中转站是他自己用的,不对外开放,
绝不能把私人域名烧成默认值)
* deploy-tune.sh 的 MTU 取 1400:路径 MTU 实测 1472,
而 MSS 钳制只管 TCP、QUIC 走 UDP 完全不受保护;
接口 MTU 降到 1400 后 QUIC 建齐 10 条连接从 28 秒缩到 12 秒
== 顺带修掉两个真 bug ==
1. write_tunnel_unit 写的文件名漏了 .service 后缀 ——
文件写下去了但 systemd 完全看不见,enable 还会安静地失败。
2. 发布包一直缺 site_shots/(老 bug):
package_remote.py 用 iterdir() + is_file() 收文件,目录进不去。
新机器装完官网截图全是 404。只有从 zip 解压真跑一遍才暴露。
同时把 deploy.sh 里 site_shots 的检查降级成警告(缺截图不该让部署失败)。
另外给 deploy-tune.sh 清旧 MSS 规则的循环加了 10 次上限 ——
原写法在 -D 报成功但规则仍在时会死循环把整个部署卡死。
== 验证 ==
* _build/test_deploy_sh.sh 沙箱 65 项,全通过
* _build/test_zip_deploy.sh 从发布 zip 解压再跑,30 项,全通过
* _build/test_deploy_real.sh **真机 Debian 上实测**(新增):
zip 解出 17 个文件 -> 语法/LF/无 BOM 通过
--help 退出码 0 且无副作用、未知参数退出码 2
沙箱安装成功、幂等重跑哨兵不被冲
现有三个中转服务全程 active,自检 200
|