Запись 51. Загадка 404 в GitHub Releases: мостик от v0.8.6 к v0.8.7
⏱️ Время: 26 сентября 2026, 18:20 — 18:35 (~15 минут)1. Повод и детективная история
История этого акта началась накануне. Едва успев развернуть релиз v0.8.6 с Universal Command Guard, мы решили протестировать функцию «Проверить обновления» прямо в карточке настроек Shellit. Нажимаем кнопку — и получаем невозмутимое сообщение: «No remote releases found. You are running the latest preview build».
Как так? На GitHub релизы лежат, тег v0.8.6 опубликован, а приложение в упор их не замечает.
Второе замечание коснулось веб-летописи: на страницах портала красовалась надпись «8 тематических глав». Но с каждым новым актом менять эту цифру вручную по всем шаблонам и RSS-лентам — верный путь к регулярному рассинхрону.
2. Подводные камни GitHub REST API
Мы полезли в сетевой вызов AboutSettingsCard.releasesApiUrl. Там красовался каноничный URL:
https://api.github.com/repos/kobaltgit/Shellit/releases/latest
Казалось бы, что может быть логичнее для поиска последнего релиза? Но чтение спецификации GitHub REST API преподнесло неожиданный сюрприз:
Эндпоинт
/releases/latestвозвращает исключительно релизы со статусом Stable. Если все релизы в репозитории имеют флаг Pre-release (а для открытой альфы Shellit все версииv0.*маркируются именно как prerelease), этот эндпоинт всегда возвращает HTTP 404 Not Found!
Получая от сервера код 404, наше приложение честно делало вывод: на удаленном сервере релизов не существует вовсе (BUG-043).
3. Исправление и стабилизация
-
Честный список релизов (
BUG-043):
Мы заменили эндпоинт наhttps://api.github.com/repos/kobaltgit/Shellit/releases?per_page=5. Теперь приложение забирает список последних опубликованных релизов, отбрасывает черновики (r['draft'] != true) и берет самый актуальный релиз независимо от того, pre-release он или stable. -
Динамические формулировки на сайте:
В шаблонах Astro (ru/chronicle/index.astro,chronicle/index.astro), RSS-генераторах иllms.txtжесткая цифра «8» заменена на качественное описание («Тематические главы…»). Теперь при добавлении новых глав структура сайта остается вечно актуальной. -
Релиз v0.8.7:
Все изменения зафиксированы, тесты обновлены (7/7 вabout_settings_card_test.dart), а версия приложения повышена до v0.8.7.
Запись 52. 16 000 открытых сокетов: как фоновый пинг исчерпал сетевой пул Windows и чему нас научил socket.destroy()
⏱️ Время: 27 сентября 2026, 20:10 — 20:45 (~35 минут)
1. Повод и тревожный звонок из системного монитора
Всё началось с внезапного сетевого блэкаута. На рабочей машине перестали открываться сайты, браузеры выдавали сетевые сбои, веб-сервисы падали по таймауту, а системный монитор рапортовал об аномалии:
Фоновый процесс shellit.exe (PID 5932) завис в цикле утечки соединений и открыл ровно 16 071 активный TCP-сокет, намертво исчерпав сетевой пул всей операционной системы. Стоило принудительно остановить процесс — и число занятых сетевых сокетов мгновенно вернулось в норму (с 16 602 до 533).
Число 16 071 сразу бросилось в глаза любому системному инженеру:
- По умолчанию в Windows динамический диапазон клиентских портов (эфемеров) — от 49152 до 65535.
- Это дает ровно 16 384 порта.
- За вычетом системных служб Shellit съел ровно 100% оставшихся портов Windows, вызвав отказ
WSAENOBUFS(No buffer space available) для всех приложений операционной системы.
2. Подводные камни: коварство полузакрытых сокетов в Dart (dart:io)
Диагностика сетевого стека вскрыла тончайший нюанс взаимодействия среды Dart с сетевым протоколом OpenSSH:
-
Разница между
close()иdestroy():
В сервисеSshClientService.pingHostизмерялась RTT-задержка до хостов. При проверке создавался сокетSocket.connect(hostname, port), после чего вызывалсяawait socket.close().
Однако в Dart методsocket.close()закрывает сокет только на передачу (shutdown(SHUT_WR)), отправляя удаленному серверу TCP FIN. Поток чтения при этом остается открытым в ожидании, пока удаленная сторона закроет свой конец. -
Приветствие OpenSSH:
При открытии порта 22 любой стандартный SSH-сервер первым делом отправляет строку идентификации версии протокола (SSH-2.0-OpenSSH...). Этот баннер прилетал в сетевой стек Windows. Поскольку Shellit данные не вычитывал и не вызывалsocket.destroy(), сокет переходил в полузакрытое состояние с непрочитанными байтами в буфере. Среда Dart VM и ядро Windows удерживали дескриптор сокета и его динамический порт. -
Фоновый таймер телеметрии:
Монитор пинга каждые 15 секунд честно опрашивал серверы из каталога. Каждый опрос оставлял в ядре «подвешенную трубку». За несколько часов работы в фоне эти сокеты монотонно копились, пока не вычерпали весь системный лимит портов до последней капли. -
Реактивный шум в базе данных:
Каждый замер пинга заодно записывал задержку в SQLite-таблицуhostsTableчерез Drift. Это вызывало постоянный триггер реактивного стримаwatchAllHosts(), провоцируя бесконечные перерисовки виджетов интерфейса.
3. Исправление и архитектурная стабилизация (BUG-044)
Для ликвидации дефекта мы применили трехуровневую защиту:
1. Гарантированный socket.destroy()
В методе SshClientService.pingHost вызов полузакрытия заменен на блок finally:
Socket? socket;try { socket = await Socket.connect(hostname, port, timeout: effectiveTimeout); stopwatch.stop(); return PingResult.success(latencyMs: stopwatch.elapsedMilliseconds);} finally { socket?.destroy();}Метод destroy() моментально обрывает соединение в обе стороны (RST), очищает буферы ядра и немедленно возвращает системный порт Windows в пул доступных портов.
2. Защита терминала и SFTP от утечек при ошибках аутентификации
В методы createTerminalSession и openSftpSession добавлен аналогичный блок finally: если аутентификация сорвалась, ключ отклонен или произошел таймаут рукопожатия, client?.close() и socket?.destroy() гарантированно вызываются до выхода из метода.
3. Разгрузка SQLite и UI-потока
В HostsNotifier.updateHostLatency добавлен флаг persist: false. Текущая RTT-задержка теперь обновляется в оперативной памяти виджетов мгновенно без записи на диск и без паразитных событий в реактивном стриме базы данных Drift.
4. Итог: Релиз v0.8.8
- Устранен инцидент
BUG-044(P0 Blocker исчерпания сетевого пула Windows). - Написан регрессионный юнит-тест с эмуляцией передачи OpenSSH-баннера (
ssh_client_service_test.dart). - Все 430+ тестов монорепозитория завершились со 100% успехом:
core_foundation: 27/27 успешно.ssh_network_core: 81/81 успешно.terminal_ui: 126/126 успешно.desktop_plugin_sdk: 62/62 успешно.apps/shellit: 61/61 успешно.
- Статический анализ
flutter analyze— абсолютный ноль замечаний. - Версия программы повышена до v0.8.8.
Фоновый мониторинг серверов в Shellit теперь работает абсолютно бесшумно, бережно относясь к ресурсам операционной системы и защищая сетевой стек от любых утечек.