From b53cf9d6e70d4266b7a67cf5d2f0fc99370344be Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 26 Sep 2026 18:12:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20=D1=82=D0=B8=D1=85=D0=B0=D1=8F?= =?UTF-8?q?=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=84=D0=BB=D0=B0=D0=B3=D1=83=20--update-now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Установка обновления всегда спрашивала согласие диалогом, поэтому сквозная проверка «поверх старой версии» не автоматизировалась (в headless-прогоне кликнуть некому), а на управляемых машинах обновление ставит оператор, а не пользователь. С флагом `--update-now` обёртка проверяет обновление сразу при старте (не через 30 секунд) и ставит найденное без диалога, после чего перезапускается; при отсутствии обновления работает как обычно. По умолчанию флаг выключен — трей, страница настроек и фоновая проверка по-прежнему спрашивают согласие. Подпись артефакта проверяется в обоих режимах: флаг убирает вопрос, но не проверку. Ход тихого обновления виден в журнале, а в строку запуска добавлена версия сборки — иначе после перезапуска не понять, какая версия работает. Решение — D-077, приёмка — desktop/README.md («Локальная проверка»). --- desktop/src-tauri/src/commands.rs | 2 +- desktop/src-tauri/src/lib.rs | 33 +++++++++++--- desktop/src-tauri/src/tray.rs | 2 +- desktop/src-tauri/src/updates.rs | 72 ++++++++++++++++++------------- 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/desktop/src-tauri/src/commands.rs b/desktop/src-tauri/src/commands.rs index 77e5da2..f4ac448 100644 --- a/desktop/src-tauri/src/commands.rs +++ b/desktop/src-tauri/src/commands.rs @@ -120,7 +120,7 @@ pub fn desktop_reload(app: AppHandle) -> Result<(), String> { /// Проверяет обновления (кнопка на странице настроек). #[tauri::command] pub async fn desktop_check_updates(app: AppHandle) { - updates::check(app, true).await; + updates::check(app, true, false).await; } /// Открывает ссылку в системном браузере. diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 74bf398..d999eea 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -44,6 +44,9 @@ struct Cli { settings: bool, /// Принудительно включить или выключить автозапуск (установка скриптом). autostart: Option, + /// Проверить обновление сразу при старте и установить его без диалога + /// (тихое обновление управляемых машин; по умолчанию выключено). + 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 || { - std::thread::sleep(UPDATE_CHECK_DELAY); + 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] diff --git a/desktop/src-tauri/src/tray.rs b/desktop/src-tauri/src/tray.rs index c6f2dd9..99f0489 100644 --- a/desktop/src-tauri/src/tray.rs +++ b/desktop/src-tauri/src/tray.rs @@ -121,7 +121,7 @@ fn on_menu_event(app: &AppHandle, 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; }); } // Выход из трея — такой же выход, как закрытие окна: если в клиенте diff --git a/desktop/src-tauri/src/updates.rs b/desktop/src-tauri/src/updates.rs index e1f493f..8889c49 100644 --- a/desktop/src-tauri/src/updates.rs +++ b/desktop/src-tauri/src/updates.rs @@ -28,7 +28,9 @@ const UPDATE_PATH: &str = "/updates/{{target}}/{{arch}}/{{current_version}}"; /// Проверяет обновления. `interactive` — вызов из меню трея или настроек, /// тогда пользователь видит результат в диалоге; фоновая проверка молчит. -pub async fn check(app: AppHandle, interactive: bool) { +/// `auto_install` — тихий режим (`--update-now`): найденное обновление ставится +/// без вопроса, диалогов нет вовсе. +pub async fn check(app: AppHandle, interactive: bool, auto_install: bool) { if UPDATER_PUBKEY.is_empty() { log::info!("автообновление не настроено: в сборке нет публичного ключа подписи"); if interactive { @@ -82,7 +84,7 @@ pub async fn check(app: AppHandle, interactive: bool) { version: version.clone(), }, ); - prompt_install(app, update, version).await; + prompt_install(app, update, version, auto_install).await; } Ok(None) => { log::info!("обновлений нет"); @@ -108,32 +110,42 @@ pub async fn check(app: AppHandle, interactive: bool) { } } -/// Спрашивает согласие и устанавливает обновление. +/// Спрашивает согласие и устанавливает обновление (в тихом режиме — сразу). async fn prompt_install( app: AppHandle, update: tauri_plugin_updater::Update, version: String, + auto_install: bool, ) { let handle = app.clone(); - let question = format!( - "Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \ - активный голосовой звонок прервётся." - ); - let (tx, rx) = std::sync::mpsc::channel::(); - app.dialog() - .message(question) - .title("Обновление glchat") - .buttons(MessageDialogButtons::YesNo) - .show(move |confirmed| { - let _ = tx.send(confirmed); - }); + if auto_install { + // Тихая установка: подпись артефакта проверяет плагин до распаковки — + // флаг убирает вопрос пользователю, но не проверку релиза. + log::info!( + "тихое обновление по флагу: {} → {version}", + app.package_info().version + ); + } else { + let question = format!( + "Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \ + активный голосовой звонок прервётся." + ); + let (tx, rx) = std::sync::mpsc::channel::(); + app.dialog() + .message(question) + .title("Обновление glchat") + .buttons(MessageDialogButtons::YesNo) + .show(move |confirmed| { + let _ = tx.send(confirmed); + }); - // Диалог асинхронный: ждём ответ, не блокируя основной поток. - let confirmed = tauri::async_runtime::spawn_blocking(move || rx.recv().unwrap_or(false)) - .await - .unwrap_or(false); - if !confirmed { - return; + // Диалог асинхронный: ждём ответ, не блокируя основной поток. + let confirmed = tauri::async_runtime::spawn_blocking(move || rx.recv().unwrap_or(false)) + .await + .unwrap_or(false); + if !confirmed { + return; + } } match update @@ -146,14 +158,16 @@ async fn prompt_install( } Err(error) => { log::error!("установка обновления не удалась: {error}"); - app.dialog() - .message(format!( - "Обновление не установлено: {error}. Скачать установщик вручную можно на \ - странице инстанса." - )) - .title("Обновление glchat") - .buttons(MessageDialogButtons::Ok) - .show(|_| {}); + if !auto_install { + app.dialog() + .message(format!( + "Обновление не установлено: {error}. Скачать установщик вручную можно на \ + странице инстанса." + )) + .title("Обновление glchat") + .buttons(MessageDialogButtons::Ok) + .show(|_| {}); + } } } }