Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7c9c93443 | |||
| 2d013df0bb | |||
| b53cf9d6e7 |
@@ -191,9 +191,12 @@ web-build: ## Сборка web-клиента в web/dist
|
||||
DESKTOP_DIR := $(ROOT)/desktop
|
||||
DESKTOP_TAURI_DIR := $(DESKTOP_DIR)/src-tauri
|
||||
DESKTOP_KEY ?= $(BUILD_DIR)/desktop-keys/glchat-updater.key
|
||||
# Бандлы Tauri: `all` — обычная сборка, `app` — только .app (и артефакты
|
||||
# обновления) без .dmg: сборка .dmg монтирует образ и требует Finder.
|
||||
# Бандлы Tauri: `all` — обычная сборка (значение по умолчанию из tauri.conf.json),
|
||||
# `app` — только .app (и артефакты обновления) без .dmg: сборка .dmg монтирует
|
||||
# образ и требует Finder. Tauri CLI не принимает `all` в --bundles: при `all`
|
||||
# флаг не передаётся вовсе, и список бандлов берётся из bundle.targets.
|
||||
DESKTOP_BUNDLES ?= all
|
||||
DESKTOP_BUNDLES_ARG := $(if $(filter-out all,$(DESKTOP_BUNDLES)),--bundles $(DESKTOP_BUNDLES),)
|
||||
TAURI_CLI ?= npx --yes @tauri-apps/cli@^2
|
||||
export CARGO_HOME ?= $(CACHE_DIR)/cargo
|
||||
export NPM_CONFIG_CACHE ?= $(CACHE_DIR)/npm
|
||||
@@ -211,11 +214,11 @@ desktop-build: ## Собрать desktop-приложение (бандлы по
|
||||
cd $(DESKTOP_TAURI_DIR) && \
|
||||
TAURI_SIGNING_PRIVATE_KEY="$$(cat $(DESKTOP_KEY))" \
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-glchat-local-dev}" \
|
||||
$(TAURI_CLI) build --bundles $(DESKTOP_BUNDLES); \
|
||||
$(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG); \
|
||||
else \
|
||||
echo " ключа $(DESKTOP_KEY) нет: сборка без артефактов автообновления"; \
|
||||
cd $(DESKTOP_TAURI_DIR) && \
|
||||
$(TAURI_CLI) build --bundles $(DESKTOP_BUNDLES) \
|
||||
$(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG) \
|
||||
--config '{"bundle":{"createUpdaterArtifacts":false}}'; \
|
||||
fi
|
||||
|
||||
|
||||
+116
-8
@@ -32,6 +32,12 @@ open desktop/src-tauri/target/release/bundle/macos/glchat.app
|
||||
Finder через AppleScript, поэтому из песочницы агента и из headless-окружения он
|
||||
падает (см. «Что не сделано»).
|
||||
|
||||
`open --args` из песочницы агента аргументы не доставляет (проверено на
|
||||
macOS 27: и на бандле glchat, и на системном TextEdit — процесс запускается без
|
||||
них; из обычного терминала то же самое работает). Поэтому `make desktop-run
|
||||
ARGS='--minimized'` и замер свёрнутого окна в `scripts/desktop-perf.py` надо
|
||||
запускать из обычного терминала, а не из песочницы.
|
||||
|
||||
Сборка релиза с артефактами автообновления использует ключ подписи:
|
||||
|
||||
```bash
|
||||
@@ -53,6 +59,7 @@ make desktop-build
|
||||
| `--minimized` | запуск свёрнутым в трей (используется автозапуском) |
|
||||
| `--settings` | сразу открыть страницу настроек обёртки |
|
||||
| `--autostart on\|off` | включить или выключить автозапуск при установке скриптом |
|
||||
| `--update-now` | сразу проверить обновление и поставить его без диалога (тихое обновление) |
|
||||
|
||||
## Настройки
|
||||
|
||||
@@ -87,6 +94,33 @@ make desktop-build
|
||||
`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/...` доходит до него без правок
|
||||
@@ -145,6 +179,54 @@ curl -s https://gl.mhspx.su/updates/darwin/aarch64/0.1.0 | jq .
|
||||
подтверждение в диалоге (во время звонка перезапуск без согласия не делается) →
|
||||
подпись проверяется до установки, 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`,
|
||||
а версия на диске — остаться прежней.
|
||||
|
||||
## Уведомления и переход в канал
|
||||
|
||||
Решение «показывать ли уведомление» принимает веб-клиент (упоминания и личные
|
||||
@@ -196,16 +278,21 @@ python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта +
|
||||
|
||||
## Что не сделано (открытые пункты Фазы 6)
|
||||
|
||||
- **Автообновление end-to-end не проверено на реальном релизе**: манифест
|
||||
отдаётся и разбирается (проверено на стенде), но установка «поверх старой
|
||||
версии» требует публикации версии с бо́льшим номером — это действие оператора
|
||||
(см. «Публикация релиза» выше).
|
||||
- **Автообновление проверено локально «поверх старой версии»** (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).
|
||||
- **`.dmg`**: собирается из обычного терминала (Finder/AppleScript для раскладки
|
||||
окна); в песочнице агента `hdiutil create` и AppleScript запрещены, но образ
|
||||
**без оформления окна** собирается флагом `--sandbox-safe` к `bundle_dmg.sh`
|
||||
(проверено 2026-09-26, см. ниже).
|
||||
- **Windows/Linux-бандлы**: конфигурация кросс-платформенная, но собирались и
|
||||
проверялись только macOS-arm64.
|
||||
|
||||
@@ -213,9 +300,10 @@ python3 scripts/desktop-perf.py --runs 3 # 3 холодных старта +
|
||||
|
||||
### macOS: `.dmg`
|
||||
|
||||
`bundle_dmg.sh` монтирует образ (`hdiutil`) и управляет Finder через AppleScript,
|
||||
поэтому из песочницы агента и из headless-сессии он падает. Из обычного
|
||||
терминала на macOS:
|
||||
`bundle_dmg.sh` создаёт образ через `hdiutil create` и раскладывает иконки через
|
||||
AppleScript/Finder. Из песочницы агента и из headless-сессии обе операции
|
||||
запрещены (`hdiutil create` → «Операция не разрешена», AppleScript → -10004).
|
||||
Из обычного терминала на macOS:
|
||||
|
||||
```bash
|
||||
cd /Volumes/Samsung/projects/glchat
|
||||
@@ -223,6 +311,26 @@ 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`.
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn desktop_reload<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
|
||||
/// Проверяет обновления (кнопка на странице настроек).
|
||||
#[tauri::command]
|
||||
pub async fn desktop_check_updates<R: Runtime>(app: AppHandle<R>) {
|
||||
updates::check(app, true).await;
|
||||
updates::check(app, true, false).await;
|
||||
}
|
||||
|
||||
/// Открывает ссылку в системном браузере.
|
||||
|
||||
@@ -44,6 +44,9 @@ struct Cli {
|
||||
settings: bool,
|
||||
/// Принудительно включить или выключить автозапуск (установка скриптом).
|
||||
autostart: Option<bool>,
|
||||
/// Проверить обновление сразу при старте и установить его без диалога
|
||||
/// (тихое обновление управляемых машин; по умолчанию выключено).
|
||||
update_now: bool,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
@@ -60,6 +63,7 @@ impl Cli {
|
||||
"--instance" => cli.instance = args.next(),
|
||||
"--minimized" => cli.minimized = true,
|
||||
"--settings" => cli.settings = true,
|
||||
"--update-now" => cli.update_now = true,
|
||||
"--autostart" => cli.autostart = args.next().as_deref().and_then(parse_switch),
|
||||
other => {
|
||||
if let Some(value) = other.strip_prefix("--instance=") {
|
||||
@@ -250,8 +254,11 @@ fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
||||
|
||||
deeplink::install(app);
|
||||
|
||||
// Версия в журнале: после автообновления это единственный след того, какая
|
||||
// сборка реально запустилась (проверка обновления сама версию не печатает).
|
||||
log::info!(
|
||||
"обёртка запущена: адрес={}, окно={}, автозапуск={}",
|
||||
"обёртка запущена: версия={}, адрес={}, окно={}, автозапуск={}",
|
||||
app.package_info().version,
|
||||
current.instance_url,
|
||||
if cli.minimized {
|
||||
"свёрнуто"
|
||||
@@ -267,16 +274,27 @@ fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
||||
}
|
||||
}
|
||||
|
||||
spawn_update_check(app.clone());
|
||||
spawn_update_check(app.clone(), cli.update_now);
|
||||
spawn_autostart_prompt(app.clone());
|
||||
}
|
||||
|
||||
/// Фоновая проверка обновлений при старте (без диалогов).
|
||||
fn spawn_update_check(app: tauri::AppHandle) {
|
||||
/// Проверка обновлений при старте.
|
||||
///
|
||||
/// Обычный режим — фоновая проверка через [`UPDATE_CHECK_DELAY`], найденное
|
||||
/// обновление ставится только после подтверждения пользователя. С флагом
|
||||
/// `--update-now` проверка идёт сразу, а установка — без диалога: это режим
|
||||
/// управляемых машин, где обновление ставит оператор, а не пользователь
|
||||
/// (desktop/README.md, «Автообновление»). Подпись проверяется в обоих режимах:
|
||||
/// флаг убирает только вопрос, а не проверку артефакта.
|
||||
fn spawn_update_check(app: tauri::AppHandle, update_now: bool) {
|
||||
std::thread::spawn(move || {
|
||||
if update_now {
|
||||
log::info!("тихое обновление по флагу --update-now: проверка сразу при старте");
|
||||
} else {
|
||||
std::thread::sleep(UPDATE_CHECK_DELAY);
|
||||
}
|
||||
tauri::async_runtime::spawn(async move {
|
||||
updates::check(app, false).await;
|
||||
updates::check(app, false, update_now).await;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -332,6 +350,9 @@ mod tests {
|
||||
assert!(cli.minimized);
|
||||
assert!(cli.settings);
|
||||
assert_eq!(cli.autostart, None);
|
||||
// Тихая установка обновления включается только явным флагом.
|
||||
assert!(!cli.update_now);
|
||||
assert!(parse(&["--update-now"]).update_now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -121,7 +121,7 @@ fn on_menu_event<R: Runtime>(app: &AppHandle<R>, event: tauri::menu::MenuEvent)
|
||||
"updates" => {
|
||||
let handle = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
updates::check(handle, true).await;
|
||||
updates::check(handle, true, false).await;
|
||||
});
|
||||
}
|
||||
// Выход из трея — такой же выход, как закрытие окна: если в клиенте
|
||||
|
||||
@@ -28,7 +28,9 @@ const UPDATE_PATH: &str = "/updates/{{target}}/{{arch}}/{{current_version}}";
|
||||
|
||||
/// Проверяет обновления. `interactive` — вызов из меню трея или настроек,
|
||||
/// тогда пользователь видит результат в диалоге; фоновая проверка молчит.
|
||||
pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
|
||||
/// `auto_install` — тихий режим (`--update-now`): найденное обновление ставится
|
||||
/// без вопроса, диалогов нет вовсе.
|
||||
pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool, auto_install: bool) {
|
||||
if UPDATER_PUBKEY.is_empty() {
|
||||
log::info!("автообновление не настроено: в сборке нет публичного ключа подписи");
|
||||
if interactive {
|
||||
@@ -82,7 +84,7 @@ pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
|
||||
version: version.clone(),
|
||||
},
|
||||
);
|
||||
prompt_install(app, update, version).await;
|
||||
prompt_install(app, update, version, auto_install).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
log::info!("обновлений нет");
|
||||
@@ -108,13 +110,22 @@ pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Спрашивает согласие и устанавливает обновление.
|
||||
/// Спрашивает согласие и устанавливает обновление (в тихом режиме — сразу).
|
||||
async fn prompt_install<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
update: tauri_plugin_updater::Update,
|
||||
version: String,
|
||||
auto_install: bool,
|
||||
) {
|
||||
let handle = app.clone();
|
||||
if auto_install {
|
||||
// Тихая установка: подпись артефакта проверяет плагин до распаковки —
|
||||
// флаг убирает вопрос пользователю, но не проверку релиза.
|
||||
log::info!(
|
||||
"тихое обновление по флагу: {} → {version}",
|
||||
app.package_info().version
|
||||
);
|
||||
} else {
|
||||
let question = format!(
|
||||
"Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \
|
||||
активный голосовой звонок прервётся."
|
||||
@@ -135,6 +146,7 @@ async fn prompt_install<R: Runtime>(
|
||||
if !confirmed {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match update
|
||||
.download_and_install(|_chunk, _total| {}, || {})
|
||||
@@ -146,6 +158,7 @@ async fn prompt_install<R: Runtime>(
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("установка обновления не удалась: {error}");
|
||||
if !auto_install {
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
"Обновление не установлено: {error}. Скачать установщик вручную можно на \
|
||||
@@ -156,6 +169,7 @@ async fn prompt_install<R: Runtime>(
|
||||
.show(|_| {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user