Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce App::run_return #12668

Open
wants to merge 36 commits into
base: dev
Choose a base branch
from

Conversation

thomaseizinger
Copy link

The current App::run_iteration function is buggy because it busy-loops. To address the use-case of cleaning up resources in the host program, we introduce App::run_return which builds on top of tao's Eventloop::run_return.

Related: #8631.

@thomaseizinger thomaseizinger requested a review from a team as a code owner February 10, 2025 05:50
Copy link
Contributor

github-actions bot commented Feb 10, 2025

Package Changes Through ba70a4f

There are 9 changes which include tauri-cli with minor, tauri-runtime with minor, tauri-runtime-wry with minor, tauri-utils with minor, tauri with minor, @tauri-apps/api with minor, @tauri-apps/cli with minor, tauri-bundler with patch, tauri-macos-sign with minor

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
@tauri-apps/api 2.2.0 2.3.0
tauri-utils 2.1.1 2.2.0
tauri-macos-sign 2.0.1 2.1.0
tauri-bundler 2.2.3 2.2.4
tauri-runtime 2.3.0 2.4.0
tauri-runtime-wry 2.3.0 2.4.0
tauri-codegen 2.0.4 2.0.5
tauri-macros 2.0.4 2.0.5
tauri-plugin 2.0.4 2.0.5
tauri-build 2.0.5 2.0.6
tauri 2.2.5 2.3.0
@tauri-apps/cli 2.2.7 2.3.0
tauri-cli 2.2.7 2.3.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

@thomaseizinger
Copy link
Author

Do we want to deprecate run_iteration as part of this?

@thomaseizinger thomaseizinger force-pushed the feat/introduce-run-return branch from 5b41a90 to e48e8c3 Compare February 11, 2025 07:02
@@ -2840,13 +2843,13 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
let active_tracing_spans = self.context.main_thread.active_tracing_spans.clone();
let proxy = self.event_loop.create_proxy();

self.event_loop.run(move |event, event_loop, control_flow| {
self.event_loop.run_return(move |e, event_loop, cf| {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed these variables here so that this stays on one line. That way, the git diff shows nicely that run_return is effectively what run used to be.

pub fn run_return<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
mut self,
mut callback: F,
) -> std::result::Result<i32, Box<dyn std::error::Error>> {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just used the same error as from the setup callback. It is missing Send + Sync + 'static bounds unfortunately but that is already the case and can't be changed now I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried it locally, and crate::Result<i32> works for me.

@FabianLars
Copy link
Member

Do we want to deprecate run_iteration as part of this?

imo yes

@FabianLars
Copy link
Member

iirc run_return also works on android (though probably good to test that first), only iOS should be a problem. I'm wondering whether we should really go for the cfg flag you used or just try to do what winit for example does and just document that it doesn't return ever on iOS (using tao's run instead of run_return for iOS internally). didn't look too much into the code yet so idk how feasible that is.

@thomaseizinger
Copy link
Author

iirc run_return also works on android (though probably good to test that first), only iOS should be a problem. I'm wondering whether we should really go for the cfg flag you used or just try to do what winit for example does and just document that it doesn't return ever on iOS (using tao's run instead of run_return for iOS internally). didn't look too much into the code yet so idk how feasible that is.

Removing the cfg is a semver-compatible change so we can always do that later? I'd prefer an incremental approach if possible! :)

run_iteration is only exposed on desktop hence why I copied that. (To me, adding run_return is a bugfix for run_iteration).

Deciding on what the mobile story is here seems like a different problem to me that I'd rather not tackle, also because I don't know the internals of tao and Tauri well enough :)

@thomaseizinger
Copy link
Author

@FabianLars

  • App::run_iteration has been deprecated.
  • I had to re-introduce an actual implementation of Wry::run because run_return in tao is not available on iOS and Android. To deduplicate the code, I extracted a factory fn for the event handler.

@WSH032
Copy link
Contributor

WSH032 commented Feb 17, 2025

What can be done to push this forward? I really want to see this feature in tauri 2.3 so that I can finalize the Python interpreter in pytauri (the current run implementation kills 🐍, and we can't clean up resources at all 😱).

@thomaseizinger
Copy link
Author

What can be done to push this forward? I really want to see this feature in tauri 2.3 so that I can finalize the Python interpreter in pytauri (the current run implementation kills 🐍, and we can't clean up resources at all 😱).

Unfortunately, the repository settings prevent me from effectively iterating on this because CI has to be approved constantly and running cargo check locally doesn't work because some native dependencies aren't satisfied and I couldn't be bothered yet to write a shell.nix that sets up fully sets up all dependencies.

@thomaseizinger
Copy link
Author

and I couldn't be bothered yet to write a shell.nix that sets up fully sets up all dependencies.

Wasn't as much work as I thought it would be (wuhu AI!) so iterating locally now.

@thomaseizinger
Copy link
Author

Should be compiling now. Some of the tests also fail on dev for me so I can't produce a clean cargo test locally.

@thomaseizinger
Copy link
Author

Yey all green!

pub fn run_return<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
mut self,
mut callback: F,
) -> std::result::Result<i32, Box<dyn std::error::Error>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried it locally, and crate::Result<i32> works for me.

@thomaseizinger
Copy link
Author

@FabianLars @WSH032 Ready for another review round. I've reverted the change of returning Result from run_return and I am now copying exactly what run does: Calling setup when RuntimeRunEvent::Ready is emitted. I don't know the details of the lifecycle but sticking to what run does seems more correct here.

Refactoring this can be left to a future PR.

Copy link
Contributor

@WSH032 WSH032 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reverted the change of returning Result from run_return

I have no opinion on this. I just think that if run_return does return a Result, I would prefer it to be tauri::Result<T> (tauri::Error::Setup) rather than std::result::Result<T, Box<dyn Error>>.


Regarding the lifecycle, we should look at @FabianLars's opinion.

@thomaseizinger
Copy link
Author

I've reverted the change of returning Result from run_return

I have no opinion on this. I just think that if run_return does return a Result, I would prefer it to be tauri::Result<T> (tauri::Error::Setup) rather than std::result::Result<T, Box<dyn Error>>.

I agree. Unfortunately, returning a Result is not compatible with running setup inside the event-loop unless we refactor callback to be able to fail and abort the event-loop.

@thomaseizinger
Copy link
Author

unless we refactor callback to be able to fail and abort the event-loop.

I think this is possible but haven't looked at the code of all platforms. It is more involved than what I'd like to do at the moment.

@FabianLars
Copy link
Member

I don't know the details of the lifecycle but sticking to what run does seems more correct here.

It is more correct* and i'd consider it a bug in run_iteration that it didn't do that as well.
* "correct" is debatable but there are a few things (mostly on macos) depending on that.

I agree. Unfortunately, returning a Result is not compatible with running setup inside the event-loop unless we refactor callback to be able to fail and abort the event-loop.

(take this with a grain of salt. i'm on my phone so basically blind) What has the callback to do with that? As far as i can see you'd just return from run_return where rn there's a panic completely ignoring the callback. The event loop should get stopped simply by dropping it (which we do by returning)

I don't really care about the error type we return, i'd just like to get rid of the panic here tbh. If this is not possible, then the function's documentation should mention the panic just in case.

@thomaseizinger thomaseizinger force-pushed the feat/introduce-run-return branch from 3db3088 to d521c2e Compare February 25, 2025 22:52
@thomaseizinger
Copy link
Author

I don't really care about the error type we return, i'd just like to get rid of the panic here tbh. If this is not possible, then the function's documentation should mention the panic just in case.

I opened #12815 because I think there are a few more things to discuss here in trying to get rid of that panic.

@thomaseizinger
Copy link
Author

@FabianLars I rebased on dev to fix the failing workflow. Let me know if there is anything else we need to ship this.

@FabianLars
Copy link
Member

i just deleted the draft i wrote 2 hours ago that i forgot to send 👏

tldr: looks good, any improvements in follow up prs will probably result in breaking changes but that's fine 🤷

@thomaseizinger
Copy link
Author

tldr: looks good, any improvements in follow up prs will probably result in breaking changes but that's fine 🤷

I thought about this. I think we could still make a backwards-compatible change by returning an exit-code of -1 in case there is an error in the setup and just log it on ERROR or WARN level.

@thomaseizinger
Copy link
Author

I've been testing this integration in our application and it works quite well: firezone/firezone#7959.

I think it might be worth showing this in a more complex example. The basic idea is:

  • Use the app builder to build your app instance
  • Grab an AppHandle, pass it to whatever your controller thingy is, i.e. your "app" outside of Tauri
  • Call AppHandle::exit whenever you want to shutdown. In our case, we do this when the controller task exits
  • Block the main-thread with run_return
  • Wait for the controller task to finish after run_return
  • Return from main

In our case, we don't actually use the return value from run_return but pass a Result back from the controller task and let that determine if we exited successfully or not.

@FabianLars
Copy link
Member

just a fyi, i'll have to keep this pr open/unmerged for a few more days. I wanted to include it in 2.3.0 but that was just released (i only was told once ci was running 🤷 ) and we'll need to delay minor changes a bit to be able to quickly release new patches if something went wrong in 2.3.0

@FabianLars FabianLars added this to the 2.4 milestone Feb 26, 2025
@thomaseizinger
Copy link
Author

just a fyi, i'll have to keep this pr open/unmerged for a few more days. I wanted to include it in 2.3.0 but that was just released (i only was told once ci was running 🤷 ) and we'll need to delay minor changes a bit to be able to quickly release new patches if something went wrong in 2.3.0

Okay no worries :)

Thanks for the reviews! Looking forward to it being released!

@cuchaz
Copy link

cuchaz commented Mar 10, 2025

Just wanted to chime in and say this PR fixes the busy main loop problem for me in Linux. Thanks! I'm looking forward to seeing it (maybe) in the 2.4 release.

Though there's a side-effect of not being able to use features = ["config-toml"] when pointing the cargo dependency to a git commit. Weird.

@cuchaz
Copy link

cuchaz commented Mar 10, 2025

Ok, after using the new run_return function for a bit, I'm starting to find some cracks. It looks like when the function exits, Tauri hasn't quite finished cleanup yet? The windows are still visible, for instance. The windows don't disappear from my desktop (Linux, btw) until the process exits. Ideally, after run_return exits, the windows would be gone.

After trying a bunch of things to poke Tauri into closing the windows before exiting the event loop, I may have found why the cleanup isn't happening. My best guess is that cleaning up the windows requires sending events, and if we exit the event loop too early, those events never get processed?

Here's some code I've figured out that successfully manages to close the windows before the event loop exits, but it requires giving the event loop some extra time to do ... something before allowing it to stop.

fn try_main(args: Args) -> Result<Code,Error> {

  // start the tokio runtime
  tokio::runtime::Builder::new_multi_thread()
    .enable_time()
    .build()
    .wrap_err()
    .context(ctx!("Failed to start tokio runtime"))?
    .block_on(async {

      tauri::async_runtime::set(tokio::runtime::Handle::current());

      let state = AppStateContainer::new(args);

      debug!("Starting tauri ...");

      // run the tauri app
      let exit = tauri::Builder::default()
        .manage(state.clone())
        .invoke_handler(tauri::generate_handler![
          status
        ])
        .setup(|app| setup(app).map_err(Into::into))
        .build(tauri::generate_context!())
        .wrap_err()
        .context(ctx!("Failed to build tauri app"))?
        .run_return({
          let mut exiting = false;
          move |app, event| match event {

            tauri::RunEvent::ExitRequested { api, .. } => {
              
              // if we've already been here before, just let the event loop exit
              if exiting {
                return;
              }
              exiting = true;

              // otherwise, prevent the event loop from exiting
              api.prevent_exit();
              
              // then hide all the windows
              for win in app.webview_windows().values() {
                match win.hide() {
                  Ok(()) => info!("Hid window {}!", win.label()),
                  Err(e) => warn!("Failed to hide window {}: {}", win.label(), e)
                }
              }
              
              // and set a task to call exit again later,
              // to give the event loop time to actually hide the windows
              tokio::task::spawn({
                let app = app.clone();
                async move {
                  debug!("Waiting for event loop to hide things ...");
                  tokio::time::sleep(std::time::Duration::from_millis(200))
                    .await;
                  debug!("Wait complete: burn it down!");
                  app.exit(0)
                  // DANGER WILL ROBINSON: if app.exit() fails internally, it will call std::process::exit() !!!
                }
              });
            }
            
            _ => () // ignore other events
          }
        });

      debug!("Tauri finished, cleaning up state ...");

      // clean up the state
      state.close()
        .await?;

      debug!("Finished");

      Ok(Code::new(exit))
    })
}

This code seems to function as a workaround, but it's not ideal, because app.exit looks like it could actually call std::process::exit. Exiting early would skip any post-tauri cleanup and defeat the whole purpose of run_return.

I'm not sure how to use this info to propose a change to the way Tauri works, but the solution here seems to be allowing the event loop to process the event queue before actually exiting. Maybe that can help someone who knows Tauri better to come up with a fix?

@thomaseizinger
Copy link
Author

thomaseizinger commented Mar 10, 2025

Ideally, after run_return exits, the windows would be gone.

In my usecase, I am calling exit when the last window got closed so that might be the issue? I am not sure we can solve this in Tauri? Once you call exit, the eventloop will stop. It sounds like you are triggering "exit" too early?

@thomaseizinger
Copy link
Author

I guess we could extend the internal "cleanup before exit" handler perhaps?

@cuchaz
Copy link

cuchaz commented Mar 10, 2025

In my case, I'm triggering "exit" by clicking the close icon on the last window. I'm not sure how to make that happen earlier or later. But after the window closes, and before the process can exit, I need to make sure other resources get cleaned up. Like the state variable in my example above.

@cuchaz
Copy link

cuchaz commented Mar 11, 2025

I tried poking around the Tauri code a bit to see if I could find a solution. Maybe I have something?

This re-implementation of Runtime::run_return for Wry seems to get all the way through cleanup before exiting the fn. Running one more iteration of the event loop seems to do the trick.

#[cfg(desktop)]
fn run_return<F: FnMut(RunEvent<T>) + 'static>(mut self, callback: F) -> i32 {
  use tao::platform::run_return::EventLoopExtRunReturn;

  let event_handler = make_event_handler(&self, callback);

  let exit = self.event_loop.run_return(event_handler);

  // after the event loop is ready to exit, run one more iteration to dispatch any cleanup messages
  // otherwise, the windows will become orphaned from the Rust code and remain visible until the process exits
  self.run_iteration(|_| {});

  exit
}

This seems to work in my case. Is this an ok thing to do in general? If not, I don't really know why it works, but maybe someone else knows a better way to get the cleanup to finish out.

@thomaseizinger
Copy link
Author

This re-implementation of Runtime::run_return for Wry seems to get all the way through cleanup before exiting the fn. Running one more iteration of the event loop seems to do the trick.

We want to deprecate run_iteration actually because it IMO misuses the underlying run_return abstraction.

I am happy for other people to jump in here, I am not gonna get to work on this for a few days.

@cuchaz
Copy link

cuchaz commented Mar 11, 2025

I finally found a way to do async state cleanup while Tauri's event loop is still running, so it's no longer necessary to rely on Tauri's internal cleanup to finish successfully. I'll share the workaround here for posterity, in case it might help anyone else:

fn main() {

  // start the tokio runtime
  tokio::runtime::Builder::new_multi_thread()
    .build()
    .unwrap()
    .block_on(async {

      tauri::async_runtime::set(tokio::runtime::Handle::current());

      // create our state
      let state = MyState::new();
      
      // run the tauri app
      tauri::Builder::default()
        .manage(state)
        .invoke_handler(...)
        .setup(...)
        .build(...)
        .unwrap()
        .run({
          let mut exiting = false;
          move |app, event| match event {

            tauri::RunEvent::ExitRequested { api, .. } => {

              // if we've already been here before, just let the event loop exit
              if exiting {
                return;
              }
              exiting = true;
              
              // don't exit just yet: still need to cleanup
              api.prevent_exit();

              // start a task to cleanup the state, since we need async
              tokio::task::spawn({
                let app = app.clone();
                async move {
                  
                  // get a reference to our state from Tauri
                  let state = app.state::<MyState>()
                    .deref()
                    .clone();
                  
                  // clean up the state
                  state.close()
                    .await
                    .unwrap();

                  // ask tauri event loop to exit again
                  app.exit(0)
                }
              });
            }
            
            _ => ()
          }
        });
    })
}

Error handling and Tauri configuration were omitted, for brevity. This pattern should work on the release version of Tauri too.

@thomaseizinger
Copy link
Author

In our case, the equivalent of your close returns an error for us that I'd like to use to inform the exit code. This is possible but duplicates all error handling because the app can also fail prior to Tauri starting and it would be nice to unify all of that into one fallible function and hence the need for run_return.

I am almost certain we can achieve the same thing with some modifications to the internals of event_loop.run_return. It should be possible to run one or a few more ticks of the event-loop after receiving the exit event.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants