# -*- coding: utf-8 -*- r'''永久修掉 nettune 死锁。 == 死锁回顾 == lanremote-nettune.service Before=lanremote-tunnel.service net-tune.sh 第 4 步 systemctl restart lanremote-tunnel nettune 在"自己还没结束"的时候去 restart 一个"必须等自己结束才能启动" 的服务 —— systemd 认为顺序冲突,restart 永久阻塞。 实测卡了 9 分半,nettune 一直 activating,隧道被 Before= 挡着起不来, 公网 530。这就是"网站容易挂"的真因之一。 == 修法 == 第 4 步的本意是"让**已经建好**的隧道用上新参数"(改完 MTU/MSS 之后 手动重跑脚本时用)。**开机时根本不该走到那一步** —— 开机顺序本来就是 nettune 先、隧道后,那时隧道还没起。 所以:unit 里加 Environment=NETTUNE_SERVICE_CALL=1, 脚本看到这个变量就跳过第 4 步。 为什么不干脆删掉第 4 步: 手动改完网络参数后,已经建立的隧道还在用旧参数, 需要重启它才生效。这个功能是有用的,只是不该在服务启动时跑。 为什么不用 `systemctl restart --no-block`: 那只是把阻塞变成异步,nettune 立刻退出、隧道随后重启 —— 开机时仍然是无谓的一次重启(隧道刚起来就被停掉), 而且没解决"不该在开机时重启"这个语义问题。 ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "_build")) from devssh import upload, run # noqa: E402 TUNE = ROOT / "relay" / "net-tune.sh" OLD = '''echo " 4) 让已经建立的隧道用上新参数"''' NEW = '''# ════════════════════════════════════════════════════════════════════ # 4) 让已经建立的隧道用上新参数 —— **只在手动跑的时候做** # ════════════════════════════════════════════════════════════════════ # # ⚠ 这一步绝对不能由 lanremote-nettune.service 执行。 # # 那个 unit 里写着 Before=lanremote-tunnel.service # (意思是"我必须先于隧道启动完")。如果脚本在自己还没结束的时候 # 去 `systemctl restart lanremote-tunnel`,systemd 会认为存在 # 顺序冲突 —— restart 永久阻塞、nettune 一直卡在 activating、 # 隧道被 Before= 挡着永远起不来。 # # 实测卡了 9 分半,表现就是"网站又挂了",而日志里什么都看不出来 # (只有 nettune 停在 activating)。手动跑本脚本却完全正常 # (因为那时没有 Before= 约束)—— 所以这个坑特别难发现。 # # 开机时本来也不需要这一步:顺序就是 nettune 先、隧道后, # 那时候隧道还没起来。 if [ "${NETTUNE_SERVICE_CALL:-0}" = "1" ]; then echo " 4) 由 nettune 服务调用,**跳过重启隧道**" echo " (unit 里有 Before=lanremote-tunnel.service," echo " 在这里 restart 会和 systemd 的顺序约束死锁)" echo " (开机顺序本来就是本脚本先、隧道后,不需要这一步)" elif [ "${SKIP_TUNNEL_RESTART:-0}" = "1" ]; then echo " 4) SKIP_TUNNEL_RESTART=1,跳过重启隧道" else echo " 4) 让已经建立的隧道用上新参数"''' def main() -> int: raw = TUNE.read_bytes() bom = raw.startswith(b"\xef\xbb\xbf") text = raw.decode("utf-8-sig") if "NETTUNE_SERVICE_CALL" in text: print(" · net-tune.sh 已经修过了") elif OLD not in text: print(" ✗ 找不到锚点") return 1 else: text = text.replace(OLD, NEW, 1) # 原来的 for 循环 + RESTARTED 逻辑要收进 else 分支 # 找到 "echo \" 网络调优已应用" 之前的部分,补一个 fi tail_anchor = 'echo " 网络调优已应用' i = text.find(tail_anchor) if i < 0: print(" ✗ 找不到结尾锚点") return 1 # 在结尾那行之前插入 fi(缩进对齐 for 循环之外) text = text[:i] + "fi\n\n" + text[i:] TUNE.write_bytes((b"\xef\xbb\xbf" if bom else b"") + text.encode("utf-8")) print(" ✓ net-tune.sh 加了 NETTUNE_SERVICE_CALL 判断") # ---- 推到设备 + 给 unit 加环境变量 ---- upload(str(TUNE), "/root/lanremote-relay/net-tune.sh") print(" ✓ 已上传 net-tune.sh") unit = '''[Unit] Description=ykxmz 网络调优(MSS 钳制 + 路径 MTU 探测) After=network-online.target Wants=network-online.target Before=lanremote-relay.service lanremote-tunnel.service lanremote-tunnel-h2.service [Service] Type=oneshot RemainAfterExit=yes # ⚠ NETTUNE_SERVICE_CALL=1 告诉 net-tune.sh:这次是**服务**在调它, # 不是人工跑的。脚本据此跳过"重启隧道让新参数生效"那一步。 # # 为什么必须跳过:本 unit 有 Before=lanremote-tunnel.service, # 脚本在自己还没结束时去 restart 隧道,systemd 会判定顺序冲突, # restart 永久阻塞 -> nettune 卡在 activating -> 隧道起不来 -> 公网 530。 # 实测卡过 9 分半。 Environment=NETTUNE_SERVICE_CALL=1 ExecStart=/bin/bash /root/lanremote-relay/net-tune.sh [Install] WantedBy=multi-user.target ''' import tempfile tmp = Path(tempfile.gettempdir()) / "lanremote-nettune.service" tmp.write_text(unit, encoding="utf-8", newline="\n") upload(str(tmp), "/etc/systemd/system/lanremote-nettune.service") print(" ✓ 已更新 unit(加了 NETTUNE_SERVICE_CALL=1)") out = run("bash -n /root/lanremote-relay/net-tune.sh && echo SH_OK; " "systemctl daemon-reload && " "systemctl restart lanremote-nettune && sleep 6 && " "echo \"nettune: $(systemctl is-active lanremote-nettune)\"; " "echo \"tunnel: $(systemctl is-active lanremote-tunnel)\"; " "echo \"h2: $(systemctl is-active lanremote-tunnel-h2)\"; " "echo \"公网: $(curl -s -o /dev/null -w '%{http_code}' " "--max-time 20 https://relay.xxmxxs.top/v1/health)\"", timeout=180) for ln in out[1].strip().splitlines(): print(" " + ln) return 0 if __name__ == "__main__": sys.exit(main())