- `--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).
26 KiB
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 # конфигурация сборки и плагинов
Сборка и запуск
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 надо
запускать из обычного терминала, а не из песочницы.
Сборка релиза с артефактами автообновления использует ключ подписи:
# ключ и пароль не коммитятся: приватный ключ живёт в 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:
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 # манифест последнего релиза — его и отдаёт инстанс
Публикация релиза:
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 — лишняя
работа без выгоды, поэтому не рекомендуется.
Проверка (стенд):
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. Порядок действий (все данные — в каталоге репозитория, стенд не затрагивается):
# 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 МБ.
Повторить замер:
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:
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):
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):
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:
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):
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). Публичный ключ уже вшит в сборку и меняется только
вместе с ключом: старые сборки отвергнут манифест, подписанный новым ключом.