Files
glchat/desktop/README.md
T
grendervill e7c9c93443 docs(desktop): приёмка автообновления, .dmg в песочнице и открытые пункты
- `--update-now` в таблице аргументов и в разделе «Автообновление»: что делает,
  как запускать, что подпись проверяется так же, пример записей в журнале;
- «Локальная проверка «поверх старой версии»»: пошаговый рецепт прогона
  0.1.0 → 0.1.1 на локальном сервере инстанса и три грабли — http-эндпоинт
  требует временного `dangerousInsecureTransportProtocol`, путь приложения не
  должен содержать символических ссылок (`/tmp` → `/private/tmp`), в headless
  нужен `--update-now`;
- `.dmg`: точные команды — обычным терминалом (`make desktop-build`, с
  оформлением окна) и без графической сессии (`bundle_dmg.sh --sandbox-safe`,
  проверено: образ монтируется, `hdiutil verify` — VALID);
- «Что не сделано»: автообновление end-to-end проверено локально, открытым
  остаётся публикация реального релиза; добавлено наблюдение, что `open --args`
  из песочницы агента аргументы не доставляет (проверено на macOS 27).
2026-09-26 18:12:42 +03:00

393 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# desktop — desktop-обёртка glchat (Tauri 2)
Тот же веб-клиент из `web/`, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного
UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт
платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk,
автозапуск, deep links и автообновление.
```
desktop/
└── src-tauri/ # Rust-проект обёртки
├── src/ # окно, трей, шорткаты, уведомления, deep links, обновления
├── capabilities/ # allowlist IPC: локальная страница настроек и домен инстанса
├── ui/ # локальная страница настроек обёртки (tauri://localhost)
├── icons/ # иконки приложения (сгенерированы из web/public/icons)
└── tauri.conf.json # конфигурация сборки и плагинов
```
## Сборка и запуск
```bash
make desktop-check # cargo fmt --check, clippy, тесты (кэш в .cache/)
make desktop-build # бандлы текущей ОС (macOS: .app и .dmg)
make desktop-build-app # только .app + артефакты обновления, без .dmg
open desktop/src-tauri/target/release/bundle/macos/glchat.app
```
Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через `npx`). Кэши `cargo` и
`npm` держатся в `.cache/` — как у остальных инструментов проекта.
`make desktop-build-app` (`tauri build --bundles app`) нужен там, где `.dmg`
собрать нельзя: `bundle_dmg.sh` монтирует образ через `hdiutil` и управляет
Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он
падает (см. «Что не сделано»).
`open --args` из песочницы агента аргументы не доставляет (проверено на
macOS 27: и на бандле glchat, и на системном TextEdit — процесс запускается без
них; из обычного терминала то же самое работает). Поэтому `make desktop-run
ARGS='--minimized'` и замер свёрнутого окна в `scripts/desktop-perf.py` надо
запускать из обычного терминала, а не из песочницы.
Сборка релиза с артефактами автообновления использует ключ подписи:
```bash
# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
make desktop-build
```
Публичный ключ уже вписан в `src-tauri/tauri.conf.json` и в `UPDATER_PUBKEY`
(`src/updates.rs`). Без локального ключа `make desktop-build` собирает бандл без
артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.
## Аргументы командной строки
| Аргумент | Что делает |
|---|---|
| `--instance <url>` | адрес инстанса вместо сохранённого (`--instance=https://…` тоже работает) |
| `--minimized` | запуск свёрнутым в трей (используется автозапуском) |
| `--settings` | сразу открыть страницу настроек обёртки |
| `--autostart on\|off` | включить или выключить автозапуск при установке скриптом |
| `--update-now` | сразу проверить обновление и поставить его без диалога (тихое обновление) |
## Настройки
Хранятся в `settings.json` каталога конфигурации приложения
(`~/Library/Application Support/su.mhspx.glchat/` на macOS):
| Ключ | Смысл |
|---|---|
| `instanceUrl` | адрес клиента, по умолчанию `https://gl.mhspx.su` |
| `closeToTray` | закрытие окна сворачивает приложение в трей |
| `autostart` | запуск при входе в систему (`--minimized`) |
| `notifications` | показывать системные уведомления |
| `pttShortcut`, `muteShortcut`, `deafenShortcut` | глобальные комбинации (пусто — выключено) |
Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»).
Сессия и токены живут только в cookie webview: обёртка их не хранит.
## Deep links
Схема `glchat://` (регистрируется бандлом):
- `glchat://invite/<code>` → страница приглашения;
- `glchat://guild/<guildId>/channel/<channelId>` → сервер и комната.
Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и
доставляется после `desktop_ready`.
## Автообновление
Обёртка проверяет обновления сама: фоновая проверка через 30 секунд после старта
и вручную — трей → «Проверить обновления…». Адрес манифеста —
`GET {instance}/updates/{target}/{arch}/{current_version}` (target:
`darwin`/`windows`/`linux`, arch: `aarch64`/`x86_64`; см. `src/updates.rs`).
Найденное обновление показывается в интерфейсе, установка спрашивается
отдельным диалогом («во время звонка перезапуск без согласия недопустим»).
Для управляемых машин есть тихий режим — флаг `--update-now`:
```bash
open -a /Applications/glchat.app --args --update-now # из обычного терминала
```
С этим флагом обёртка проверяет обновление сразу при старте (не через 30 секунд)
и ставит найденное без вопросов, после чего перезапускается; диалогов нет вовсе,
а если обновления нет — приложение просто работает дальше. По умолчанию флаг
выключен, поведение обычного запуска не меняется. Подпись артефакта проверяется
в обоих режимах: флаг убирает только вопрос пользователю, но не проверку
(docs/DECISIONS.md, D-077).
В журнале (`~/Library/Logs/su.mhspx.glchat/glchat.log`) видны и версия запущенной
сборки, и ход обновления:
```
обёртка запущена: версия=0.1.0, адрес=https://gl.mhspx.su, окно=видимо, автозапуск=true
тихое обновление по флагу --update-now: проверка сразу при старте
доступно обновление 0.1.1
тихое обновление по флагу: 0.1.0 → 0.1.1
обновление 0.1.1 установлено, перезапуск
обёртка запущена: версия=0.1.1, …
```
Манифест отдаёт сам инстанс (`internal/server/updates.go`), отдельная статика в
Caddy не нужна: и встроенный Caddy профиля, и внешний прокси стенда проксируют
весь домен на приложение, поэтому `/updates/...` доходит до него без правок
чужих конфигов.
Раскладка каталога (`UPDATES_DIR`; на стенде `/opt/glchat/updates`, в контейнере
смонтирован как `/app/updates:ro`):
```
/opt/glchat/updates/
└── darwin/aarch64/
├── glchat.app.tar.gz # артефакт релиза (создаёт tauri build)
├── glchat.app.tar.gz.sig # подпись ed25519 (формат minisign)
├── 0.1.0.json # манифест версии
└── latest.json # манифест последнего релиза — его и отдаёт инстанс
```
Публикация релиза:
```bash
make desktop-build # собрать бандл (.app/.dmg) и артефакты обновления
sudo make desktop-release # разложить в /opt/glchat/updates + создать манифест
make desktop-release-upload HOST=user@server # то же, но по SSH (sudo спросит пароль в терминале)
make desktop-release ARGS='--dry-run' # посмотреть план, ничего не записывая
```
Скрипт `scripts/desktop-release.sh` берёт версию из `tauri.conf.json`, сам
определяет target/arch по имени артефакта, копирует артефакт и `.sig`, собирает
манифест (`version`, `notes`, `pub_date`, `platforms.<target>-<arch>.signature/url`)
и кладёт его и как `<version>.json`, и как `latest.json`. Несколько платформ
одной версии (macOS + Windows + Linux) складываются в один манифест: запускайте
скрипт по разу на артефакт.
`latest.json` нужен потому, что обёртка приходит со **своей установленной**
версией: «новее ли релиз» решает клиент сравнением semver
(`tauri-plugin-updater`), поэтому на запрос любой версии сервер отдаёт
последний манифест, а если есть манифест конкретной версии — его. Без этого
обновление не получил бы никто, кроме тех, у кого уже стоит последняя версия.
Владельцу прокси добавлять ничего не нужно: `/updates/...` уходит в
catch-all `handle` и доходит до приложения (так же, как статика клиента).
Вариант «раздавать обновления статикой мимо приложения» требует правила
`rewrite` на `latest.json` с подстановкой `target`/`arch` в Caddy — лишняя
работа без выгоды, поэтому не рекомендуется.
Проверка (стенд):
```bash
curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq .
# в логе обёртки (~/Library/Logs/su.mhspx.glchat/glchat.log) —
# «обновлений нет» (манифест разобран) вместо
# «проверка обновлений не удалась: … 404»
```
Порядок обновления: проверка → уведомление в интерфейсе → скачивание →
подтверждение в диалоге (во время звонка перезапуск без согласия не делается) →
подпись проверяется до установки, downgrade отклоняется.
### Локальная проверка «поверх старой версии» (без стенда)
Установка обновления проверена локально на настоящем сервере инстанса: сборка
0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка → перезапуск
на 0.1.1. Порядок действий (все данные — в каталоге репозитория, стенд не
затрагивается):
```bash
# 1. артефакт текущей версии и приложение 0.1.0 (с ключом подписи)
make desktop-build-app
# 2. версия 0.1.1 в desktop/src-tauri/tauri.conf.json и Cargo.toml, затем:
make desktop-build-app
# 3. раскладка манифеста в свой каталог, ссылки — на локальный сервер
bash scripts/desktop-release.sh --artifact \
desktop/src-tauri/target/release/bundle/macos/glchat.app.tar.gz \
--version 0.1.1 --base-url http://127.0.0.1:8100 --dir /tmp/updates
# 4. настоящий сервер инстанса со своим каталогом обновлений и клиентом
LISTEN_ADDR=127.0.0.1:8099 DATA_DIR=/tmp/glchat-e2e/data WEB_ROOT=$PWD/web/dist \
UPDATES_DIR=/tmp/updates DOMAIN=127.0.0.1:8099 TLS_ENABLED=false \
APP_VERSION=0.1.0-local SESSION_PEPPER=… MASTER_KEY=… TOTP_ENCRYPTION_KEY=… \
./build/glchat
# 5. запуск 0.1.0 с тихим обновлением (адрес инстанса — локальный)
/Applications/glchat.app/Contents/MacOS/glchat-desktop \
--instance http://127.0.0.1:8099 --update-now
```
Что нужно знать, повторяя это:
- **локальный стенд — http, а `tauri-plugin-updater` по умолчанию требует
`https`** («The configured updater endpoint must use a secure protocol»), для
проверки сборку делают с временным флагом
`tauri build --config '{"plugins":{"updater":{"dangerousInsecureTransportProtocol":true}}}'`.
В релизной сборке этого флага быть не должно: боевой адрес — `https://`;
- **приложение должно лежать на пути без символических ссылок**: macOS-проверка
`tauri-utils` отвергает `current_exe()` с symlink-компонентом, поэтому
`/tmp/glchat-e2e/…` не годится (`/tmp` — ссылка на `/private/tmp`), а
`/private/tmp/glchat-e2e/…` годится. Тот же смысл у требования ставить
приложение в `/Applications`, а не запускать через ссылку;
- **`--update-now` обязателен**: без него установку спрашивают диалогом, а
кликнуть в headless-прогоне некому;
- диагностика в логе: `GET /updates/darwin/aarch64/0.1.0` в журнале сервера,
затем `доступно обновление 0.1.1` → `тихое обновление по флагу: 0.1.0 → 0.1.1`
→ `обновление 0.1.1 установлено, перезапуск` → строка запуска с
`версия=0.1.1` и запрос манифеста уже для `0.1.1`;
- проверка подписи проверяется и «от обратного»: если испортить байт в
артефакте, установка обязана упасть с `The signature verification failed`,
а версия на диске — остаться прежней.
## Уведомления и переход в канал
Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные
сообщения, окно неактивно или комната не открыта), обёртка показывает системное
уведомление и обновляет счётчик.
Клик по уведомлению: у `tauri-plugin-notification` на desktop нет колбэка
действия, поэтому реализовано лучшее доступное поведение — уведомление,
показанное при неактивном окне, запоминается, и когда приложение
активируется в течение 10 секунд (окно получило фокус или macOS прислала
`Reopen`), окно поднимается и открывается комната из уведомления
(`glchat://guild/<id>/channel/<id>`, для личной беседы `glchat://dm/<id>`).
Ограничение (честно): ОС не сообщает, что активация вызвана именно кликом по
уведомлению, поэтому переход сработает и при обычном возврате в приложение в
течение этих 10 секунд. Клик по уведомлению старше 10 секунд только поднимает
окно. Полноценный колбэк требует своего моста к `UNUserNotificationCenter`
(`UNUserNotificationCenterDelegate`) — отдельная задача.
## Ресурсы и холодный старт
Замеры собраны скриптом `scripts/desktop-perf.py` (macOS arm64, Mac на M-серии,
инстанс `gl.mhspx.su` через внешний прокси; подробности — `docs/PERF.md`):
| Метрика | Значение | Бюджет (docs/client-tauri.md §1) |
|---|---|---|
| Холодный старт (`open` → процесс → строка «загружена страница») | 773–898 мс (три прогона) | ≤ 2 с |
| Из них: запуск процесса | 74–78 мс | — |
| Из них: загрузка страницы клиента | 696–821 мс | — |
| RAM, клиент открыт и простаивает (RSS: обёртка + 4 процесса WebKit) | 254 МБ (117 + 137) | ≤ 350 МБ |
| RAM, окно свёрнуто в трей (`--minimized`) | 256 МБ | ≤ 350 МБ |
| Физический след (Activity Monitor) | 106 МБ | — |
Наблюдения:
- свёрнутое в трей окно памяти не освобождает: webview с загруженным клиентом
продолжает жить (так работают WKWebView и трей-режим) — экономия была бы
только при выгрузке страницы, а это потеря мгновенного возврата в клиент;
- первый запуск сразу после сборки бандла (холодный кэш файловой системы и
страницы) дал 1,26 с — тоже в бюджете;
- почти вся память — WebKit (страница клиента), сама обёртка занимает ~117 МБ.
Повторить замер:
```bash
make desktop-build-app # свежий .app (+ артефакты обновления)
python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта + простой в трее
```
## Что не сделано (открытые пункты Фазы 6)
- **Автообновление проверено локально «поверх старой версии»** (2026-09-26):
0.1.0 → манифест 0.1.1 → скачивание → проверка подписи → установка →
перезапуск на 0.1.1, подробности и команды — «Локальная проверка» выше.
Открытым остаётся публикация реального релиза на стенде (номер версии
поднимает оператор) и то, что локальная проверка шла по http с временным
флагом `dangerousInsecureTransportProtocol` — на стенде канал https.
- **Аватары в уведомлениях** (thumb/буква) и группировка — сейчас иконка приложения.
- **Захват экрана и микрофон**: разрешения macOS выдаёт webview, отдельных
экранов-инструкций у обёртки нет.
- **Developer ID и нотаризация** macOS, EV/Trusted Signing для Windows — только
в CI с платными сертификатами (ниже — точные команды).
- **`.dmg`**: собирается из обычного терминала (Finder/AppleScript для раскладки
окна); в песочнице агента `hdiutil create` и AppleScript запрещены, но образ
**без оформления окна** собирается флагом `--sandbox-safe` к `bundle_dmg.sh`
(проверено 2026-09-26, см. ниже).
- **Windows/Linux-бандлы**: конфигурация кросс-платформенная, но собирались и
проверялись только macOS-arm64.
## Что осталось оператору (точные команды)
### macOS: `.dmg`
`bundle_dmg.sh` создаёт образ через `hdiutil create` и раскладывает иконки через
AppleScript/Finder. Из песочницы агента и из headless-сессии обе операции
запрещены (`hdiutil create` → «Операция не разрешена», AppleScript → -10004).
Из обычного терминала на macOS:
```bash
cd /Volumes/Samsung/projects/glchat
make desktop-build # .app + .dmg + артефакты обновления
ls -lh desktop/src-tauri/target/release/bundle/dmg/
```
Если терминала с графической сессией нет (CI, headless), тот же образ
собирается без оформления окна — тем же скриптом Tauri с флагом
`--sandbox-safe` (он пропускает AppleScript и `hdiutil create`):
```bash
cd /Volumes/Samsung/projects/glchat
make desktop-build-app # .app и артефакты обновления
rm -rf /tmp/dmg-stage && mkdir -p /tmp/dmg-stage
cp -R desktop/src-tauri/target/release/bundle/macos/glchat.app /tmp/dmg-stage/
cd desktop/src-tauri/target/release/bundle/dmg
bash bundle_dmg.sh --volname glchat --volicon icon.icns \
--icon glchat.app 180 170 --app-drop-link 480 170 --hide-extension glchat.app \
--sandbox-safe glchat_0.1.0_aarch64.dmg /tmp/dmg-stage
hdiutil verify glchat_0.1.0_aarch64.dmg
```
Такой `.dmg` монтируется и содержит `glchat.app` и ссылку `Applications`, но без
красивой раскладки иконок — для локальных проверок и CI этого достаточно, для
публикации лучше собрать обычным `make desktop-build`.
Артефакты без `.dmg` (для автообновления) собираются где угодно:
`make desktop-build-app`.
### macOS: подпись Developer ID и нотаризация
Нужны сертификат Developer ID Application в связке ключей и пароль приложения
(appleid.apple.com → App-Specific Passwords):
```bash
security find-identity -v -p codesigning # проверить наличие сертификата
export APPLE_SIGNING_IDENTITY="Developer ID Application: … (TEAMID)"
export APPLE_ID="…@example.com"
export APPLE_PASSWORD="app-specific-password"
export APPLE_TEAM_ID="TEAMID"
make desktop-build # Tauri подпишет и отправит на нотаризацию
xcrun stapler validate desktop/src-tauri/target/release/bundle/macos/glchat.app
spctl --assess --type execute --verbose=4 desktop/src-tauri/target/release/bundle/macos/glchat.app
```
Переменные читает сам Tauri (`APPLE_*`), в репозиторий они не попадают.
Проверка «не подписано»: `codesign -dv --verbose=4 <app>` покажет `Signature=adhoc`.
### Windows (x64): MSI/NSIS + подпись
Сборка идёт на Windows-машине или в CI (`windows-latest`), Rust + Node 22:
```powershell
npx @tauri-apps/cli@^2 build --bundles nsis,msi
# подпись (EV/Trusted Signing или signtool с сертификатом):
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
/f cert.pfx /p $env:CERT_PASSWORD `
desktop\src-tauri\target\release\bundle\nsis\glchat_0.1.0_x64-setup.exe
```
Артефакты обновления (`*.nsis.zip`/`*.msi` + `.sig`) публикуются тем же
скриптом: `bash scripts/desktop-release.sh --artifact <файл> --target windows --arch x86_64`.
### Linux (AppImage + deb)
Сборка на Ubuntu 24.04/26.04 (нужны `libwebkit2gtk-4.1-dev`, `librsvg2-dev`,
`patchelf`, `libssl-dev`, `build-essential`):
```bash
sudo apt-get install -y libwebkit2gtk-4.1-dev librsvg2-dev patchelf libssl-dev build-essential
npx @tauri-apps/cli@^2 build --bundles appimage,deb
bash scripts/desktop-release.sh --artifact desktop/src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \
--target linux --arch x86_64
```
На Wayland нативная тряска окна при несохранённых изменениях не работает
(композитор запрещает `set_position`) — остаётся CSS-тряска контента; это
ожидаемое поведение, а не дефект.
### Ключ подписи обновлений
Приватный ключ — только в CI-секретах (`TAURI_SIGNING_PRIVATE_KEY`,
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`); локально — `build/desktop-keys/`
(каталог в `.gitignore`). Публичный ключ уже вшит в сборку и меняется только
вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.