feat(watch): treat ModemManager journal as redial standard
Use MM registration/packet-service events as the recovery trigger instead of public egress probes, so re-register heals with zero cellular traffic. Wait for wwan IPv4 after settle, coalesce bursts, and keep ip-monitor + poll as fallbacks.
This commit is contained in:
@@ -81,7 +81,8 @@ cpxy watch once # 手动跑一轮检查
|
|||||||
| 层级 | 做法 |
|
| 层级 | 做法 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **绑定(推荐)** | 默认 `BIND_SOURCE_IP=false`:只 `bind_interface=wwanX`,**不 pin 私网源 IP**,重拨换地址通常不用 regenerate |
|
| **绑定(推荐)** | 默认 `BIND_SOURCE_IP=false`:只 `bind_interface=wwanX`,**不 pin 私网源 IP**,重拨换地址通常不用 regenerate |
|
||||||
| **监控** | `cellular-proxy-watch`:`ip monitor` 事件 + 60s 轮询;网卡改名/掉线/仍 pin 了 stale IP 时自动 `generate` |
|
| **监控** | `cellular-proxy-watch`:`ip monitor` + **ModemManager 重拨/重注册日志** + 60s 轮询;网卡改名/掉线/stale pin 或 MM 抖动后自动等数据面再 `generate` |
|
||||||
|
| **MM 标准(省流量)** | 默认 `WATCH_MM_EVENTS=true`:只读本机 `journalctl -u ModemManager`,**不做公网探针**;命中 `registration/packet service` 等状态变化后 settle→等 IPv4→restart |
|
||||||
| **兜底 timer** | 每 2 分钟 oneshot 再检查一次 |
|
| **兜底 timer** | 每 2 分钟 oneshot 再检查一次 |
|
||||||
| **手动** | `cpxy rebind && cpxy verify` |
|
| **手动** | `cpxy rebind && cpxy verify` |
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,19 @@ REQUIRE_CELLULAR_IFACE=true
|
|||||||
CELLULAR_IFACE_PATTERNS=wwan,wwp,usb,enx,ppp,cdc
|
CELLULAR_IFACE_PATTERNS=wwan,wwp,usb,enx,ppp,cdc
|
||||||
# 后台监控(推荐开):网卡改名/掉线/无 IPv4 时自动 rebind;默认开启 long-running watch
|
# 后台监控(推荐开):网卡改名/掉线/无 IPv4 时自动 rebind;默认开启 long-running watch
|
||||||
ENABLE_CELLULAR_WATCH=true
|
ENABLE_CELLULAR_WATCH=true
|
||||||
# watch 轮询兜底间隔秒(事件驱动 ip monitor 为主)
|
# watch 轮询兜底间隔秒(事件驱动 ip monitor + ModemManager 日志为主)
|
||||||
WATCH_INTERVAL_SEC=60
|
WATCH_INTERVAL_SEC=60
|
||||||
|
# 以 ModemManager 日志为「重拨/重注册」标准(零公网流量,只读本机 journal)
|
||||||
|
# 命中 home→searching / attached→detached / bearer 断连等后:settle → 等 IPv4 → generate/restart
|
||||||
|
WATCH_MM_EVENTS=true
|
||||||
|
WATCH_MM_UNIT=ModemManager
|
||||||
|
# 事件后先等几秒再查数据面(避免刚 detached 就 generate)
|
||||||
|
WATCH_MM_SETTLE_SEC=5
|
||||||
|
# 等待 wwan IPv4 / MM home 的最长时间
|
||||||
|
WATCH_MM_READY_TIMEOUT_SEC=90
|
||||||
|
WATCH_MM_READY_POLL_SEC=2
|
||||||
|
# 同一轮抖动合并窗口(searching→registering→home 只恢复一次)
|
||||||
|
WATCH_MM_COALESCE_SEC=20
|
||||||
|
|
||||||
# ---- 代理监听(局域网其它设备用 0.0.0.0)----
|
# ---- 代理监听(局域网其它设备用 0.0.0.0)----
|
||||||
# mixed = 同一端口同时支持 HTTP 代理 与 SOCKS5
|
# mixed = 同一端口同时支持 HTTP 代理 与 SOCKS5
|
||||||
|
|||||||
+11
@@ -6,6 +6,17 @@
|
|||||||
2. 需要「用流量出口」时:把应用的 HTTP/SOCKS 指到 `LAN:7890`
|
2. 需要「用流量出口」时:把应用的 HTTP/SOCKS 指到 `LAN:7890`
|
||||||
3. `cpxy verify` 确认代理 IP ≠ 直连 IP
|
3. `cpxy verify` 确认代理 IP ≠ 直连 IP
|
||||||
|
|
||||||
|
## 重拨检测(ModemManager 日志标准)
|
||||||
|
|
||||||
|
watch 默认以 **本机 ModemManager journal** 作为「数据网络重拨/重注册」信号,**不消耗蜂窝公网流量**:
|
||||||
|
|
||||||
|
- 典型命中:`registration state changed (home -> searching)`、`packet service state changed (attached -> detached)`、bearer 断连、access tech 抖动
|
||||||
|
- 动作:`WATCH_MM_SETTLE_SEC` 后等待 `wwan` IPv4(可选 mmcli home),再 `generate/restart` 清半开连接
|
||||||
|
- 同一轮抖动用 `WATCH_MM_COALESCE_SEC` 合并,避免连刷三次
|
||||||
|
- 首次启动写 `mm.cursor`,**不回放历史日志**,避免升级后误 rebind
|
||||||
|
- 相关日志:`/var/log/cellular-proxy/watch.log`、`mm.last_event`、`watch.state`
|
||||||
|
- 关闭:`WATCH_MM_EVENTS=false` 后 `systemctl restart cellular-proxy-watch`
|
||||||
|
|
||||||
## 内存
|
## 内存
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ load_config() {
|
|||||||
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
|
CELLULAR_IFACE_PATTERNS="${CELLULAR_IFACE_PATTERNS:-wwan,wwp,usb,enx,ppp,cdc,rmnet,ccmni,mbim,qmi}"
|
||||||
ENABLE_CELLULAR_WATCH="${ENABLE_CELLULAR_WATCH:-true}"
|
ENABLE_CELLULAR_WATCH="${ENABLE_CELLULAR_WATCH:-true}"
|
||||||
WATCH_INTERVAL_SEC="${WATCH_INTERVAL_SEC:-60}"
|
WATCH_INTERVAL_SEC="${WATCH_INTERVAL_SEC:-60}"
|
||||||
|
WATCH_MM_EVENTS="${WATCH_MM_EVENTS:-true}"
|
||||||
|
WATCH_MM_UNIT="${WATCH_MM_UNIT:-ModemManager}"
|
||||||
|
WATCH_MM_SETTLE_SEC="${WATCH_MM_SETTLE_SEC:-5}"
|
||||||
|
WATCH_MM_READY_TIMEOUT_SEC="${WATCH_MM_READY_TIMEOUT_SEC:-90}"
|
||||||
|
WATCH_MM_READY_POLL_SEC="${WATCH_MM_READY_POLL_SEC:-2}"
|
||||||
|
WATCH_MM_COALESCE_SEC="${WATCH_MM_COALESCE_SEC:-20}"
|
||||||
PROXY_LISTEN_HOST="${PROXY_LISTEN_HOST:-0.0.0.0}"
|
PROXY_LISTEN_HOST="${PROXY_LISTEN_HOST:-0.0.0.0}"
|
||||||
PROXY_MIXED_PORT="${PROXY_MIXED_PORT:-7890}"
|
PROXY_MIXED_PORT="${PROXY_MIXED_PORT:-7890}"
|
||||||
PROXY_USER="${PROXY_USER:-}"
|
PROXY_USER="${PROXY_USER:-}"
|
||||||
|
|||||||
+272
-26
@@ -1,8 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 监控数据网卡:IP/链路变化时自动 regenerate(避免 stale bind 或网卡改名导致 rep=0x01)
|
# 监控数据网卡 + ModemManager 重拨/重注册事件,自动等待附着并 regenerate。
|
||||||
# 策略:
|
# 策略:
|
||||||
# 1) 默认配置只 bind_interface(不写死源 IP)—— 多数场景无需本脚本也能自愈
|
# 1) 默认只 bind_interface(不写死源 IP)—— 多数 IP 漂移无需 pin
|
||||||
# 2) 本脚本仍负责:iface 丢失→重探、可选 BIND_SOURCE_IP 时同步源 IP、链路 down/up 后拉起
|
# 2) iface 丢失→重探;可选 BIND_SOURCE_IP 时同步源 IP
|
||||||
|
# 3) **优先以 ModemManager 日志为重拨标准**(零公网流量):
|
||||||
|
# home→searching / attached→detached / bearer 断连 等 → 等 IPv4 回来 → generate/restart
|
||||||
|
# 4) 不默认做公网出口探针(省流量);仅本地 iface/MM 状态
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
@@ -20,13 +23,31 @@ load_config
|
|||||||
|
|
||||||
INTERVAL="${WATCH_INTERVAL_SEC:-60}"
|
INTERVAL="${WATCH_INTERVAL_SEC:-60}"
|
||||||
DEBOUNCE_SEC="${WATCH_DEBOUNCE_SEC:-3}"
|
DEBOUNCE_SEC="${WATCH_DEBOUNCE_SEC:-3}"
|
||||||
|
# ModemManager 事件驱动(默认开;只读本机 journal,不耗蜂窝公网流量)
|
||||||
|
WATCH_MM_EVENTS="${WATCH_MM_EVENTS:-true}"
|
||||||
|
WATCH_MM_UNIT="${WATCH_MM_UNIT:-ModemManager}"
|
||||||
|
WATCH_MM_SETTLE_SEC="${WATCH_MM_SETTLE_SEC:-5}"
|
||||||
|
WATCH_MM_READY_TIMEOUT_SEC="${WATCH_MM_READY_TIMEOUT_SEC:-90}"
|
||||||
|
WATCH_MM_READY_POLL_SEC="${WATCH_MM_READY_POLL_SEC:-2}"
|
||||||
|
# 同一轮 MM 抖动合并窗口(秒):连续 searching/detached 只恢复一次
|
||||||
|
WATCH_MM_COALESCE_SEC="${WATCH_MM_COALESCE_SEC:-20}"
|
||||||
|
|
||||||
LOCK_DIR="${LOG_DIR:-/var/log/cellular-proxy}"
|
LOCK_DIR="${LOG_DIR:-/var/log/cellular-proxy}"
|
||||||
mkdir -p "$LOCK_DIR" 2>/dev/null || true
|
mkdir -p "$LOCK_DIR" 2>/dev/null || true
|
||||||
STATE_FILE="${LOCK_DIR}/watch.state"
|
STATE_FILE="${LOCK_DIR}/watch.state"
|
||||||
|
MM_CURSOR_FILE="${LOCK_DIR}/mm.cursor"
|
||||||
|
MM_LAST_EVENT_FILE="${LOCK_DIR}/mm.last_event"
|
||||||
LOG_TAG="cellular-proxy-watch"
|
LOG_TAG="cellular-proxy-watch"
|
||||||
|
|
||||||
logw() { printf '[%s] %s %s\n' "$(date '+%F %T')" "$LOG_TAG" "$*" >&2; }
|
logw() { printf '[%s] %s %s\n' "$(date '+%F %T')" "$LOG_TAG" "$*" >&2; }
|
||||||
|
|
||||||
|
truthy() {
|
||||||
|
case "${1,,}" in
|
||||||
|
1|true|yes|on) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
current_cfg_iface() {
|
current_cfg_iface() {
|
||||||
python3 - <<'PY' 2>/dev/null || true
|
python3 - <<'PY' 2>/dev/null || true
|
||||||
import json
|
import json
|
||||||
@@ -60,6 +81,168 @@ print("")
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 将 ModemManager 日志行判定为「重拨 / 脱网 / 重注册」类事件
|
||||||
|
is_mm_redial_line() {
|
||||||
|
local line="${1,,}"
|
||||||
|
# 忽略纯噪声
|
||||||
|
[[ -z "$line" ]] && return 1
|
||||||
|
[[ "$line" == *"-- no entries --"* ]] && return 1
|
||||||
|
|
||||||
|
# 核心:注册/分组附着状态机(本次 16:48 日志即此类)
|
||||||
|
if [[ "$line" == *"registration state changed"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$line" == *"packet service state changed"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$line" == *"consolidated registration state"* && ( "$line" == *"searching"* || "$line" == *"denied"* || "$line" == *"unknown"* ) ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# bearer / 连接态
|
||||||
|
if [[ "$line" == *"bearer"* && ( "$line" == *"disconnect"* || "$line" == *"connect"* || "$line" == *"failed"* ) ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$line" == *"state changed"* && ( "$line" == *"connected"* || "$line" == *"disconnected"* || "$line" == *"searching"* || "$line" == *"registered"* ) ]]; then
|
||||||
|
# 避免把完全无关的 service 状态刷进来:需 modem 语境
|
||||||
|
if [[ "$line" == *"[modem"* || "$line" == *"modem"* || "$line" == *"3gpp"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$line" == *"access technology changed"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$line" == *"unexpected lte system info"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
init_mm_cursor_now() {
|
||||||
|
# 升级/首次启动:锚到「现在」,不回放历史,避免一启动就误 rebind
|
||||||
|
[[ -f "$MM_CURSOR_FILE" ]] && return 0
|
||||||
|
if ! command -v journalctl >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local cur
|
||||||
|
cur="$(journalctl -u "$WATCH_MM_UNIT" -n 0 --show-cursor --no-pager 2>/dev/null | sed -n 's/^-- cursor: //p' | tail -1 || true)"
|
||||||
|
if [[ -n "$cur" ]]; then
|
||||||
|
printf '%s\n' "$cur" >"$MM_CURSOR_FILE"
|
||||||
|
logw "mm cursor initialized (no backlog replay)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 无 journal 时写空标记
|
||||||
|
: >"$MM_CURSOR_FILE"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 扫描 cursor 之后的 MM 日志;命中则打印摘要并 return 0
|
||||||
|
scan_mm_events_since_cursor() {
|
||||||
|
truthy "$WATCH_MM_EVENTS" || return 1
|
||||||
|
command -v journalctl >/dev/null 2>&1 || return 1
|
||||||
|
init_mm_cursor_now || true
|
||||||
|
|
||||||
|
local out hits=0 sample=""
|
||||||
|
# --after-cursor 需要已有 cursor;首次空文件则用 -n 0 建锚后返回无事件
|
||||||
|
if [[ ! -s "$MM_CURSOR_FILE" ]]; then
|
||||||
|
init_mm_cursor_now || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 用临时 cursor 复制,避免 journalctl 在无新条目时搞乱;成功读后再写回
|
||||||
|
local cur
|
||||||
|
cur="$(tr -d '\r\n' <"$MM_CURSOR_FILE" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$cur" ]]; then
|
||||||
|
init_mm_cursor_now || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(journalctl -u "$WATCH_MM_UNIT" --after-cursor="$cur" --no-pager -o short-iso 2>/dev/null || true)"
|
||||||
|
# 推进 cursor 到最新(即使本轮无匹配事件也推进,避免重复扫)
|
||||||
|
local newcur
|
||||||
|
newcur="$(journalctl -u "$WATCH_MM_UNIT" -n 0 --show-cursor --no-pager 2>/dev/null | sed -n 's/^-- cursor: //p' | tail -1 || true)"
|
||||||
|
if [[ -n "$newcur" ]]; then
|
||||||
|
printf '%s\n' "$newcur" >"$MM_CURSOR_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "$out" ]] && return 1
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -z "$line" || "$line" == --* ]] && continue
|
||||||
|
if is_mm_redial_line "$line"; then
|
||||||
|
hits=$((hits + 1))
|
||||||
|
sample="$line"
|
||||||
|
fi
|
||||||
|
done <<<"$out"
|
||||||
|
|
||||||
|
if (( hits > 0 )); then
|
||||||
|
printf '%s\n' "$sample" >"$MM_LAST_EVENT_FILE" 2>/dev/null || true
|
||||||
|
logw "mm redial/re-register events=${hits} last=${sample}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mm_line_looks_recovered() {
|
||||||
|
local line="${1,,}"
|
||||||
|
[[ "$line" == *"registration state changed"*"home"* ]] && return 0
|
||||||
|
[[ "$line" == *"packet service state changed"*"attached"* ]] && return 0
|
||||||
|
[[ "$line" == *"consolidated registration state"* && "$line" == *"'home'"* ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 等待蜂窝数据面可用:iface 存在 + IPv4;可选 mmcli connected
|
||||||
|
wait_cellular_ready() {
|
||||||
|
local cell="${1:-}"
|
||||||
|
local deadline now src
|
||||||
|
deadline=$(( $(date +%s) + WATCH_MM_READY_TIMEOUT_SEC ))
|
||||||
|
logw "wait cellular ready iface=${cell:-?} timeout=${WATCH_MM_READY_TIMEOUT_SEC}s"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
now="$(date +%s)"
|
||||||
|
if (( now >= deadline )); then
|
||||||
|
logw "wait cellular ready: timeout"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
load_config
|
||||||
|
cell="${CELLULAR_IFACE:-$cell}"
|
||||||
|
if [[ -z "$cell" ]] || ! iface_exists "$cell"; then
|
||||||
|
cell="$(detect_cellular_iface "" || true)"
|
||||||
|
fi
|
||||||
|
if [[ -n "$cell" ]] && iface_exists "$cell"; then
|
||||||
|
src="$(detect_source_ip "$cell" || true)"
|
||||||
|
if [[ -n "$src" ]]; then
|
||||||
|
# 若 mmcli 可用,尽量确认 modem 已 connected/attached(失败不阻断,有 IPv4 即可)
|
||||||
|
if command -v mmcli >/dev/null 2>&1; then
|
||||||
|
if mmcli -L 2>/dev/null | grep -qi modem; then
|
||||||
|
if mmcli -m any 2>/dev/null | grep -qiE 'state:[[:space:]]*connected|packet service state:[[:space:]]*attached|registration:[[:space:]]*home'; then
|
||||||
|
logw "cellular ready: iface=$cell src=$src (mmcli ok)"
|
||||||
|
printf '%s\n' "$cell"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 有 IPv4 但 mmcli 尚未 home:再等一小会儿
|
||||||
|
sleep "$WATCH_MM_READY_POLL_SEC"
|
||||||
|
src2="$(detect_source_ip "$cell" || true)"
|
||||||
|
if [[ -n "$src2" ]]; then
|
||||||
|
logw "cellular ready: iface=$cell src=$src2 (ipv4 present; mmcli not yet home)"
|
||||||
|
printf '%s\n' "$cell"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
logw "cellular ready: iface=$cell src=$src"
|
||||||
|
printf '%s\n' "$cell"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
logw "cellular ready: iface=$cell src=$src"
|
||||||
|
printf '%s\n' "$cell"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep "$WATCH_MM_READY_POLL_SEC"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
need_refresh() {
|
need_refresh() {
|
||||||
load_config
|
load_config
|
||||||
local cell="${CELLULAR_IFACE:-}"
|
local cell="${CELLULAR_IFACE:-}"
|
||||||
@@ -74,9 +257,8 @@ need_refresh() {
|
|||||||
|
|
||||||
live_src="$(detect_source_ip "$cell" || true)"
|
live_src="$(detect_source_ip "$cell" || true)"
|
||||||
if [[ -z "$live_src" ]]; then
|
if [[ -z "$live_src" ]]; then
|
||||||
# 网卡在但无 IPv4:可能刚重拨;不立刻 die,等下一轮
|
# 网卡在但无 IPv4:可能刚重拨;不立刻 die,等下一轮或 MM 恢复路径
|
||||||
logw "wwan up but no IPv4 yet on $cell"
|
logw "wwan up but no IPv4 yet on $cell"
|
||||||
# 若配置里仍绑旧源 IP,清掉/重生更安全
|
|
||||||
if [[ -n "$cfg_src" ]]; then
|
if [[ -n "$cfg_src" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -106,21 +288,22 @@ do_refresh() {
|
|||||||
load_config
|
load_config
|
||||||
local cell="${CELLULAR_IFACE:-}"
|
local cell="${CELLULAR_IFACE:-}"
|
||||||
local src=""
|
local src=""
|
||||||
|
local reason="${1:-manual}"
|
||||||
|
|
||||||
if [[ -z "$cell" ]] || ! iface_exists "$cell"; then
|
if [[ -z "$cell" ]] || ! iface_exists "$cell"; then
|
||||||
cell="$(detect_cellular_iface "" || true)"
|
cell="$(detect_cellular_iface "" || true)"
|
||||||
if [[ -z "$cell" ]]; then
|
if [[ -z "$cell" ]]; then
|
||||||
logw "detect failed; skip"
|
logw "detect failed; skip reason=$reason"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
src="$(detect_source_ip "$cell" || true)"
|
src="$(detect_source_ip "$cell" || true)"
|
||||||
if [[ -z "$src" ]]; then
|
if [[ -z "$src" ]]; then
|
||||||
logw "no IPv4 on $cell yet; skip generate"
|
logw "no IPv4 on $cell yet; skip generate reason=$reason"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" || true
|
persist_cellular_to_settings "$CONFIG_FILE" "$cell" "$src" || true
|
||||||
logw "refresh: iface=$cell src=$src"
|
logw "refresh: reason=$reason iface=$cell src=$src"
|
||||||
# 走 cpxy generate(含 check + restart);失败则直接 scripts
|
# 走 cpxy generate(含 check + restart);失败则直接 scripts
|
||||||
if command -v cpxy >/dev/null 2>&1; then
|
if command -v cpxy >/dev/null 2>&1; then
|
||||||
cpxy generate >/dev/null 2>&1 || {
|
cpxy generate >/dev/null 2>&1 || {
|
||||||
@@ -137,57 +320,120 @@ do_refresh() {
|
|||||||
install -m 0644 "$ROOT_DIR/generated/config.json" "$ROOT_DIR/etc/config.json"
|
install -m 0644 "$ROOT_DIR/generated/config.json" "$ROOT_DIR/etc/config.json"
|
||||||
systemctl restart cellular-proxy 2>/dev/null || true
|
systemctl restart cellular-proxy 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
printf '%s iface=%s src=%s\n' "$(date -Iseconds)" "$cell" "$src" >"$STATE_FILE" 2>/dev/null || true
|
printf '%s reason=%s iface=%s src=%s\n' "$(date -Iseconds)" "$reason" "$cell" "$src" >"$STATE_FILE" 2>/dev/null || true
|
||||||
logw "refresh done"
|
logw "refresh done reason=$reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
# oneshot mode (systemd timer)
|
# MM 事件后:settle → 等数据面 → refresh(即使未 pin 源 IP,也重启清半开连接)
|
||||||
if [[ "${1:-}" == "--once" ]]; then
|
handle_mm_recover() {
|
||||||
if need_refresh; then
|
local now last=0
|
||||||
do_refresh || true
|
now="$(date +%s)"
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
# 合并窗口:避免 searching→registering→home 连打三次
|
||||||
|
last="$(stat -c %Y "$STATE_FILE" 2>/dev/null || echo 0)"
|
||||||
|
if (( now - last < WATCH_MM_COALESCE_SEC )); then
|
||||||
|
# 若 state 很新且是 mm 恢复,跳过
|
||||||
|
if grep -q 'reason=mm' "$STATE_FILE" 2>/dev/null; then
|
||||||
|
logw "mm recover coalesced (<${WATCH_MM_COALESCE_SEC}s)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# long-running: periodic + optional ip monitor events
|
sleep "${WATCH_MM_SETTLE_SEC}"
|
||||||
logw "start interval=${INTERVAL}s (debounce=${DEBOUNCE_SEC}s)"
|
local cell=""
|
||||||
|
cell="$(wait_cellular_ready "${CELLULAR_IFACE:-}" || true)"
|
||||||
|
if [[ -z "$cell" ]]; then
|
||||||
|
# 超时仍尝试一轮 need_refresh / refresh(可能仅有短暂 IPv4)
|
||||||
|
logw "mm recover: ready wait failed; try best-effort refresh"
|
||||||
|
fi
|
||||||
|
do_refresh "mm" || true
|
||||||
|
}
|
||||||
|
|
||||||
last_run=0
|
last_run=0
|
||||||
trigger() {
|
trigger() {
|
||||||
local now
|
local now reason="${1:-poll}"
|
||||||
now="$(date +%s)"
|
now="$(date +%s)"
|
||||||
if (( now - last_run < DEBOUNCE_SEC )); then
|
if (( now - last_run < DEBOUNCE_SEC )); then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
last_run=$now
|
last_run=$now
|
||||||
|
|
||||||
|
# 1) ModemManager 重拨标准(零公网流量)
|
||||||
|
if scan_mm_events_since_cursor; then
|
||||||
|
handle_mm_recover
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) 原有 iface / stale pin 逻辑
|
||||||
if need_refresh; then
|
if need_refresh; then
|
||||||
do_refresh || true
|
do_refresh "$reason" || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# oneshot mode (systemd timer)
|
||||||
|
if [[ "${1:-}" == "--once" ]]; then
|
||||||
|
trigger "once"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# long-running: periodic + ip monitor + MM journal follow
|
||||||
|
logw "start interval=${INTERVAL}s debounce=${DEBOUNCE_SEC}s mm_events=${WATCH_MM_EVENTS} mm_unit=${WATCH_MM_UNIT}"
|
||||||
|
init_mm_cursor_now || true
|
||||||
|
|
||||||
# initial
|
# initial
|
||||||
trigger
|
trigger "start"
|
||||||
|
|
||||||
|
child_pids=()
|
||||||
|
cleanup() {
|
||||||
|
local p
|
||||||
|
for p in "${child_pids[@]:-}"; do
|
||||||
|
kill "$p" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap cleanup TERM INT
|
||||||
|
|
||||||
# Prefer event-driven if available; always keep poll as safety net
|
# Prefer event-driven if available; always keep poll as safety net
|
||||||
if command -v ip >/dev/null 2>&1; then
|
if command -v ip >/dev/null 2>&1; then
|
||||||
(
|
(
|
||||||
# address/link changes on any iface — cheap filter in shell
|
|
||||||
ip -o monitor address link 2>/dev/null | while read -r line; do
|
ip -o monitor address link 2>/dev/null | while read -r line; do
|
||||||
cell="${CELLULAR_IFACE:-}"
|
cell="${CELLULAR_IFACE:-}"
|
||||||
# reload cell from settings occasionally
|
|
||||||
if [[ -f "$CONFIG_FILE" ]]; then
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
cell="$(awk -F= '/^CELLULAR_IFACE=/{print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
cell="$(awk -F= '/^CELLULAR_IFACE=/{print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
if [[ -n "$cell" ]] && [[ "$line" == *"$cell"* || "$line" == *Deleted* || "$line" == *wwan* || "$line" == *usb* || "$line" == *enx* ]]; then
|
if [[ -n "$cell" ]] && [[ "$line" == *"$cell"* || "$line" == *Deleted* || "$line" == *wwan* || "$line" == *usb* || "$line" == *enx* ]]; then
|
||||||
sleep "$DEBOUNCE_SEC"
|
sleep "$DEBOUNCE_SEC"
|
||||||
trigger
|
trigger "ip-monitor"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
mon_pid=$!
|
child_pids+=($!)
|
||||||
trap 'kill $mon_pid 2>/dev/null || true; exit 0' TERM INT
|
fi
|
||||||
|
|
||||||
|
# ModemManager 实时日志(标准:重拨/重注册)
|
||||||
|
if truthy "$WATCH_MM_EVENTS" && command -v journalctl >/dev/null 2>&1; then
|
||||||
|
(
|
||||||
|
# -n 0:不回放历史;-f:跟随
|
||||||
|
journalctl -u "$WATCH_MM_UNIT" -f -n 0 --no-pager 2>/dev/null | while IFS= read -r line; do
|
||||||
|
if is_mm_redial_line "$line"; then
|
||||||
|
logw "mm live: $line"
|
||||||
|
printf '%s\n' "$line" >"$MM_LAST_EVENT_FILE" 2>/dev/null || true
|
||||||
|
# 推进 cursor,避免 poll 再扫到同一批
|
||||||
|
newcur="$(journalctl -u "$WATCH_MM_UNIT" -n 0 --show-cursor --no-pager 2>/dev/null | sed -n 's/^-- cursor: //p' | tail -1 || true)"
|
||||||
|
[[ -n "$newcur" ]] && printf '%s\n' "$newcur" >"$MM_CURSOR_FILE"
|
||||||
|
sleep "$DEBOUNCE_SEC"
|
||||||
|
handle_mm_recover
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
child_pids+=($!)
|
||||||
|
logw "mm journal follow enabled unit=$WATCH_MM_UNIT"
|
||||||
|
else
|
||||||
|
logw "mm journal follow disabled or journalctl missing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
sleep "$INTERVAL"
|
sleep "$INTERVAL"
|
||||||
trigger
|
trigger "poll"
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user