Поставил v0.1.2-b7 в изолированную песочницу (свои HOME и HERMES_HOME, живая установка не тронута) и запустил обновление на v0.1.3-b1. Хаб умер через шесть секунд и за 150 секунд не поднялся. Установка при этом не произошла вовсе: манифест и код остались на 0.1.2. В задании было написано, что установщик доживает сиротой и файлы обновляет. Прогон это опроверг. Настоящая цепочка: хаб зовёт установщик через capture_output=True, то есть читателем его вывода становится сам хаб; установщик на шаге [0/6] снимает хаб; у трубы не остаётся читателя; следующий echo даёт SIGPIPE, и установщик умирает на шаге [1/6], не поставив ничего. Проверено контрольным опытом: тот же скрипт с выводом в файл доходит до конца, с трубой умирает. Отсюда следует, что перестановкой schedule_restart делу не помочь. Тем же прогоном найдено второе: deployed_at пишется в момент установки, а не сборки, и сравнивается с датой публикации релиза. Хаб на b7 при живом b1 ответил «установлена сборка новее опубликованного релиза» и обновление не предложил. Переустановил старую сборку — выпал из обновлений молча. Вынесено в P0-5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
232 lines
16 KiB
Markdown
232 lines
16 KiB
Markdown
# Задание 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`.
|