router_local_update.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. #!/bin/sh
  2. set -eu
  3. timestamp() {
  4. date '+%Y-%m-%d %H:%M:%S'
  5. }
  6. log() {
  7. printf '[%s] %s\n' "$(timestamp)" "$*"
  8. }
  9. log_err() {
  10. printf '[%s] %s\n' "$(timestamp)" "$*" >&2
  11. }
  12. SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
  13. APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
  14. CONFIG_PATH=${1:-"$APP_DIR/config_router.conf"}
  15. if [ ! -r "$CONFIG_PATH" ]; then
  16. log_err "[router-local] config not found: $CONFIG_PATH"
  17. exit 1
  18. fi
  19. # shellcheck disable=SC1090
  20. . "$CONFIG_PATH"
  21. CFST_WORK_DIR=${CFST_WORK_DIR:-"$APP_DIR/cfst"}
  22. CFST_BIN=${CFST_BIN:-"./cfst"}
  23. CFST_IP_FILE=${CFST_IP_FILE:-"ip.txt"}
  24. CFST_RESULT_FILE=${CFST_RESULT_FILE:-"result.csv"}
  25. CFST_DISPLAY_COUNT=${CFST_DISPLAY_COUNT:-10}
  26. CFST_THREADS=${CFST_THREADS:-}
  27. CFST_TEST_COUNT=${CFST_TEST_COUNT:-}
  28. CFST_DOWNLOAD_COUNT=${CFST_DOWNLOAD_COUNT:-}
  29. CFST_DOWNLOAD_TIME=${CFST_DOWNLOAD_TIME:-}
  30. CFST_PORT=${CFST_PORT:-443}
  31. CFST_URL=${CFST_URL:-}
  32. CFST_HTTPING=${CFST_HTTPING:-0}
  33. CFST_HTTPING_CODE=${CFST_HTTPING_CODE:-}
  34. CFST_CFCOLO=${CFST_CFCOLO:-}
  35. CFST_LATENCY_LIMIT=${CFST_LATENCY_LIMIT:-}
  36. CFST_LATENCY_LOWER=${CFST_LATENCY_LOWER:-}
  37. CFST_LOSS_LIMIT=${CFST_LOSS_LIMIT:-}
  38. CFST_SPEED_LIMIT=${CFST_SPEED_LIMIT:-}
  39. CFST_DISABLE_DOWNLOAD=${CFST_DISABLE_DOWNLOAD:-0}
  40. CFST_ALL_IP=${CFST_ALL_IP:-0}
  41. CFST_DEBUG=${CFST_DEBUG:-0}
  42. CFST_SKIP_RUN=${CFST_SKIP_RUN:-0}
  43. CFST_QUIET_LOG=${CFST_QUIET_LOG:-1}
  44. CFST_SHOW_LOG_ON_TTY=${CFST_SHOW_LOG_ON_TTY:-1}
  45. TOP_N=${TOP_N:-3}
  46. RUNTIME_DIR=${RUNTIME_DIR:-"$APP_DIR/cfip_runtime"}
  47. VALUE_TEXT_FILE=${VALUE_TEXT_FILE:-"current_ip.txt"}
  48. VALUE_JSON_FILE=${VALUE_JSON_FILE:-"current_ip.json"}
  49. STATE_FILE=${STATE_FILE:-"state.json"}
  50. EXPORT_VARS_FILE=${EXPORT_VARS_FILE:-"substore_vars.json"}
  51. VALUE_JSON_KEY=${VALUE_JSON_KEY:-ip}
  52. STATE_LAST_GOOD_KEY=${STATE_LAST_GOOD_KEY:-last_good_ip}
  53. EXPORT_VALUE_KEY=${EXPORT_VALUE_KEY:-AUTO_CFIP}
  54. resolve_path() {
  55. _p="$1"
  56. _base_dir="$2"
  57. case "$_p" in
  58. /*) printf '%s\n' "$_p" ;;
  59. *) printf '%s/%s\n' "$_base_dir" "$_p" ;;
  60. esac
  61. }
  62. CURRENT_TEXT_PATH=$(resolve_path "$VALUE_TEXT_FILE" "$RUNTIME_DIR")
  63. CURRENT_JSON_PATH=$(resolve_path "$VALUE_JSON_FILE" "$RUNTIME_DIR")
  64. STATE_PATH=$(resolve_path "$STATE_FILE" "$RUNTIME_DIR")
  65. EXPORT_VARS_PATH=$(resolve_path "$EXPORT_VARS_FILE" "$RUNTIME_DIR")
  66. INDEX_PATH="$RUNTIME_DIR/index.html"
  67. RESULT_PATH=$(resolve_path "$CFST_RESULT_FILE" "$CFST_WORK_DIR")
  68. UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  69. mkdir -p "$RUNTIME_DIR"
  70. json_get_string() {
  71. key="$1"
  72. file="$2"
  73. if [ ! -f "$file" ]; then
  74. return 0
  75. fi
  76. sed -n "s/.*\"$key\":[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" | head -n 1
  77. }
  78. json_escape() {
  79. printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
  80. }
  81. build_top_candidates_json() {
  82. awk -F',' -v top_n="$TOP_N" '
  83. function trim(v) {
  84. gsub(/\r/, "", v)
  85. sub(/^[[:space:]]+/, "", v)
  86. sub(/[[:space:]]+$/, "", v)
  87. return v
  88. }
  89. function esc(v) {
  90. gsub(/\\/,"\\\\",v)
  91. gsub(/"/,"\\\"",v)
  92. return v
  93. }
  94. NR == 1 { next }
  95. count >= top_n { exit }
  96. NF < 1 { next }
  97. { if (trim($1) == "") next }
  98. {
  99. ip = esc(trim($1))
  100. sent = esc(trim($2))
  101. received = esc(trim($3))
  102. loss = esc(trim($4))
  103. latency = esc(trim($5))
  104. speed = esc(trim($6))
  105. region = esc(trim($7))
  106. if (count > 0) {
  107. printf(",")
  108. }
  109. printf("{\"domain\":\"%s\",\"ip\":\"%s\",\"source_type\":\"cfst_local_busybox\",\"sent\":\"%s\",\"received\":\"%s\",\"loss_rate\":\"%s\",\"avg_latency\":\"%s\",\"download_speed\":\"%s\",\"region\":\"%s\",\"location_country\":\"\",\"location_city\":\"\",\"host_provider\":\"\",\"score_value\":null,\"scores\":[],\"created_raw\":\"\"}", ip, ip, sent, received, loss, latency, speed, region)
  110. count++
  111. }
  112. END {
  113. if (count == 0) {
  114. printf("")
  115. }
  116. }
  117. ' "$RESULT_PATH"
  118. }
  119. atomic_write_file() {
  120. _dest="$1"
  121. _tmp="${_dest}.tmp.$$"
  122. cat > "$_tmp" && mv -f "$_tmp" "$_dest"
  123. }
  124. write_text_value() {
  125. selected_ip="$1"
  126. _tmp_txt="${CURRENT_TEXT_PATH}.tmp.$$"
  127. printf '%s\n' "$selected_ip" > "$_tmp_txt" && mv -f "$_tmp_txt" "$CURRENT_TEXT_PATH"
  128. rm -f "$INDEX_PATH"
  129. cp "$CURRENT_TEXT_PATH" "$INDEX_PATH"
  130. }
  131. write_success_outputs() {
  132. selected_ip="$1"
  133. top_candidates_json="$2"
  134. source_count="$3"
  135. escaped_ip=$(json_escape "$selected_ip")
  136. write_text_value "$selected_ip"
  137. atomic_write_file "$CURRENT_JSON_PATH" <<EOF
  138. {
  139. "$VALUE_JSON_KEY": "$escaped_ip",
  140. "updated_at": "$UPDATED_AT",
  141. "status": "ok",
  142. "source_type": "cfst_local_busybox",
  143. "source_count": $source_count,
  144. "checked_count": 0,
  145. "top_candidates": [${top_candidates_json}]
  146. }
  147. EOF
  148. if [ "$selected_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ] || [ "${LAST_STATE_STATUS:-}" != "ok" ]; then
  149. atomic_write_file "$STATE_PATH" <<EOF
  150. {
  151. "updated_at": "$UPDATED_AT",
  152. "$STATE_LAST_GOOD_KEY": "$escaped_ip",
  153. "status": "ok",
  154. "source_count": $source_count,
  155. "checked_count": 0,
  156. "source_type": "cfst_local_busybox"
  157. }
  158. EOF
  159. fi
  160. atomic_write_file "$EXPORT_VARS_PATH" <<EOF
  161. {
  162. "$EXPORT_VALUE_KEY": "$escaped_ip",
  163. "UPDATED_AT": "$UPDATED_AT",
  164. "STATUS": "ok"
  165. }
  166. EOF
  167. }
  168. write_error_with_fallback() {
  169. last_good_ip="$1"
  170. error_message="$2"
  171. escaped_ip=$(json_escape "$last_good_ip")
  172. escaped_error=$(json_escape "$error_message")
  173. write_text_value "$last_good_ip"
  174. atomic_write_file "$CURRENT_JSON_PATH" <<EOF
  175. {
  176. "$VALUE_JSON_KEY": "$escaped_ip",
  177. "updated_at": "$UPDATED_AT",
  178. "status": "error_use_last_good",
  179. "error": "$escaped_error",
  180. "source_type": "cfst_local_busybox"
  181. }
  182. EOF
  183. if [ "$last_good_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ]; then
  184. atomic_write_file "$STATE_PATH" <<EOF
  185. {
  186. "updated_at": "$UPDATED_AT",
  187. "$STATE_LAST_GOOD_KEY": "$escaped_ip",
  188. "status": "error",
  189. "error": "$escaped_error",
  190. "source_type": "cfst_local_busybox"
  191. }
  192. EOF
  193. fi
  194. atomic_write_file "$EXPORT_VARS_PATH" <<EOF
  195. {
  196. "$EXPORT_VALUE_KEY": "$escaped_ip",
  197. "UPDATED_AT": "$UPDATED_AT",
  198. "STATUS": "error_use_last_good"
  199. }
  200. EOF
  201. }
  202. run_cfst() (
  203. cd "$CFST_WORK_DIR"
  204. set -- "$CFST_BIN" -f "$CFST_IP_FILE" -o "$CFST_RESULT_FILE" -p "$CFST_DISPLAY_COUNT" -tp "$CFST_PORT"
  205. if [ -n "$CFST_THREADS" ]; then
  206. set -- "$@" -n "$CFST_THREADS"
  207. fi
  208. if [ -n "$CFST_TEST_COUNT" ]; then
  209. set -- "$@" -t "$CFST_TEST_COUNT"
  210. fi
  211. if [ -n "$CFST_DOWNLOAD_COUNT" ]; then
  212. set -- "$@" -dn "$CFST_DOWNLOAD_COUNT"
  213. fi
  214. if [ -n "$CFST_DOWNLOAD_TIME" ]; then
  215. set -- "$@" -dt "$CFST_DOWNLOAD_TIME"
  216. fi
  217. if [ -n "$CFST_URL" ]; then
  218. set -- "$@" -url "$CFST_URL"
  219. fi
  220. if [ "$CFST_HTTPING" = "1" ]; then
  221. set -- "$@" -httping
  222. fi
  223. if [ -n "$CFST_HTTPING_CODE" ]; then
  224. set -- "$@" -httping-code "$CFST_HTTPING_CODE"
  225. fi
  226. if [ -n "$CFST_CFCOLO" ]; then
  227. set -- "$@" -cfcolo "$CFST_CFCOLO"
  228. fi
  229. if [ -n "$CFST_LATENCY_LIMIT" ]; then
  230. set -- "$@" -tl "$CFST_LATENCY_LIMIT"
  231. fi
  232. if [ -n "$CFST_LATENCY_LOWER" ]; then
  233. set -- "$@" -tll "$CFST_LATENCY_LOWER"
  234. fi
  235. if [ -n "$CFST_LOSS_LIMIT" ]; then
  236. set -- "$@" -tlr "$CFST_LOSS_LIMIT"
  237. fi
  238. if [ -n "$CFST_SPEED_LIMIT" ]; then
  239. set -- "$@" -sl "$CFST_SPEED_LIMIT"
  240. fi
  241. if [ "$CFST_DISABLE_DOWNLOAD" = "1" ]; then
  242. set -- "$@" -dd
  243. fi
  244. if [ "$CFST_ALL_IP" = "1" ]; then
  245. set -- "$@" -allip
  246. fi
  247. if [ "$CFST_DEBUG" = "1" ]; then
  248. set -- "$@" -debug
  249. fi
  250. # Show cfst live logs for manual interactive runs, keep quiet for redirected/background runs.
  251. is_interactive_tty=0
  252. if [ "$CFST_SHOW_LOG_ON_TTY" = "1" ] && [ -t 1 ]; then
  253. is_interactive_tty=1
  254. fi
  255. if [ "$CFST_QUIET_LOG" = "1" ] && [ "$CFST_DEBUG" != "1" ] && [ "$is_interactive_tty" != "1" ]; then
  256. "$@" >/dev/null 2>&1
  257. else
  258. "$@"
  259. fi
  260. )
  261. log_update_summary() {
  262. selected_ip="$1"
  263. source_count="$2"
  264. log "[router-local] update_at=$UPDATED_AT selected_ip=$selected_ip candidates=$source_count"
  265. awk -F',' -v top_n="$TOP_N" '
  266. function trim(v) {
  267. gsub(/\r/, "", v)
  268. sub(/^[[:space:]]+/, "", v)
  269. sub(/[[:space:]]+$/, "", v)
  270. return v
  271. }
  272. NR == 1 { next }
  273. count >= top_n { exit }
  274. NF < 1 { next }
  275. { if (trim($1) == "") next }
  276. {
  277. ip = trim($1)
  278. sent = trim($2)
  279. received = trim($3)
  280. loss = trim($4)
  281. latency = trim($5)
  282. speed = trim($6)
  283. region = trim($7)
  284. count++
  285. printf("[router-local] candidate_%d ip=%s sent=%s recv=%s loss=%s latency=%s speed=%s region=%s\n", count, ip, sent, received, loss, latency, speed, region)
  286. }
  287. ' "$RESULT_PATH"
  288. }
  289. LAST_GOOD_IP=$(json_get_string "$STATE_LAST_GOOD_KEY" "$STATE_PATH")
  290. LAST_STATE_STATUS=$(json_get_string "status" "$STATE_PATH")
  291. if [ "$CFST_SKIP_RUN" != "1" ]; then
  292. if ! run_cfst; then
  293. if [ -n "$LAST_GOOD_IP" ]; then
  294. write_error_with_fallback "$LAST_GOOD_IP" "cfst run failed"
  295. log "[router-local] cfst run failed, fallback to last good ip: $LAST_GOOD_IP"
  296. exit 0
  297. fi
  298. log_err "[router-local] cfst run failed and no last good ip available"
  299. exit 1
  300. fi
  301. fi
  302. if [ ! -s "$RESULT_PATH" ]; then
  303. if [ -n "$LAST_GOOD_IP" ]; then
  304. write_error_with_fallback "$LAST_GOOD_IP" "cfst result file missing or empty"
  305. log "[router-local] empty result, fallback to last good ip: $LAST_GOOD_IP"
  306. exit 0
  307. fi
  308. log_err "[router-local] result file missing or empty: $RESULT_PATH"
  309. exit 1
  310. fi
  311. if [ "${CFST_DISABLE_DOWNLOAD:-0}" = "1" ]; then
  312. BEST_LINE=$(sed -n '2p' "$RESULT_PATH" | tr -d '\r')
  313. else
  314. # 优先挑选下载测速大于 0 的最优行
  315. BEST_LINE=$(awk -F',' '
  316. function trim(v) { gsub(/\r/, "", v); sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v); return v }
  317. NR > 1 && NF >= 6 {
  318. speed = trim($6) + 0;
  319. if (speed > 0) { print $0; exit }
  320. }
  321. ' "$RESULT_PATH" | tr -d '\r')
  322. # 若所有候选均为 0.00 下载速度(测速失败)且有 last_good,置空以触发 fallback
  323. if [ -z "$BEST_LINE" ] && [ -n "$LAST_GOOD_IP" ]; then
  324. BEST_LINE=""
  325. elif [ -z "$BEST_LINE" ]; then
  326. BEST_LINE=$(sed -n '2p' "$RESULT_PATH" | tr -d '\r')
  327. fi
  328. fi
  329. BEST_IP=$(printf '%s' "$BEST_LINE" | cut -d',' -f1 | tr -d ' ')
  330. SOURCE_COUNT=$(awk -F',' 'NR > 1 && NF > 0 { count++ } END { print count + 0 }' "$RESULT_PATH")
  331. TOP_CANDIDATES_JSON=$(build_top_candidates_json)
  332. if [ -z "$BEST_IP" ]; then
  333. if [ -n "$LAST_GOOD_IP" ]; then
  334. write_error_with_fallback "$LAST_GOOD_IP" "no valid ip found in cfst result"
  335. log "[router-local] no valid ip found, fallback to last good ip: $LAST_GOOD_IP"
  336. exit 0
  337. fi
  338. log_err "[router-local] no valid ip found in result file"
  339. exit 1
  340. fi
  341. write_success_outputs "$BEST_IP" "$TOP_CANDIDATES_JSON" "$SOURCE_COUNT"
  342. log_update_summary "$BEST_IP" "$SOURCE_COUNT"