- priority.uc: replace 1s tight sleep loop with dynamic interval based on group config (5s default)
- dns_failover.uc: replace 1s sleep with min(active_interval, recovery_interval)
- watchdog.uc: cap pending_smart_domains at 500 entries, add 300s TTL expiry
- event_controller.uc: batch is_dns_working nslookup calls into single shell process
- byedpi/runtime.uc: add crash-loop backoff (up to 6x base delay after 5 rapid crashes)
- nfqueue/runtime.uc: same crash-loop backoff for nfqws supervisors
- Fix response parsing: check report field, not just message
- Pass user query text to LLM instead of ignoring it
- Append user question to diagnostic prompt with system context
- Increase timeout to 120s for LM Studio responses
- Add command_status_from_args to core/common.uc exports
Closes#38
Пустых catch в кодовой базе около полусотни, и они не одинаковые. Большинство
выражает намерение: fs.unlink бросает на ENOENT, а вызывающий говорит «пусть
этого файла не будет», и отсутствующий файл это условие уже выполняет; разбор
json в цикле пропускает мусорную строку и берёт следующую. Такие места теперь
несут пояснение, почему пустое тело здесь и есть правильный ответ.
Семь мест к этому классу не относились: там сбой означал потерю состояния, а
дальше код шёл так, будто всё получилось.
Watchdog писал через пустой catch файл лока перезапуска прокси, шкалу
GOMEMLIMIT после OOM, бэкап конфига и метку keepalive. Не лёгший лок означает,
что следующий тик перезапустит прокси заново, а счётчик rate-limit об этом не
узнает; не записанная метка keepalive заставляет init.d считать watchdog мёртвым
и перезапускать его. Все четыре теперь идут через write_state_file, который
логирует отказ. Метка пишется на каждом тике uloop, поэтому безусловная строка
залила бы syslog именно при переполненной файловой системе — там, где лог нужнее
всего; вместо этого каждый путь сообщает о первом отказе и молчит до следующего
успеха, что заодно делает видимым восстановление.
Контроллер разбирал /etc/sing-box/config.json ради портов http и tproxy и при
неудаче молча брал 4534 и 4530. Watchdog после этого опрашивает не тот порт,
вечно считает прокси нерабочим и лечит его по кругу — при том, что в логе нет
ничего, что связало бы это с конфигом. Разбор вынесен в общую функцию, которая
сообщает о непарсящемся конфиге один раз на процесс.
Telegram молча терял карту callback-токенов: токен уже вшит в клавиатуру к
моменту записи, поэтому провал отдаёт пользователю кнопку, которая на нажатие
ответит «кнопка устарела». Обновление списков молча теряло маркер отложенного
применения nft — списки обновлены на диске и до nftables не доехали никогда,
а строкой выше в лог написано, что маркер сохраняется. urltest молча выкидывал
битое регулярное выражение из конфига пользователя, отчего группа тихо
подхватывала меньше outbound-ов, чем сказано в конфиге.
tests/heal_escalation.sh проверял порядок строк по литералу fs.writefile для
лока и обновлён под новый вызов.
ucode держит по одному открытому дескриптору на каждый require()-модуль всё
время жизни интерпретатора и не даёт ни закрыть их, ни пометить close-on-exec,
поэтому каждый фоновый процесс наследует весь набор. На роутере watchdog несёт
около 90 таких дескрипторов и передаёт их детям: logread, sh и nfqws2 найдены
держащими дескрипторы на собственных .uc-файлах Tachyon, часть уже удалена
обновлением пакета — старый инод прибит к диску процессом, которому он не нужен.
Против этого код писал литерал 1000<&- в 22 местах 13 модулей. Он не закрывал
ничего: дескриптор 1000 никогда не открывается, а все настоящие лежат в 6..89.
Хуже того, этого литерала требовал tests/service_start_trap.sh — именно поэтому
он и разошёлся по кодовой базе. Проверка пинила видимость защиты и продолжала бы
проходить вечно при живой утечке.
Замена оказалась не такой прямой, как выглядит. Счёт от 3 до потолка не годится:
в dash закрытие дескриптора 10 и выше фатально даже для номера, который никогда
не открывался — dash перемещает туда собственную служебную бухгалтерию, и
перенаправление убивает шелл целиком, а 2>/dev/null прячет сообщение, но не
смерть. Перечисление /proc/self/fd трогает только реально открытое, но сами
высокие дескрипторы никуда не деваются: набор модулей любого размера уводит их
за 10. Поэтому пролог сначала спрашивает шелл в подоболочке, которая поглощает
смерть, и опускает собственный потолок, если ответ отрицательный. busybox ash,
на котором работает OpenWrt, отвечает утвердительно и закрывает весь набор.
Три хелпера в core/common.uc задают форму фонового запуска — раньше каждое место
собирало перенаправления вручную, и они разошлись. Форма с пидом ставит exec
внутрь подоболочки, чтобы $! называл сам демон, а не обёртку, которая тут же
завершается; ведущие присваивания VAR=x при этом выносятся перед exec, иначе
шелл ищет программу с именем VAR=x. Для конвейеров, которые exec заменить не
может, есть отдельный хелпер. initd.uc и state.uc лежат на пути раннего старта и
несут собственные копии, как уже несут shell_quote и command_from_args.
Попутно вскрылись два настоящих дефекта. В лестнице эскалации watchdog строка
читалась как restart & rm -f lock &, то есть лок снимался параллельно с
рестартом, который он охраняет, и вторая эскалация могла начаться посреди
первой. В пакетном прогреве Zero-RTT знак & стоял внутри строки, поэтому
перенаправления после него цеплялись к пустой команде в родительском шелле,
а не к фоновому циклу.
Ещё четыре места запускали фон вообще мимо механизма; одно из них аудит
пропустил, его нашёл новый тест.
tests/background_spawn_fds.sh считает дескрипторы у ребёнка при обоих способах
запуска: структурная проверка такое показать не может, строку надо выполнить.
На роутере старый способ даёт ребёнку 7 дескрипторов, новый — 3.
Критические исправления:
- safe_proxy_restart: удаление lock-файла после фонового перезапуска
с защитой от stale lock (>300 сек)
- write_json_file: атомарная запись через tmp+mv вместо прямой
- write_reload_state: атомарная запись через tmp+mv
- state_add_list: отклонение значений с пробелами
Исправления безопасности:
- killall logread заменён на pkill -f 'logread -f'
- SIGTERM→SIGKILL для Telegram и sing-box при остановке
- Recover OOM scale: инкремент +0.05 каждые 10 минут
- nfqueue и byedpi: trap для корректного завершения дочерних процессов
- nfqueue: валидация стратегий и откат при ошибке запуска
- nfqueue: flush mangle_output перед добавлением правил
- Исправлен парсинг exit code в 14 файлах (status & 127)
Улучшения стабильности:
- init.d: trap для start_service/stop_service
- watchdog: signal traps для legacy fallback loop
- lifecycle: логирование ошибок при остановке компонентов
- singbox/priority: SIGTERM→SIGKILL при остановке
- telegram: exponential backoff (1x→300s) при ошибках API
- telegram: process_updates возвращает boolean для отслеживания успеха
- watchdog: кэш proxy port для уменьшения чтения конфига sing-box
- singbox/generator: упрощён atomic_write_json_file
- packages.uc: переупорядочивание apk_version (лёгкие запросы первыми)
- dns/apply: проверка PID sing-box перед настройкой dnsmasq
- server/service: cleanup_orphaned_server_certs перед вызовом
Resolves merge conflicts:
- install.sh, build.sh, .github/workflows/build.yml: Reverted to Dushnilin's versions to avoid installing SourceForge mirrors.
- fe-app-tachyon: Resolved type, formatting and copyOutbound conflicts, keeping both Dushnilin's custom features and ushan0v's frontend performance optimizations.
- backend: Kept Dushnilin's ucode refactorings, Telegram bot integration, and release repository URL (Dushnilin/tachyon) while merging updates.uc cache format v8 & persistent cache format v7.
- tests: Merged tests from both branches, preserving ucode -L wrappers for local test execution.