tauri-apps / tauri

Build smaller, faster, and more secure desktop and mobile applications with a web frontend.
https://tauri.app
Apache License 2.0
85.45k stars 2.58k forks source link

[bug] App::restart does not restart after update.download_and_install in rust #11392

Open jacobbuck72 opened 1 month ago

jacobbuck72 commented 1 month ago

Describe the bug

When a new version on macOS has been downloaded and installed the app closes down but does not start again.

It is a desktop app for macOS and Windows. It has a main window that the user can close/hide while the icon tray remains. So on WindowEvent::CloseRequested we prevent closing the app. Could this cause the issue?

We just migrated to Tauri 2 so had to provide our own dialog (see updater.rs below) and doing version check and triggering download, install and restart (which fails on macOS).

The app is built with our self hosted GitLab CI/CD pipeline producing a static updater.json uploaded to S3 together with the artefacts:

{
    "version": "#VERSION",
    "notes": "",
    "pub_date": "#PUB_TIME",
    "platforms": {
        "universal-apple-darwin": {
            "signature": "#UNIVERSAL_APPLE_DARWIN_SIGNATURE",
            "url": "https://autoremind-app.xxx/release/autoremind-app.app.tar.gz"
        },
    }
}

Important part of my main.rs:

use tauri::{ Builder, Manager, Window, WindowEvent, WebviewWindowBuilder, WebviewUrl};

#[path = "updater.rs"]
mod updater;

fn main() {
    let context = tauri::generate_context!();
    let url = format!("http://localhost:{}", BUILD_PORT).parse().unwrap();
    let window_url = WebviewUrl::External(url);

    //let app2 = tauri::Builder::default()
    Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
            println!("Trying to open another instance");
            let window = app.get_webview_window("main")
                .expect("no main window");
            window.show().map_err(|err| println!("{:?}", err)).ok();
            let _ = window.set_focus();
        }))
        .plugin(tauri_plugin_localhost::Builder::new(BUILD_PORT).build())
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_process::init())
        .setup(|app| {
            WebviewWindowBuilder::new(
                app,
                "main".to_string(),
                if cfg!(dev) {
                    Default::default()
                } else {
                    window_url
                },
            )
                .title(MAIN_WINDOW_TITLE)
                .position(30.0, 30.0)
                .inner_size(800.0, 650.0)
                .initialization_script(&pass_app_environment_to_window())
                .build()?;

            menubar::build_tray_and_menus(app);

            badge::set_badge(app.handle(), 5);

            updater::init(&app);
            Ok(())
        })
        .on_window_event(handle_window_event)
        //.enable_macos_default_menu(false)
        .invoke_handler(tauri::generate_handler![
            open_two_way,
            close_two_way,
            set_badge
        ])
        //.build(context)  // in this case app.run() below is needed
        .run(context)
        .expect("error while running tauri application");
}

fn handle_window_event(window: &Window, event: &WindowEvent) {
    match event {
        WindowEvent::CloseRequested { api, .. } => {
            api.prevent_close(); // We want the icon tray to remain after closing main window
            window.hide().unwrap();
        }
        _ => {}
    }
}

I have created updater.rs:

use tauri::App;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
use tauri_plugin_updater::UpdaterExt;

pub fn init(app: &App) {
    let handle = app.handle().clone();
    tauri::async_runtime::spawn(async move {
        let _response = update_setup(handle).await;
    });
}

async fn update_setup(app: tauri::AppHandle) -> tauri::Result<()> {
  if let Some(update) = app.updater().unwrap().check().await.unwrap() {

    let current_version = app.package_info().version.to_string();
    let new_version = update.version.to_string();

    app.dialog()
        .message(&format!("Version {new_version} is now available -- you have {current_version}. \nWould you like to install it now?"))
        .title("A new version of AutoRemind App is available")
        .buttons(MessageDialogButtons::OkCancelCustom("Yes".to_string(), "No".to_string()))
        .show(|result| match result {
            false => {
                println!("Ignored update");
                },
            true => {
                tauri::async_runtime::spawn(async move {
                    let mut downloaded = 0;
                    update.download_and_install(|chunk_length, content_length| {
                        downloaded += chunk_length;
                        println!("downloaded {downloaded} / {content_length:?}");
                    }, || {
                        println!("download finished");
                    }).await.unwrap();

                    println!("update installed. Restarting.");
                    app.restart(); // This shuts down but does not not relaunch new version
            });
        }
    });
  }

  Ok(())
}

capabilities/main.json

{
    "$schema": "./schemas/desktop-schema.json",
    "identifier": "main-capability",
    "description": "Capability for the main window",
    "windows": ["main"],
    "local": false,
    "remote": {
        "urls": ["http://localhost:4000/", "http://localhost:4200/"]
    },
    "permissions": [
        "core:event:allow-emit",
        "core:event:allow-listen",
        "core:window:allow-set-resizable",
        "core:window:allow-set-size",
        "core:webview:allow-internal-toggle-devtools",
        "updater:allow-check",
        "updater:allow-download",
        "updater:allow-download-and-install",
        "updater:allow-install",
        "process:default",
        "process:allow-exit",
        "process:allow-restart"
    ]
}

Important parts of tauri.config.json:

{
    "productName": "AutoRemind App",
    "mainBinaryName": "AutoRemind App",
    "version": "0.1.10000", //replaced by the CI/CD script
    "identifier": "com.autoremind.app",
    "bundle": {
        "active": true,
        "externalBin": [],
        "targets": [
            "nsis",
            "dmg",
            "app"
        ],
        "macOS": {
            "entitlements": null,
            "exceptionDomain": "",
            "frameworks": [],
            "providerShortName": null,
            "signingIdentity": "Developer ID Application: AutoRemind Inc (XXXXXXXX)"
        },
        "resources": [],
        "createUpdaterArtifacts": true
    },
    "app": {
        "windows": [],
        "security": {
            "csp": null
        }
    },
    "plugins": {
        "updater": {
            "endpoints": [
                "https://autoremind-app.xxx/updater.json"
            ],
            "windows": {
                "installMode": "passive"
            },
            "pubkey": "XXXXXXXXXXXX"
        }
    }
}

zx

Reproduction

I have not tried building a simple app since this would require a full CI pipeline and new S3 bucket to test. But it's the same every time I do this:

  1. Let our GitLab CI pipeline build a new set of artefacts (DMG and .app.tar.gz file with .app.tar.sig file), create new updater.json file and upload all to S3 - version 1
  2. Build a newer version 2
  3. Download version 1 DMG and install (move app to Applications)
  4. Launch the application clicking on the app in Applications
  5. Accept dialog asking me to upgrade to version 2
  6. Observe the app closing (but not starting again)
  7. Manually launch the app from Applications and observe (in an About dialog) that it's on version 2

Expected behavior

After installing the update the app should start again.

Full tauri info output

[✔] Environment
    - OS: Mac OS 14.6.1 arm64 (X64)
    ✔ Xcode Command Line Tools: installed
    ✔ rustc: 1.81.0 (eeb90cda1 2024-09-04)
    ✔ cargo: 1.81.0 (2dbb1af80 2024-08-20)
    ✔ rustup: 1.27.1 (54dd3d00f 2024-04-24)
    ✔ Rust toolchain: stable-aarch64-apple-darwin (default)
    - node: 22.3.0
    - pnpm: 9.7.0
    - npm: 10.8.1

[-] Packages
    - tauri 🦀: 2.0.2
    - tauri-build 🦀: 2.0.1
    - wry 🦀: 0.44.1
    - tao 🦀: 0.30.3

[-] Plugins
    - tauri-plugin-fs 🦀: 2.0.1
    - tauri-plugin-process 🦀: 2.0.1
    - tauri-plugin-updater 🦀: 2.0.2
    - tauri-plugin-localhost 🦀: 2.0.1
    - tauri-plugin-dialog 🦀: 2.0.1
    - tauri-plugin-single-instance 🦀: 2.0.1

[-] App
    - build-type: bundle
    - CSP: unset
    - frontendDist: ../dist/apps/portal-desktop/.next
    - devUrl: http://localhost:4200/

Stack trace

I do not know how to produce a stack trace. I've built a version with --debug and launched from Termial with RUST_LOG=debug (also tested with "trace") and got no errors or exceptions - only the app printing that it was restarting. This is the last I see:

downloaded 30171747 / Some(30203582)
downloaded 30188131 / Some(30203582)
downloaded 30203582 / Some(30203582)
download finished
update installed. Restarting.

Additional context

No response

jacobbuck72 commented 3 weeks ago

I think it is a race condition and I've made a work-around...

I’ve narrowed it down to the fact that in app.restart() it is triggering the RunEvent::ExitRequested and right after it is calling process::restart(). And at least on my MacBook M2 Pro the process:restart() method starts loading the Info.plist but before this I/O is done the RunEvent:Exit is already dispatched ad the process killed before process:restarts() manages to fully spawn the new process. So it seems like a race condition between two threads trying to exit the process.

I’ve resolved this by calling app.exit(RESTART_EXIT_CODE) in stead and then on RunEvent::Exit I trigger process:update() (which will call exit(0) when done but first doing app.cleanup_before_exit() before calling process::update() since eventloop is stopped here.

    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;

       let app = Builder::default()
        .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
            log::warn!("Single instance version {}", app.package_info().version.to_string());
        }))
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_process::init())

        ...        

        let is_restart_requested = Arc::new(AtomicBool::new(false));
        let is_restart_requested_clone = is_restart_requested.clone();

        // Attempt to better handle removal of system tray on Windows
        app.run(move |app_handle, event| match event {
            tauri::RunEvent::ExitRequested { code, ..} => {
                if code.unwrap() == RESTART_EXIT_CODE {
                    // RunEvent does not hold the exit code so we store it separately
                    is_restart_requested.store(true, Ordering::SeqCst);
                }
            }
            tauri::RunEvent::Exit => {
                if is_restart_requested_clone.load(Ordering::SeqCst) {
                    app_handle.cleanup_before_exit();
                    let env = _app_handle.env();
                    tauri::process::restart(&env); // this will call exit(0) so we'll not return to the event loop
                }
            },
            _ => {}
        });

Perhaps it would be a good idea if the runtime would handle this restart in the RunEvent::Exit handling (after callback) and with the RunEvent::Exit passing both "api" and "code" with the option to call api.prevent_restart() - or api.prevent_exit() should cancel both exit and restart in the RunEvent::Exit step. Then app.restart() would just need to trigger RunEvent::ExitRequested(RESTART_EXIT_CODE).

idevsoftware commented 1 week ago

Hi, I just came to leave a +1 - I'm new to Tauri and I suspect I'm seeing the same issue happening.

baoyachi commented 1 day ago

+1