tauri-apps / tauri

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

[bug] `ExitRequested` not fired on macOS #9198

Open lockieluke opened 8 months ago

lockieluke commented 8 months ago

Describe the bug

// Prevents additipub(crate)onal console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;

use tauri::{Manager, RunEvent, WindowEvent};
use tokio::runtime::Runtime;

use crate::download_helper::{get_download_client, pause_all_downloads, start_download};
use crate::hardware::{get_cpu_architecture, get_disk_usage, get_friendly_os_name, get_os_type};
use crate::window_ext::WindowExt;

mod window_ext;
mod hardware;
mod download_helper;
mod utils;

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
async fn show_main_window(window: tauri::Window) {
    window.show().expect("Failed to show main window");
}

#[tokio::main]
async fn main() {
    tauri::async_runtime::set(tokio::runtime::Handle::current());

    let app = tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            show_main_window,
            get_cpu_architecture,
            get_os_type,
            get_friendly_os_name,
            get_disk_usage,
            pause_all_downloads,
            start_download
        ])
        .on_window_event(|e| {
            let apply_offset = || {
                let win = e.window();
                win.position_traffic_lights(20., 20.);
            };

            match e.event() {
                WindowEvent::Resized(..) => apply_offset(),
                WindowEvent::ThemeChanged(..) => apply_offset(),
                _ => {}
            }
        })
        .build(tauri::generate_context!())
        .expect("error while running tauri application");

    app.run(|app, event| match event {
        RunEvent::ExitRequested {api, ..} => {
            api.prevent_exit();

            println!("HELLO");

            tauri::async_runtime::block_on(async {
                pause_all_downloads().await;
            });
        },
        RunEvent::Ready { .. } => {
            let win = app.get_window("main").unwrap();
            win.set_transparent_titlebar(true);
            win.position_traffic_lights(20., 20.);
        },
        _ => {}
    });
}

nothing gets run when exit is requested

Reproduction

No response

Expected behavior

No response

Full tauri info output

[✔] Environment
    - OS: Mac OS 14.2.1 X64
    ✔ Xcode Command Line Tools: installed
    ✔ rustc: 1.78.0-nightly (0ecbd0605 2024-02-25)
    ✔ cargo: 1.78.0-nightly (194a60b29 2024-02-21)
    ✔ rustup: 1.26.0 (5af9b9484 2023-04-05)
    ✔ Rust toolchain: nightly-aarch64-apple-darwin (default)
    - node: 21.1.0
    - pnpm: 8.15.4
    - yarn: 4.0.2
    - npm: 9.1.2
    - bun: 1.0.30

[-] Packages
    - tauri [RUST]: 1.6.1
    - tauri-build [RUST]: 1.5.1
    - wry [RUST]: 0.24.7
    - tao [RUST]: 0.16.7
    - tauri-cli [RUST]: 1.5.10
    - @tauri-apps/api [NPM]: 1.5.3
    - @tauri-apps/cli [NPM]: 1.5.11

[-] App
    - build-type: bundle
    - CSP: unset
    - distDir: ../dist
    - devPath: http://localhost:1420/
    - framework: SolidJS
    - bundler: Vite


### Stack trace

_No response_

### Additional context

_No response_
lockieluke commented 7 months ago

For those who are looking for a way to reliably run async functions on exit in a Tauri app, this is how I managed to do it:

First make an exit lock, this is responsible for blocking your Tauri app from exiting, I made a Lazy global variable for this, just to make it nice and simple. You'll need once_cell.

use once_cell::sync::Lazy;

pub static EXIT_LOCKED: Lazy<std::sync::Mutex<bool>> = Lazy::new(|| {
    std::sync::Mutex::new(true)
});

Then, make an on_exit function that takes in an AppHandle. This function runs your cleanup actions and blocks the main thread using a loop until they have been run, no matter sync or async. You'll need the futures crate for this one.

fn on_exit(app: &AppHandle, need_exiting: bool) {
    futures::executor::block_on(async move {
        tauri::async_runtime::spawn(async move {
            // Execute your cleanup actions here
            stop_all_downloads().await;
            *EXIT_LOCKED.lock().unwrap() = false;
        })
    });
    let win = app.get_window("main").unwrap();
    win.hide().unwrap();
    loop {
        if !*EXIT_LOCKED.lock().unwrap() {
            break;
        }
    };
    win.close().unwrap();

    if need_exiting {
        app.exit(0);
    }
}

Intercept window events to run cleanup actions when a window is closed, this implementation is for single-window apps. Make sure your storing your tauri::Builder to a variable or this won't work.

let app = tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![
    // Your handlers
    ])
   .on_window_event(|event| match event {
        WindowEvent::CloseRequested { api, .. } => {
            api.prevent_close();
            on_exit(&event.window().app_handle(), true);
        },
         _ => {}
    })
    .build(tauri::generate_context!())
    .expect("error while running tauri application");

Intercept events from the app lifecycle events(RunEvent) and run your cleanup actions also when Exit is triggered.

app.run(move |app, event| match event {
    RunEvent::Exit => {
        // Since RunEvent:Exit already executes exit, we'll pass false to on_exit so it won't exit twice
        on_exit(app, false);
    },
    _ => {}
});

This is a janky workaround but it works reliably enough, if there's a better way, please share. Hopefully, Tauri will handle pre-exit events reliably in v2 🤞

FabianLars commented 7 months ago

How exactly are you exiting the app? Closing Windows via the red button for example should trigger ExitRequested once all windows are closed (even though that's not really how macos apps should behave). right click -> Quit on the dock item may be the same as https://github.com/tauri-apps/tauri/issues/3084 if it's not working.

lockieluke commented 7 months ago

Using Command+Q or Quit from menu bar

cadomani commented 2 months ago

For those who are looking for a way to reliably run async functions on exit in a Tauri app, this is how I managed to do it:

First make an exit lock, this is responsible for blocking your Tauri app from exiting, I made a Lazy global variable for this, just to make it nice and simple. You'll need once_cell.

use once_cell::sync::Lazy;

pub static EXIT_LOCKED: Lazy<std::sync::Mutex<bool>> = Lazy::new(|| {
    std::sync::Mutex::new(true)
});

Then, make an on_exit function that takes in an AppHandle. This function runs your cleanup actions and blocks the main thread using a loop until they have been run, no matter sync or async. You'll need the futures crate for this one.

fn on_exit(app: &AppHandle, need_exiting: bool) {
    futures::executor::block_on(async move {
        tauri::async_runtime::spawn(async move {
          // Execute your cleanup actions here
            stop_all_downloads().await;
            *EXIT_LOCKED.lock().unwrap() = false;
        })
    });
    let win = app.get_window("main").unwrap();
    win.hide().unwrap();
    loop {
        if !*EXIT_LOCKED.lock().unwrap() {
            break;
        }
    };
    win.close().unwrap();

    if need_exiting {
        app.exit(0);
    }
}

Intercept window events to run cleanup actions when a window is closed, this implementation is for single-window apps. Make sure your storing your tauri::Builder to a variable or this won't work.

let app = tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![
  // Your handlers
    ])
   .on_window_event(|event| match event {
      WindowEvent::CloseRequested { api, .. } => {
          api.prevent_close();
          on_exit(&event.window().app_handle(), true);
      },
           _ => {}
    })
    .build(tauri::generate_context!())
    .expect("error while running tauri application");

Intercept events from the app lifecycle events(RunEvent) and run your cleanup actions also when Exit is triggered.

app.run(move |app, event| match event {
    RunEvent::Exit => {
      // Since RunEvent:Exit already executes exit, we'll pass false to on_exit so it won't exit twice
        on_exit(app, false);
    },
    _ => {}
});

This is a janky workaround but it works reliably enough, if there's a better way, please share. Hopefully, Tauri will handle pre-exit events reliably in v2 🤞

Is this still currently the best way to do this?