Жалобы вида «AWG не работает» (#39, #42) почти всегда означают: конфиг
секции применён корректно, но пир не отвечает (сервер лежит, провайдер
режет UDP до эндпоинта). Раньше доктор об этом молчал, и виноватым
выглядел Tachyon.
Новый чек 6c сканирует свежий лог ядра на ошибки outbound/wireguard
(operation timed out / handshake timeout): при свидетельствах мёртвого
туннеля - явный вердикт «проблема вне Tachyon» с подсказкой проверить
сервер и UDP-порт у провайдера. Если wireguard-трафика в логе нет вовсе,
чек не выдаёт ни одной строки - без шума для пользователей без AWG.
- has_outbound_section: туннельные секции (awg/warp/vpn/openvpn/masque/
anytls/snell/mieru/sudoku) теперь считаются исходящей конфигурацией -
одинокая AWG-секция больше не получает вводящее в заблуждение
«No proxy outbound sections found» (жалобы пользователей #39/#42)
- новая миграция orphan_section_interface_cleanup: удаление секции
маршрутизации раньше оставляло после себя дочерние section_interface,
ссылающиеся на несуществующего родителя (у пользователя - запись на
секцию '2' при живой только '1'); сироты удаляются через новую операцию
delete_section; функция объявлена до массива MIGRATIONS - ucode не
поднимает объявления
- strip_unmarked_tachyon_cron_lines: старые версии писали cron-строки без
маркер-комментария, они переживали фильтрацию по маркерам, копились при
обновлениях и crond запускал component_updates_if_due дважды за тик;
немаркированные строки с нашими командами теперь вычищаются при каждом
refresh и remove
На сетях, где api.cloudflareclient.com недоступен (типичный случай из
issue #39), генератор сжигал весь временной бюджет - фронтенд умирал по
XHR timeout через 30 секунд, не показывая причины.
- префлайт: одна проба (4с connect / 6с max) напрямую и через доступный
транспорт; если Cloudflare недостижим - мгновенная понятная ошибка
«Cloudflare API недоступен...» вместо минутного зависания
- бюджеты ужаты до 20с на call_api и 20с на всю регистрацию, чтобы
худший случай укладывался в XHR-таймаут LuCI
- все ошибочные выходы теперь печатают JSON-конверт и exit(0): фронтенд
парсит data.message и показывает настоящую причину (ненулевой код
заставлял его писать безликое Unknown error)
- новая slow-проверка (раз в 5 мин): sing-box запущен, но mixed-порт 4534
не слушается две проверки подряд (~10 мин) -> один авторестарт ядра;
если не помогло - уведомление админу не чаще раза в час, без циклов
бесконечных рестартов
- проверка живости телеграм-воркера: бот включён, pid жив, но heartbeat
старше 15 минут (зависший long-poll/мёртвый цикл) или pid отсутствует ->
рестарт воркера через telegram_start с ограничением раз в час
- обе проверки добавлены в perform_slow_checks через safe_call
Корень «бот молчит»: get_proxy_args считал достаточным условием маршрутизации
через 4534 сам факт запущенного sing-box - если mixed-inbound не поднялся,
каждый запрос вечно падал, воркер уходил в бесконечный backoff (468 ретраев
подряд у пользователя), хотя api.telegram.org был доступен напрямую.
- mixed_port_alive(): TCP-проба слушается ли 4534 (netstat) с кэшем 30с;
маршрут через прокси выдаётся только при живом порте
- tg_request разделён на tg_request_via + fallback-цепочку: прокси не ответил -
повтор без прокси; отключается UCI telegram.bot_direct_fallback='0'
- алерт админу однократно за эпизод после 5 неудачных опросов подряд с
указанием причины; сбрасывается при восстановлении опроса
- heartbeat /var/run/tachyon_telegram.heartbeat после каждого успешного
цикла - основа для вотчдог-проверки живости (следующий коммит)
- ротация tmpfs-лога воркера: потолок 512KB, хвост 100KB, проверка раз в час
- /doctor в боте рендерит человекочитаемую сводку (проблемы/исправлено/
запланировано) вместо сырого JSON
- config_version убран из человекочитаемого вывода конфига (внутренний
маркер миграций)
UTF-8 BOM раньше прятал shebang CGI от скана инвентаря; после удаления BOM
скрипт (корректно) определяется как shell, поэтому ожидаемый список
пополняется tachyon/files/usr/lib/cgi-bin/tachyon-agent.
- workflow shellcheck теперь покрывает агентский CGI, hotplug WAN-монитор и
точки входа usr/bin; в CGI перед shebang стоял UTF-8 BOM, из-за которого
Linux вообще не мог исполнить скрипт (bad interpreter) - удалён вместе со
случайными BOM в исходниках фронтенда
- новый rag_embeddings.sh фиксирует исправленный порядок аргументов push(),
инвалидацию кэша по отпечатку индекса, нормализацию null model_override и
порядок объявлений - ни один из починенных багов RAG не сможет вернуться
незаметно
- hosts_li
run_all.sh гонял набор последовательно с fail-fast: первый упавший тест
отменял всё остальное, а CI (xargs -P4) расходился с локальным порядком.
Теперь поддерживается -P N параллельность как в CI, ведётся лог по каждому
тесту, печатается сводка pass/fail и вывод упавших, принимаются фильтры имён
для запуска подмножества, а ошибки синтакс-проверки по-прежнему прерывают
всё до старта тестов.
Доктор мутировал систему при каждом диагностическом запуске: отключал
rebind protection dnsmasq, делал kill -9 процессу на портах 4534/9090,
стирал syslog во время OOM-аудита и подменял resolv.conf на 1.1.1.1 как
побочный эффект "проверки".
- run_doctor_checks() теперь только диагностирует в dry-run: каждая мутация
идёт через обёртки doc_set/doc_commit/doc_run/doc_unlink, которые
записывают планируемое действие и сообщают "WILL FIX (doctor --fix)";
ремонты выполняются только через "tachyon doctor --fix" или repair=true
- recovery-проверки (сервис остановлен) следуют тому же правилу
- параллельные запуски доктора из LuCI, Telegram и cron сериализуются
mkdir-локом со взятием устаревшего лока через 15 минут тишины
- трекер применённых фиксов пишется атомарно (tmp+mv)
- OOM-проверка больше не стирает logread (это форензика) и атрибутирует
kill именно sing-box
- ai_doctor прогонял весь набор проверок дважды (внутри local_rule_doctor и
напрямую); результаты считаются один раз и переиспользуются
- curl к LLM/Clash/подпискам получили connect + total таймауты, зависший
эндпоинт больше не подвешивает доктора
- service_check: lan_interface() запрашивала несуществующий конфиг
forkop/podkop, из-за чего все проверки сервисов смотрели в br-lan вместо
настроенных LAN-интерфейсов - секции AWG/WireGuard всегда получали вердикт
"не отвечает" при рабочем kernel-конфиге (#41, #32, #39)
- rag: у push() были перепутаны аргументы - коллекция эмбеддингов оставалась
пустой для всех провайдеров OpenAI-формата; кэш эмбеддингов теперь
инвалидируется по отпечатку индекса (mtime+size), null model_override
обрабатывается корректно
- warp_generator: curl к Clash API без таймаута и матрица ретраев без общего
дедлайна могли блокировать выполнение минутами и ловить XHR timeout LuCI
(#39); добавлены -m 5 на локальные вызовы и бюджеты 40с/90с на весь цикл
- build.sh: релизные пакеты теперь содержат тот же состав файлов, что и
сборка через OpenWrt Makefile (hotplug WAN-монитор, агентский CGI в
/usr/lib/cgi-bin, defaults/config, профили servicecheck); конфиги с
секретами ставятся с правами 0600; тест паритета инвентаря
- updates UI: после самообновления tachyon страница перезагружается только
когда перезапущенный бэкенд снова отвечает на get_ui_state, вместо
фиксированной задержки 8 секунд (#23)
Новые опции config schedule:
- blocked_domains: домены для блокировки (full:/keyword:/regex: префиксы)
- mode: block (список) | allow (whitelist — всё кроме списка)
- dns_level: блокировка на DNS-уровне (NXDOMAIN через dns-block-in)
- notify: уведомление в Telegram при попытке доступа
Механизм:
- sing-box: новый dns-block-in inbound (127.0.0.43:1053) с reject-правилами
для заблокированных доменов; source_ip_cidr для IP-устройств;
invert для whitelist-режима
- nftables: nat chain dns_block — REDIRECT DNS-запросов устройств на
dns-block-in; time-based через meta hour для расписаний; MAC-устройства
матчатся по ether saddr (DHCP-динамические IP поддерживаются)
- route rules reject для always-on расписаний (fallback при обходе DNS)
- telegram worker: проверка nft counters каждые 60s, уведомление о
заблокированной активности
UI (parental.js):
- колонка Blocked Sites в таблице
- textarea с пресетами: соцсети, видео, игры, взрослый контент,
мессенджеры, магазины
- mode / dns_level / notify переключатели
Валидация: домены, mode, dns_level, notify в validator.uc
Тесты: tests/content_blocking.sh (98/98 backend, 522/522 frontend)
- 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
The progress modal faked its steps on a timer because the worker only wrote to syslog. Now every updates_log line is appended to <job>.log (UPDATES_JOB_LOG), the running state advertises log_path, the log survives job completion until the TTL sweep, and the new component_action_log CLI mode serves it from a byte offset. The UI polls it into a monospace panel with auto-scroll, exposes a Copy log button, and the modal cannot be closed while the job runs (only the disabled progress button is shown).
With set -eo pipefail, when grep -qx exits early after finding a match
printf gets SIGPIPE and returns non-zero, which with pipefail triggers the
|| fail branch even though grep returned 0 (found).
Here-string avoids the pipe entirely.
- watchdog: удалён дублирующий check_tachyon_cli_running(), везде используется controller.check_tachyon_cli_running()
- watchdog: smart_detect_process_pending использует controller.proxy_port() вместо повторного парсинга config.json
- watchdog: удалена однострочная обёртка is_dns_dead() — заменена is_dns_working() напрямую
- watchdog: heal_proxy_connectivity требует streak >= 2 перед очисткой cache.db — устраняет ложные ночные уведомления при кратких просадках прокси-ноды
- event_controller: push_history переведён на slice() вместо ручного цикла
- event_controller: direct_ok использует тот же check_url что и proxy check — корректность при кастомном ai_proxy_health_url
- event_controller: добавлен subscription_fail_streak — одиночный таймаут сети больше не триггерит Telegram
- telegram: tg_request больше не создаёт временные файлы в /tmp — JSON передаётся напрямую через curl -d, устранена утечка tg_payload_*.json при kill процесса
- telegram: worker() при старте удаляет оставшиеся tg_payload_*.json от предыдущих сессий
- tests: исправлен шаблон mktemp в background_spawn_fds.sh — busybox не поддерживает двойное расширение (.XXXXXX.uc)
Самообновление Tachyon — это компонентное задание, которое устанавливает
пакет, чей postinst и вызывает remove_component_update_cache(). Очистка
стирала каталог component-actions целиком, включая файл состояния того
самого задания, которое ещё выполнялось. LuCI опрашивал задание, не
находил его, не получал ни message, ни stderr и показывал фолбэк
"Failed to execute" поверх успешно прошедшего обновления.
Теперь очистка пропускает задания, помеченные running. Устаревшие
результаты и кэш проверок обновлений убираются как раньше. Зависшее
задание не залипнет: детект по PID и started_at помечает такие stale.
Пути каталогов состояния стали переопределяемыми через окружение, чтобы
тест мог прогнать luci-postinst, не трогая /var/run.
Воркер компонентного действия перенаправлял stderr в тот же файл, что и
stdout (2>&1). Действие печатает результат в stdout одним JSON-объектом,
а stderr — предупреждения apk/opkg, вывод init-скрипта при рестарте —
попадал в тот же файл после JSON. Разбор всего файла падал, построчный
фолбэк не находил объекта, и finish_component_job писал синтетический
success: false с последней строкой лога в качестве сообщения.
Особенно надёжно это ломало самообновление Tachyon: install_tachyon
рестартует сервис, будучи предком воркера, поэтому вывод рестарта
гарантированно ложился поверх канала результата.
Теперь stderr пишется в отдельный <job>.out.stderr, он же используется
как источник сообщения, когда воркер действительно умер, не напечатав
результат. Файл убирается вместе с заданием и подметается orphan-очисткой.
Заодно валидация sing-box перестала судить о бинарнике по коду возврата:
popen().close() отдаёт сырой wait-статус, а command_output отбрасывал
stdout при любом ненулевом статусе. `sing-box version` печатает версию и
может завершиться ненулевым кодом или получить сигнал от остановки
сервиса перед установкой — валидация получала "", объявляла рабочий
пакет битым и откатывала установку с exit_code 129 (128+SIGHUP). Добавлен
command_output_lenient, он же чинит показ версии в дашборде.
The dashboard showed a sing-box version that never changed. build_system_info()
preferred /etc/tachyon/sing-box-version for every "extended" variant, and that
state file is written by the component action alone — a sing-box installed via
install.sh, opkg or by hand leaves it pinned at whatever was recorded last. The
UI then reported the stale version indefinitely, and the update badge compared
the release against it rather than against the binary actually on disk.
The state file only exists for variants whose binary cannot be executed to print
a version: extended-compressed is a self-extracting stub, and lx needs a runtime
that may be absent. Plain extended answers `sing-box version` fine — singbox
/runtime.uc:259 and subscription/cache.uc:1482 both gate on the compressed
variants only. Covering plain extended was a regression from 46ef97fe.
Both call sites now share sing_box_resolved_version(), which also falls through
to the live probe when the state file is missing or empty instead of reporting
"unknown".
Verified on a router: with the state file still holding 1.13.16-extended-2.6.2,
system-info now reports the installed 1.13.18-extended-2.6.3.
build.sh assembles the APK and IPK packages itself rather than going through
OpenWrt's packaging, so it carries its own copies of the prerm-family hooks.
Bounding Package/tachyon/prerm in tachyon/Makefile therefore never reached the
released artifacts: 1.2.66 shipped with
exit(system("/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1"));
which is both unbounded and status-propagating. `exit(system(...))` returns a raw
wait status, so a cleanup that was merely killed took the upgrade down with it —
on the router that surfaced as
ERROR: lib/apk/exec/tachyon-1.2.66.pre-upgrade: exited with error 247
247 being 128+119, the cleanup SIGKILLed by its own sweep.
All three hooks written here (APK pre-upgrade, APK pre-deinstall, backend IPK
prerm) now probe the BusyBox timeout spelling against /bin/true, bound the
cleanup at 90s, and exit 0 unconditionally — matching the Makefile.
tests/package_lifecycle.sh gains contracts over all three: each must be bounded,
must not propagate the cleanup's status, and must compile. They live inside
heredocs, so ucode_syntax_lint.sh never sees them and a syntax error would only
surface on a router mid-upgrade.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ps | grep '/usr/bin/tachyon'` matches the hook itself, because
/usr/bin/tachyon is what invoked it. The sweep therefore killed its own
parent, the parent died on SIGKILL, and apk saw
`tachyon-<version>.pre-upgrade: exited with error 247` (128+119) and
reported a failed hook.
The sweep now walks /proc/<pid>/stat to collect its own ancestor chain and
skips those pids.
Holders of procd's serialization lock are also cleared by descriptor rather
than by name. procd_lock() does `exec 1000>` on
/var/lock/procd_tachyon.lock and then `flock 1000` with no -w, so any
background spawn inheriting fd 1000 keeps the lock held. On a live router
the holders were watchdog workers, logread, the zapret2 supervisors, nfqws2
and a stray curl — none of which match a tachyon name pattern. While any of
them lives, every init.d call blocks in flock until the package manager
gives up on the hook.
Same descriptor-based sweep added to install.sh, which faces the same lock
left behind by pre-fix builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>