# Задание A60: обновление доводит себя до конца ## Дата поступления 2026-09-02 ## База `origin/main` (`2f35377`). A59 влит: ветка `antigravity/a59-visible-update` принята с исправлениями ревьюера (`285ae7c`). Показ и загрузку переписывать второй раз не надо. ``` git fetch origin --prune git checkout -b antigravity/a60-update-completes origin/main ``` В `main` напрямую не пушить. ## Порядок исполнения Два прохода: **Flash** реализует, **Pro** проводит аудит. Пункт **P0-6** написан для аудитора. Зона: применение обновления и перезапуск. Показа и загрузки не касается — там всё принято и проверено. --- ## Что уже сделано — переделывать не надо A59 довёл обновление до экрана. Ревьюер проверил и принял: ``` окно при запуске, отказ по версии, молчание без обновлений работает полоса хода, честное Н/Д без размера работает отмена с удалением недокачанного файла работает отмена принимается только на проверке и загрузке работает SHA-256 обязательна, непроверенный файл не запускается работает запись о применённом обновлении только после успеха работает ``` Ничего из этого не трогать. --- ## Задача Владелец нажимает «Обновить сейчас». Пакет скачивается на глазах, сумма сходится, начинается установка — и на этом всё кончается. Хаб не поднимается, окно гаснет, владелец идёт ставить сборку руками. Ровно то, из-за чего писалось A59. --- ## Разрыв, и он один на обеих системах **Установщик снимает тот процесс, который его запустил и ждёт.** `install_latest_update` делает так: ``` stop_running_hub() свои процессы, кроме текущего subprocess.run(["bash", installer], timeout=600) ЖДЁТ здесь schedule_restart() сюда управление не доходит ``` **Linux — проверено исполнением, цепочка целиком.** ``` 1. хаб: subprocess.run(["bash", installer], capture_output=True) stdout установщика — труба, единственный читатель которой сам хаб 2. install-linux.sh, шаг [0/6]: stop_running_hub снимает хаб pgrep -u $(id -u) -f "antigravity_provider.router.web|hermes_hub_web_entry" никого не исключает, под шаблон попадает тот, кто запустил установщик 3. хаб мёртв -> у трубы не осталось читателя 4. следующий echo установщика -> SIGPIPE -> установщик умирает на шаге [1/6] 5. не установлено ничего; перезапускать нечего ``` **Установщик не доживает до конца — он умирает раньше, чем что-либо поставит.** Это не «поставилось, но не запустилось»: манифест и код остаются на прежней сборке. Проверено контрольным опытом — тот же скрипт, та же смерть родителя: с выводом в файл доходит до конца, с `capture_output=True` умирает. Отсюда следует, что одной перестановкой `schedule_restart` делу не помочь: пока установщик пишет в трубу убитого им процесса, он не доживёт до установки при любом порядке вызовов. **Windows — прочитано по коду, живьём не проверялось.** `installer/HermesHubSetup.cs:280` (`StopOwnedRuntime`) выглядит аккуратнее: строит `$protected` — цепочку собственных предков, чтобы не снять того, кто его запустил. Но проверка `-notin $protected` стоит **только на дочерних** процессах внутри `Stop-HubBranch`. Сам процесс-цель снимается безусловно, а хаб под шаблон `antigravity_provider\.router\.web` подходит. Труба там та же: `proc.wait(timeout=600)` при `Popen` без перенаправления вывода. **Подтвердить исполнением, а не поверить на слово.** Отката на путях установщика нет вообще: он есть только для `.zip` (`apply_update_sync`). Ни `.sh`, ни `.exe` при срыве на середине ничего не возвращают. --- ## P0-1. Отсоединённый помощник Порядок не выдумывать заново — он описан в docstring `schedule_restart`: сначала отсоединённый помощник, потом выход текущего процесса. Лаунчер считает хаб работающим, пока порт отвечает, поэтому поднимать новый, не освободив порт, бесполезно. 1. **Хаб порождает помощника** (`setsid` или отдельная группа процессов на Linux, `DETACHED_PROCESS` на Windows), передаёт ему путь к уже проверенному пакету и **выходит сам**, освободив порт. 2. **Помощник**: дожидается освобождения порта → запускает установщик → поднимает хаб → завершается. 3. **Ни одна труба помощника не должна вести в хаб.** Это то самое место, где всё ломается сейчас: `capture_output=True` делает читателем вывода тот процесс, который установщик собирается снять. Вывод установщика — сразу в файл, а не в `PIPE`, и не через процесс, которому предстоит умереть. 4. **Помощник не наследует** ни stdout хаба, ни его рабочий каталог: хаб исчезнет раньше, чем помощник закончит. 5. **Помощник пишет свой ход в файл** `~/.hermes/updates/apply-<время>.log`, чтобы после неудачи было что показать. Пустой отказ без причины — уже было в A59, второй раз не проходит. ## P0-2. Владелец видит, что происходит 1. **Перед выходом статус `restarting`** с текстом, что хаб сейчас закроется и поднимется сам. Не «установлено» — установка ещё идёт. 2. **Интерфейс переживает разрыв.** Опрос `get_update_progress` получит отказ соединения: это ожидаемое состояние, а не ошибка. Показывать «Hermes Hub перезапускается», продолжать пробовать, при возврате — перечитать страницу. 3. **Не молчать бесконечно.** Не поднялся за отведённое время — сказать это прямо и назвать путь к журналу помощника. ## P0-3. Откат на путях установщика 1. **Помощник снимает копию установленного** до запуска установщика. 2. **Установщик вернул не ноль или хаб не поднялся** за отведённое время — вернуть прежнее и поднять его. 3. **Причина отказа** — с кодом возврата и хвостом вывода — в журнал и на экран при следующем старте. 4. **Программа обязана остаться работоспособной.** Это главное требование пункта: неудачное обновление не имеет права оставить владельца без хаба. ## P0-4. Проверка исполнением Тестами это не ловится: дело в живом переходе между версиями и в том, кто кого снимает. 1. **Поставить `v0.1.2-b7`, обновиться на `v0.1.3-b1` через интерфейс.** Оба релиза опубликованы, установщики и `checksums.txt` на месте. Скриншоты: окно, полоса, экран после возврата. 2. **Замерить время** от нажатия до готовности. 3. **Проверить, что работает новый код** — по `running_commit`, снятому при старте процесса, а не по номеру версии. 4. **Повторить на Windows.** 5. **Сорвать установку намеренно** (испорченный установщик) — проверить откат и что хаб жив. 6. **Проверить, что старый процесс не остался** и порт занят новым. ## P0-5. Обновление вообще предлагается Найдено тем же прогоном, до того как дело дошло до установки. Хаб на `v0.1.2-b7` при живом релизе `v0.1.3-b1` ответил: `«Установлена сборка новее опубликованного релиза (4fa9939 от 2026-09-02)»`, `update_available: false`. Обновиться было нельзя вообще. Причина: `deployed_at` в `deployment_manifest.json` пишется установщиком в момент **установки**, а не сборки, и сравнивается с `published_at` релиза. Поставил старую сборку сегодня — она «новее» любого релиза, и обновление не предложат больше никогда. Владелец, переставивший сборку руками, выпадает из обновлений молча. 1. **Сравнивать сборки, а не дату установки.** Дата установки не говорит о том, какой код внутри. 2. **Если сравнить нечем — предложить обновление, а не промолчать.** Молчание здесь дороже лишнего окна: владелец не узнает, что отстал. 3. **Причину решения показывать.** «Установлена сборка новее релиза» — вывод, а не факт; рядом должно стоять, из чего он сделан. ## P0-6. Аудит вторым проходом 1. **Пройти обновление целиком на обеих системах.** 2. **Проверить, что помощник не снимает чужого** — только процессы хаба своего пользователя. 3. **Проверить откат** при сорвавшейся установке и что после него хаб отвечает. 4. **Проверить, что интерфейс не объявляет успех раньше времени** — ни на выходе хаба, ни на разрыве связи. 5. **Побочные изменения** объяснить. 6. **Пропущенный пункт назвать пропущенным.** В A59 живая проверка была пропущена молча, при том что релиз для неё был опубликован за девять часов до сдачи. --- ## Ограничения - Показ и загрузку из A59 не переделывать. - Проверку SHA-256 и список разрешённых адресов не ослаблять. - Показ работающего коммита и времени запуска не ломать. - Фронтенд без npm, без сборки, без фреймворков — по `docs/web-api/CONTRACT.md` §1. - **Правки ревьюера из `main` не откатывать — включая комментарии.** В A59 сняли шесть блоков с объяснением прошлых регрессий, ревьюер возвращал их руками. - Версию `0.1.3` не понижать. - Правило честности без исключений: неизвестное — `Н/Д` с причиной, а не правдоподобное число и не полоса во всю ширину. ## Критерии приёмки 1. Ветка в `origin`, `git status` чист. 2. Обновление, запущенное из интерфейса, доходит до работающего нового хаба **без участия владельца** — на Linux и на Windows, подтверждено скриншотами. 3. После перезапуска `running_commit` соответствует новой сборке. 4. Сорвавшаяся установка откатывается, хаб остаётся работоспособным. 5. Старый процесс не остался, порт занят новым. 6. Журнал помощника пишется, и при отказе на него указывают. 7. Обновление предлагается по сравнению сборок, а не по дате установки; переустановка старой сборки не выключает обновления навсегда. 8. `ruff check .` чисто; релизный гейт пройден; тестов не меньше **740**. 9. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, `X passed / Y skipped / Z failed`, тайминги, скриншоты. ## Главное A59 сделал обновление видимым: владелец видит окно и видит загрузку. Дальше механизм обрывается на самом простом — установщик снимает того, кто его запустил и ждёт результата. Задание про один шаг: чтобы после нажатия «Обновить сейчас» владелец больше ничего не делал. ## Порядок сдачи Передать точный `FINAL_COMMIT_SHA`.