Commit 3efd019e authored by Vitaly Lipatov's avatar Vitaly Lipatov

route-update: add flock, duration tracking; web-api: fix NOIP, reorder buttons

- route-update.sh: add flock to prevent concurrent runs - route-update.sh: measure and save execution duration - route-web-api.py: skip IPv6 checks for domains without AAAA (NOIP) - route-web-api.py: move "Проверить" button first, add Enter key handler - route-web-api.py: show update duration in status bar - route-web-api.py: expose duration in /api/status Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent bd2a6117
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
cd "$(dirname "$(realpath "$0")")" || exit cd "$(dirname "$(realpath "$0")")" || exit
# Prevent concurrent runs
LOCKFILE="/var/lock/route-update.lock"
exec 9>"$LOCKFILE"
flock -n 9 || { echo "$(date +%H:%M:%S) Another route-update.sh is running, skipping" >&2; exit 0; }
. ./functions . ./functions
ROUTES_DIR=routes.d ROUTES_DIR=routes.d
...@@ -26,6 +31,7 @@ PREF_STEP=10 ...@@ -26,6 +31,7 @@ PREF_STEP=10
BIRD_AS=65000 BIRD_AS=65000
BIRD_CONF="/etc/bird/route-tables.conf" BIRD_CONF="/etc/bird/route-tables.conf"
DURATION_FILE="/home/routeweb/route-web-api/update-duration"
FORCE= FORCE=
RESOLVE= RESOLVE=
...@@ -1047,6 +1053,8 @@ check_list_duplicates() ...@@ -1047,6 +1053,8 @@ check_list_duplicates()
} }
# --- Main --- # --- Main ---
_start_time=$(date +%s)
[ -n "$SHOW" ] && log "Dry-run mode (no changes will be made)" [ -n "$SHOW" ] && log "Dry-run mode (no changes will be made)"
[ -n "$VERBOSE" ] && log "Verbose mode, state_dir=$STATE_DIR" [ -n "$VERBOSE" ] && log "Verbose mode, state_dir=$STATE_DIR"
...@@ -1064,4 +1072,6 @@ cleanup_state ...@@ -1064,4 +1072,6 @@ cleanup_state
generate_bird_config generate_bird_config
generate_web_json generate_web_json
[ -n "$SHOW" ] || log "All done" _duration=$(( $(date +%s) - _start_time ))
[ -n "$SHOW" ] || echo "$_duration" > "$DURATION_FILE" 2>/dev/null
[ -n "$SHOW" ] || log "All done in ${_duration}s"
...@@ -37,6 +37,7 @@ LIST_FILES = { ...@@ -37,6 +37,7 @@ LIST_FILES = {
} }
ALL_ROUTES_JSON = os.path.join(BASEDIR, "all-routes.json") ALL_ROUTES_JSON = os.path.join(BASEDIR, "all-routes.json")
DURATION_FILE = os.path.join(BASEDIR, "update-duration")
MAX_ENTRIES = 500 MAX_ENTRIES = 500
UPDATE_INTERVAL = 300 # route-update.sh cycle, seconds UPDATE_INTERVAL = 300 # route-update.sh cycle, seconds
...@@ -274,6 +275,14 @@ def check_site(domain): ...@@ -274,6 +275,14 @@ def check_site(domain):
"""Check domain: resolve IPs, find in route lists, whois, test gateways.""" """Check domain: resolve IPs, find in route lists, whois, test gateways."""
url = "https://%s/" % domain url = "https://%s/" % domain
# Check if domain has AAAA records
has_v6 = False
try:
socket.getaddrinfo(domain, None, socket.AF_INET6, socket.SOCK_STREAM)
has_v6 = True
except socket.gaierror:
pass
# Phase 1: gateway checks (IPv4 + IPv6), whois, and fetch assets in parallel # Phase 1: gateway checks (IPv4 + IPv6), whois, and fetch assets in parallel
checks = {} checks = {}
workers = len(CHECK_GATEWAYS) * 2 + 2 workers = len(CHECK_GATEWAYS) * 2 + 2
...@@ -281,7 +290,8 @@ def check_site(domain): ...@@ -281,7 +290,8 @@ def check_site(domain):
gw_futures = [] gw_futures = []
for name, proxy in CHECK_GATEWAYS: for name, proxy in CHECK_GATEWAYS:
gw_futures.append(pool.submit(_check_one, name, proxy, url, "-4")) gw_futures.append(pool.submit(_check_one, name, proxy, url, "-4"))
gw_futures.append(pool.submit(_check_one, name, proxy, url, "-6")) if has_v6:
gw_futures.append(pool.submit(_check_one, name, proxy, url, "-6"))
whois_future = pool.submit(get_whois, domain) whois_future = pool.submit(get_whois, domain)
# Fetch HTML and parse assets via first available gateway # Fetch HTML and parse assets via first available gateway
assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url) assets_future = pool.submit(_find_assets, CHECK_GATEWAYS[0][1], url)
...@@ -297,6 +307,14 @@ def check_site(domain): ...@@ -297,6 +307,14 @@ def check_site(domain):
checks[name]["status_v6"] = status checks[name]["status_v6"] = status
checks[name]["http_code_v6"] = code checks[name]["http_code_v6"] = code
# Fill NOIP for gateways when domain has no AAAA
if not has_v6:
for name, _proxy in CHECK_GATEWAYS:
if name not in checks:
checks[name] = {}
checks[name]["status_v6"] = "NOIP"
checks[name]["http_code_v6"] = 0
whois_info = whois_future.result() whois_info = whois_future.result()
assets = assets_future.result() assets = assets_future.result()
...@@ -484,7 +502,7 @@ OPENAPI_SPEC = { ...@@ -484,7 +502,7 @@ OPENAPI_SPEC = {
"/api/check": { "/api/check": {
"post": { "post": {
"summary": "Проверка блокировки", "summary": "Проверка блокировки",
"description": "Проверяет доступность домена/IP через все шлюзы параллельно (curl через SOCKS5).", "description": "Проверяет доступность домена/IP напрямую (policy routing) и через все шлюзы параллельно (curl через SOCKS5).",
"requestBody": {"required": True, "content": {"application/json": {"schema": { "requestBody": {"required": True, "content": {"application/json": {"schema": {
"type": "object", "type": "object",
"required": ["domain"], "required": ["domain"],
...@@ -686,11 +704,12 @@ HTML_PAGE = """\ ...@@ -686,11 +704,12 @@ HTML_PAGE = """\
<div class="content"> <div class="content">
<div class="input-row"> <div class="input-row">
<input type="text" id="domain" placeholder="домен, URL или IP-адрес" <input type="text" id="domain" placeholder="домен, URL или IP-адрес"
autofocus autocomplete="off"> autofocus autocomplete="off"
onkeydown="if(event.key==='Enter'){event.preventDefault();checkDomain();}">
<button class="btn-check" id="btn-check" onclick="checkDomain()">Проверить</button>
<button class="btn-bypass" onclick="addEntry('bypass')">Обход (egw)</button> <button class="btn-bypass" onclick="addEntry('bypass')">Обход (egw)</button>
<button class="btn-direct" onclick="addEntry('direct')">Напрямую (dgw)</button> <button class="btn-direct" onclick="addEntry('direct')">Напрямую (dgw)</button>
<button class="btn-geo" onclick="addEntry('geo')">Geo (gre)</button> <button class="btn-geo" onclick="addEntry('geo')">Geo (gre)</button>
<button class="btn-check" id="btn-check" onclick="checkDomain()">Проверить</button>
</div> </div>
<div id="check-result" class="check-result"> <div id="check-result" class="check-result">
<h3>Проверка: <span id="check-domain"></span></h3> <h3>Проверка: <span id="check-domain"></span></h3>
...@@ -936,7 +955,7 @@ function renderCheck(data) { ...@@ -936,7 +955,7 @@ function renderCheck(data) {
el4.textContent = name + ': ' + st4; el4.textContent = name + ': ' + st4;
if (gwThr && gwThr.throttled) el4.title = gwThr.asset + ': ' + gwThr.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + gwThr.time + '\\u0441'; if (gwThr && gwThr.throttled) el4.title = gwThr.asset + ': ' + gwThr.size + ' \\u0431\\u0430\\u0439\\u0442 \\u0437\\u0430 ' + gwThr.time + '\\u0441';
gwrap.appendChild(el4); gwrap.appendChild(el4);
if (info.status_v6 && info.status_v6 !== 'PROXY?') { if (info.status_v6 && info.status_v6 !== 'PROXY?' && info.status_v6 !== 'NOIP') {
const cls6 = info.status_v6 === 'OK' ? 'ok' : info.status_v6 === 'BLOCK' ? 'block' : 'other'; const cls6 = info.status_v6 === 'OK' ? 'ok' : info.status_v6 === 'BLOCK' ? 'block' : 'other';
const el6 = document.createElement('span'); const el6 = document.createElement('span');
el6.className = 'check-gw ' + cls6; el6.className = 'check-gw ' + cls6;
...@@ -1033,7 +1052,9 @@ refresh(); ...@@ -1033,7 +1052,9 @@ refresh();
fetch('/api/status').then(r => r.json()).then(d => { fetch('/api/status').then(r => r.json()).then(d => {
if (d.updated) { if (d.updated) {
const ts = new Date(d.updated * 1000); const ts = new Date(d.updated * 1000);
$('last-update').textContent = 'Обновлено ' + ts.toLocaleString('ru') + ' · '; let info = 'Обновлено ' + ts.toLocaleString('ru');
if (d.duration != null) info += ' за ' + d.duration + 's';
$('last-update').textContent = info + ' · ';
} }
remaining = d.remaining; remaining = d.remaining;
pending = d.pending; pending = d.pending;
...@@ -1185,10 +1206,17 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1185,10 +1206,17 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
if os.path.exists(f) if os.path.exists(f)
) )
remaining = max(0, updated + UPDATE_INTERVAL - int(time.time())) remaining = max(0, updated + UPDATE_INTERVAL - int(time.time()))
duration = None
try:
with open(DURATION_FILE, "r") as f:
duration = int(f.read().strip())
except (FileNotFoundError, ValueError):
pass
self.send_json({ self.send_json({
"updated": updated, "updated": updated,
"pending": pending, "pending": pending,
"remaining": remaining, "remaining": remaining,
"duration": duration,
}) })
elif path == "/api/active": elif path == "/api/active":
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment