fix(updater): drive download/install/restart from backend to avoid hang

The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall()
then relaunchApp(). relaunch() routed through AppHandle::restart(), and
the old WebView had to keep running JS after the .app bundle was already
swapped — an unstable window that could hang the update or leave the old
version running until a manual restart.

Move the whole download -> install -> cleanup -> restart chain into a new
backend command install_update_and_restart, so it no longer depends on the
old WebView running JS after the bundle is swapped.

Platform-aware install ordering (install() behaves differently per OS):
- Windows: install() launches the external installer and exits the process
  internally, so cleanup + single-instance destroy must run before install.
  Surface a recovery hint on failure since the proxy may already be stopped.
- macOS/Linux: install() returns, so install first then cleanup — an install
  failure no longer wrongly stops the proxy / reverts takeover.

Eliminate the restart vs single-instance race: restart_process() destroys
the single-instance lock (remove socket on macOS, ReleaseMutex on Windows)
before tauri::process::restart(), so the freshly spawned process can't
connect to the old listener and exit itself.

Also remove the now-dead frontend update plumbing (relaunchApp,
UpdateHandle, mapUpdateHandle) and surface backend errors in the toast.

Adapted from the original af4271f4 while rebasing onto #4069: the
ExitRequested handler changes were dropped entirely — the classifier from
#4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow,
and the original should_restart branch (prevent_exit + async cleanup)
would have reintroduced the window-state deadlock that #4069 fixed.
install_update_and_restart bypasses ExitRequested entirely, so the two
fixes compose cleanly.
This commit is contained in:
Jason
2026-06-03 16:36:54 +08:00
parent a3598fd976
commit 7d0eacac33
6 changed files with 100 additions and 103 deletions
+17
View File
@@ -1165,6 +1165,7 @@ pub fn run() {
commands::get_log_config,
commands::set_log_config,
commands::restart_app,
commands::install_update_and_restart,
commands::check_for_updates,
commands::is_portable_mode,
commands::copy_text_to_clipboard,
@@ -1962,6 +1963,22 @@ pub fn save_window_state_before_exit(app_handle: &tauri::AppHandle) {
}
}
/// 主动释放 single-instance 锁。
///
/// macOS single-instance 使用 `/tmp/{identifier}.sock`。我们有若干路径会直接
/// `std::process::exit(0)`,不会触发插件挂在 `RunEvent::Exit` 上的清理钩子。
/// 重启前主动 destroy 可以避免新进程误连旧 listener 后自行退出。
pub fn destroy_single_instance_lock(app_handle: &tauri::AppHandle) {
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
tauri_plugin_single_instance::destroy(app_handle);
}
/// 释放 single-instance 锁后重启当前应用。
pub fn restart_process(app_handle: &tauri::AppHandle) -> ! {
destroy_single_instance_lock(app_handle);
tauri::process::restart(&app_handle.env());
}
#[cfg(test)]
mod tests {
use super::{classify_exit_request, ExitRequestAction};