feat(desktop): тихая установка обновления по флагу --update-now

Установка обновления всегда спрашивала согласие диалогом, поэтому сквозная
проверка «поверх старой версии» не автоматизировалась (в headless-прогоне
кликнуть некому), а на управляемых машинах обновление ставит оператор, а не
пользователь. С флагом `--update-now` обёртка проверяет обновление сразу при
старте (не через 30 секунд) и ставит найденное без диалога, после чего
перезапускается; при отсутствии обновления работает как обычно.

По умолчанию флаг выключен — трей, страница настроек и фоновая проверка
по-прежнему спрашивают согласие. Подпись артефакта проверяется в обоих
режимах: флаг убирает вопрос, но не проверку. Ход тихого обновления виден в
журнале, а в строку запуска добавлена версия сборки — иначе после перезапуска
не понять, какая версия работает.

Решение — D-077, приёмка — desktop/README.md («Локальная проверка»).
This commit is contained in:
2026-09-26 18:12:35 +03:00
parent 7512857363
commit b53cf9d6e7
4 changed files with 72 additions and 37 deletions
+1 -1
View File
@@ -120,7 +120,7 @@ pub fn desktop_reload<R: Runtime>(app: AppHandle<R>) -> Result<(), String> {
/// Проверяет обновления (кнопка на странице настроек). /// Проверяет обновления (кнопка на странице настроек).
#[tauri::command] #[tauri::command]
pub async fn desktop_check_updates<R: Runtime>(app: AppHandle<R>) { pub async fn desktop_check_updates<R: Runtime>(app: AppHandle<R>) {
updates::check(app, true).await; updates::check(app, true, false).await;
} }
/// Открывает ссылку в системном браузере. /// Открывает ссылку в системном браузере.
+26 -5
View File
@@ -44,6 +44,9 @@ struct Cli {
settings: bool, settings: bool,
/// Принудительно включить или выключить автозапуск (установка скриптом). /// Принудительно включить или выключить автозапуск (установка скриптом).
autostart: Option<bool>, autostart: Option<bool>,
/// Проверить обновление сразу при старте и установить его без диалога
/// (тихое обновление управляемых машин; по умолчанию выключено).
update_now: bool,
} }
impl Cli { impl Cli {
@@ -60,6 +63,7 @@ impl Cli {
"--instance" => cli.instance = args.next(), "--instance" => cli.instance = args.next(),
"--minimized" => cli.minimized = true, "--minimized" => cli.minimized = true,
"--settings" => cli.settings = true, "--settings" => cli.settings = true,
"--update-now" => cli.update_now = true,
"--autostart" => cli.autostart = args.next().as_deref().and_then(parse_switch), "--autostart" => cli.autostart = args.next().as_deref().and_then(parse_switch),
other => { other => {
if let Some(value) = other.strip_prefix("--instance=") { if let Some(value) = other.strip_prefix("--instance=") {
@@ -250,8 +254,11 @@ fn setup(app: &tauri::AppHandle, cli: &Cli) {
deeplink::install(app); deeplink::install(app);
// Версия в журнале: после автообновления это единственный след того, какая
// сборка реально запустилась (проверка обновления сама версию не печатает).
log::info!( log::info!(
"обёртка запущена: адрес={}, окно={}, автозапуск={}", "обёртка запущена: версия={}, адрес={}, окно={}, автозапуск={}",
app.package_info().version,
current.instance_url, current.instance_url,
if cli.minimized { 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()); 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::spawn(move || {
if update_now {
log::info!("тихое обновление по флагу --update-now: проверка сразу при старте");
} else {
std::thread::sleep(UPDATE_CHECK_DELAY); std::thread::sleep(UPDATE_CHECK_DELAY);
}
tauri::async_runtime::spawn(async move { 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.minimized);
assert!(cli.settings); assert!(cli.settings);
assert_eq!(cli.autostart, None); assert_eq!(cli.autostart, None);
// Тихая установка обновления включается только явным флагом.
assert!(!cli.update_now);
assert!(parse(&["--update-now"]).update_now);
} }
#[test] #[test]
+1 -1
View File
@@ -121,7 +121,7 @@ fn on_menu_event<R: Runtime>(app: &AppHandle<R>, event: tauri::menu::MenuEvent)
"updates" => { "updates" => {
let handle = app.clone(); let handle = app.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
updates::check(handle, true).await; updates::check(handle, true, false).await;
}); });
} }
// Выход из трея — такой же выход, как закрытие окна: если в клиенте // Выход из трея — такой же выход, как закрытие окна: если в клиенте
+17 -3
View File
@@ -28,7 +28,9 @@ const UPDATE_PATH: &str = "/updates/{{target}}/{{arch}}/{{current_version}}";
/// Проверяет обновления. `interactive` — вызов из меню трея или настроек, /// Проверяет обновления. `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() { if UPDATER_PUBKEY.is_empty() {
log::info!("автообновление не настроено: в сборке нет публичного ключа подписи"); log::info!("автообновление не настроено: в сборке нет публичного ключа подписи");
if interactive { if interactive {
@@ -82,7 +84,7 @@ pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
version: version.clone(), version: version.clone(),
}, },
); );
prompt_install(app, update, version).await; prompt_install(app, update, version, auto_install).await;
} }
Ok(None) => { Ok(None) => {
log::info!("обновлений нет"); log::info!("обновлений нет");
@@ -108,13 +110,22 @@ pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
} }
} }
/// Спрашивает согласие и устанавливает обновление. /// Спрашивает согласие и устанавливает обновление (в тихом режиме — сразу).
async fn prompt_install<R: Runtime>( async fn prompt_install<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
update: tauri_plugin_updater::Update, update: tauri_plugin_updater::Update,
version: String, version: String,
auto_install: bool,
) { ) {
let handle = app.clone(); let handle = app.clone();
if auto_install {
// Тихая установка: подпись артефакта проверяет плагин до распаковки —
// флаг убирает вопрос пользователю, но не проверку релиза.
log::info!(
"тихое обновление по флагу: {} → {version}",
app.package_info().version
);
} else {
let question = format!( let question = format!(
"Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \ "Доступна версия {version}. Скачать и установить сейчас? Приложение будет перезапущено, \
активный голосовой звонок прервётся." активный голосовой звонок прервётся."
@@ -135,6 +146,7 @@ async fn prompt_install<R: Runtime>(
if !confirmed { if !confirmed {
return; return;
} }
}
match update match update
.download_and_install(|_chunk, _total| {}, || {}) .download_and_install(|_chunk, _total| {}, || {})
@@ -146,6 +158,7 @@ async fn prompt_install<R: Runtime>(
} }
Err(error) => { Err(error) => {
log::error!("установка обновления не удалась: {error}"); log::error!("установка обновления не удалась: {error}");
if !auto_install {
app.dialog() app.dialog()
.message(format!( .message(format!(
"Обновление не установлено: {error}. Скачать установщик вручную можно на \ "Обновление не установлено: {error}. Скачать установщик вручную можно на \
@@ -157,6 +170,7 @@ async fn prompt_install<R: Runtime>(
} }
} }
} }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {