Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
82efc67
Add handler for web content process termination on macOS and iOS
JeffTsang Oct 18, 2025
f39aeb6
Remove redundant closure
JeffTsang Oct 18, 2025
2954c83
Add documentation
JeffTsang Oct 18, 2025
04c64dc
Update .changes
JeffTsang Nov 23, 2025
c848d4b
Merge branch 'dev' into fix/web-content-process-termination
JeffTsang Nov 23, 2025
13bb380
Update .changes
JeffTsang Nov 23, 2025
160d7e2
Merge branch 'dev' into fix/web-content-process-termination
JeffTsang Jan 21, 2026
12d4894
Merge branch 'dev' into fix/web-content-process-termination
JeffTsang Mar 11, 2026
fb41639
Add default handler for web content process termination on iOS
JeffTsang Mar 13, 2026
05daf95
Merge branch 'dev' into fix/web-content-process-termination
JeffTsang Mar 13, 2026
9d087bd
Use navigate instead of reload when recovering from web content proce…
JeffTsang Mar 13, 2026
477e710
Merge branch 'fix/web-content-process-termination' of https://github.…
JeffTsang Mar 13, 2026
b296654
Merge branch 'dev' into fix/web-content-process-termination
JeffTsang Mar 13, 2026
4988d6e
Fix handler in webview window
JeffTsang Mar 17, 2026
c5a9f9d
Reload by default on iOS
JeffTsang Mar 17, 2026
3791c6c
Update .changes
JeffTsang Mar 18, 2026
1bda592
Fix code format
JeffTsang Mar 18, 2026
3e7e738
Add default handler to macOS
JeffTsang Mar 19, 2026
f184c59
Move function to tauri::Builder
JeffTsang Apr 1, 2026
578b7b3
Add comment
JeffTsang Apr 1, 2026
31aef4d
Allow fallback handler
JeffTsang Apr 1, 2026
69d4483
Add os check
JeffTsang Apr 1, 2026
f28ede5
Add os checks to use declarations
JeffTsang Apr 1, 2026
cfa6a38
Fix tests
JeffTsang Apr 1, 2026
e491659
Update comment with platform support
JeffTsang Apr 1, 2026
129f0fb
Simplify default handler logic
JeffTsang Apr 2, 2026
342e25b
Merge branch 'fix/web-content-process-termination' of https://github.…
JeffTsang Apr 2, 2026
23250c0
Fix default handler
JeffTsang Apr 2, 2026
b6fb7ec
Remove extra clone
JeffTsang Apr 3, 2026
7160b46
Fix default handler
JeffTsang Apr 3, 2026
e8991c2
Add logging
JeffTsang Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changes/web-content-process-termination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"tauri": "minor:feat"
"tauri-runtime": "minor:feat"
"tauri-runtime-wry": "minor:feat"
---

Add handler for web content process termination on macOS and iOS.
29 changes: 29 additions & 0 deletions crates/tauri-runtime-wry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4929,6 +4929,35 @@ You may have it installed on another user account, but it is not available for t

webview_builder =
webview_builder.with_allow_link_preview(webview_attributes.allow_link_preview);

if let Some(on_web_content_process_terminate_handler) =
pending.on_web_content_process_terminate_handler
{
webview_builder = webview_builder
.with_on_web_content_process_terminate_handler(on_web_content_process_terminate_handler);
} else {
log::debug!("web content process terminated");
let context_ = context.clone();
let window_id_ = window_id.clone();
webview_builder = webview_builder.with_on_web_content_process_terminate_handler(move || {
if let Ok(windows) = &context_.main_thread.windows.0.try_borrow() {
if let Some(window) = windows.get(&*window_id_.lock().unwrap()) {
if let Some(webview) = window.webviews.iter().find(|w| w.id == id) {
match webview.reload() {
Ok(_) => log::debug!("webview reloaded"),
Err(e) => log::error!("failed to reload webview: {}", e),
}
} else {
log::error!("failed to find webview")
}
} else {
log::error!("failed to get window")
}
} else {
log::error!("failed to borrow windows")
}
});
}
}

#[cfg(target_os = "ios")]
Expand Down
8 changes: 8 additions & 0 deletions crates/tauri-runtime/src/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type DocumentTitleChangedHandler = dyn Fn(String) + Send + 'static;

type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync;

#[cfg(any(target_os = "macos", target_os = "ios"))]
type OnWebContentProcessTerminateHandler = dyn Fn() + Send;

#[cfg(target_os = "ios")]
type InputAccessoryViewBuilderFn = dyn Fn(&objc2_ui_kit::UIView) -> Option<objc2::rc::Retained<objc2_ui_kit::UIView>>
+ Send
Expand Down Expand Up @@ -225,6 +228,9 @@ pub struct PendingWebview<T: UserEvent, R: Runtime<T>> {
pub on_page_load_handler: Option<Box<OnPageLoadHandler>>,

pub download_handler: Option<Arc<DownloadHandler>>,

#[cfg(any(target_os = "macos", target_os = "ios"))]
pub on_web_content_process_terminate_handler: Option<Box<OnWebContentProcessTerminateHandler>>,
}

impl<T: UserEvent, R: Runtime<T>> PendingWebview<T, R> {
Expand All @@ -251,6 +257,8 @@ impl<T: UserEvent, R: Runtime<T>> PendingWebview<T, R> {
web_resource_request_handler: None,
on_page_load_handler: None,
download_handler: None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate_handler: None,
})
}
}
Expand Down
28 changes: 28 additions & 0 deletions crates/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ pub type SetupHook<R> =
Box<dyn FnOnce(&mut App<R>) -> std::result::Result<(), Box<dyn std::error::Error>> + Send>;
/// A closure that is run every time a page starts or finishes loading.
pub type OnPageLoad<R> = dyn Fn(&Webview<R>, &PageLoadPayload<'_>) + Send + Sync + 'static;
/// A closure that is run when the web content process terminates.
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub type OnWebContentProcessTerminate<R> = dyn Fn(&Webview<R>) + Send + Sync + 'static;
pub type ChannelInterceptor<R> =
Box<dyn Fn(&Webview<R>, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static>;

Expand Down Expand Up @@ -1390,6 +1393,10 @@ pub struct Builder<R: Runtime> {
/// Page load hook.
on_page_load: Option<Arc<OnPageLoad<R>>>,

/// Web content process termination hook.
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminate<R>>>,

/// All passed plugins
plugins: PluginStore<R>,

Expand Down Expand Up @@ -1476,6 +1483,8 @@ impl<R: Runtime> Builder<R> {
.into_string(),
channel_interceptor: None,
on_page_load: None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate: None,
plugins: PluginStore::default(),
uri_scheme_protocols: Default::default(),
state: StateManager::new(),
Expand Down Expand Up @@ -1656,6 +1665,23 @@ tauri::Builder::default()
self
}

/// Defines the web content process termination hook.
Comment thread
JeffTsang marked this conversation as resolved.
///
/// ## Platform-specific
///
/// - **Linux / Windows / Android:** Unsupported.
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[must_use]
pub fn on_web_content_process_terminate<F>(mut self, on_web_content_process_terminate: F) -> Self
where
F: Fn(&Webview<R>) + Send + Sync + 'static,
{
self
.on_web_content_process_terminate
.replace(Arc::new(on_web_content_process_terminate));
self
}

/// Adds a Tauri application plugin.
///
/// A plugin is created using the [`crate::plugin::Builder`] struct.Check its documentation for more information.
Expand Down Expand Up @@ -2104,6 +2130,8 @@ tauri::Builder::default()
self.plugins,
self.invoke_handler,
self.on_page_load,
#[cfg(any(target_os = "macos", target_os = "ios"))]
self.on_web_content_process_terminate,
self.uri_scheme_protocols,
self.state,
#[cfg(desktop)]
Expand Down
4 changes: 4 additions & 0 deletions crates/tauri/src/ipc/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,8 @@ mod tests {
PluginStore::default(),
Box::new(|_| false),
None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
None,
Default::default(),
StateManager::new(),
Default::default(),
Expand Down Expand Up @@ -687,6 +689,8 @@ mod tests {
PluginStore::default(),
Box::new(|_| false),
None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
None,
Default::default(),
StateManager::new(),
Default::default(),
Expand Down
10 changes: 10 additions & 0 deletions crates/tauri/src/manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ use crate::{
Assets, Context, DebugAppIcon, EventName, Pattern, Runtime, StateManager, Webview, Window,
};

#[cfg(any(target_os = "macos", target_os = "ios"))]
use crate::app::OnWebContentProcessTerminate;

#[cfg(desktop)]
mod menu;
#[cfg(all(desktop, feature = "tray-icon"))]
Expand Down Expand Up @@ -251,6 +254,9 @@ impl<R: Runtime> AppManager<R> {
plugins: PluginStore<R>,
invoke_handler: Box<InvokeHandler<R>>,
on_page_load: Option<Arc<OnPageLoad<R>>>,
#[cfg(any(target_os = "macos", target_os = "ios"))] on_web_content_process_terminate: Option<
Arc<OnWebContentProcessTerminate<R>>,
>,
uri_scheme_protocols: HashMap<String, Arc<webview::UriSchemeProtocol<R>>>,
state: StateManager,
#[cfg(desktop)] menu_event_listener: Vec<crate::app::GlobalMenuEventListener<AppHandle<R>>>,
Expand Down Expand Up @@ -284,6 +290,8 @@ impl<R: Runtime> AppManager<R> {
webviews: Mutex::default(),
invoke_handler,
on_page_load,
#[cfg(any(target_os = "macos", target_os = "ios"))]
on_web_content_process_terminate,
uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
event_listeners: Arc::new(webview_event_listeners),
invoke_initialization_script,
Expand Down Expand Up @@ -762,6 +770,8 @@ mod test {
PluginStore::default(),
Box::new(|_| false),
None,
#[cfg(any(target_os = "macos", target_os = "ios"))]
None,
Default::default(),
StateManager::new(),
Default::default(),
Expand Down
29 changes: 29 additions & 0 deletions crates/tauri/src/manager/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ use crate::{
EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window,
};

#[cfg(any(target_os = "macos", target_os = "ios"))]
use crate::app::OnWebContentProcessTerminate;

use super::{
window::{DragDropPayload, DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT},
{AppManager, EmitPayload},
Expand Down Expand Up @@ -70,6 +73,9 @@ pub struct WebviewManager<R: Runtime> {
pub invoke_handler: Box<InvokeHandler<R>>,
/// The page load hook, invoked when the webview performs a navigation.
pub on_page_load: Option<Arc<OnPageLoad<R>>>,
/// The web content process termination hook.
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub on_web_content_process_terminate: Option<Arc<OnWebContentProcessTerminate<R>>>,
/// The webview protocols available to all webviews.
pub uri_scheme_protocols: Mutex<HashMap<String, Arc<UriSchemeProtocol<R>>>>,
/// Webview event listeners to all webviews.
Expand Down Expand Up @@ -304,6 +310,29 @@ impl<R: Runtime> WebviewManager<R> {
}
}));

#[cfg(any(target_os = "macos", target_os = "ios"))]
if pending.on_web_content_process_terminate_handler.is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fallback resolves the global Builder-level handler here, but falls through to the default in tauri-runtime-wry when nothing is configured. This splits handler resolution across two crates.

Could this be consolidated so the full priority chain is resolved in one place? Something like:

// in into_pending_webview:                                                                                                                            
let handler = per_webview_handler.or_else(|| global_handler.clone());                                                                                                               
                                                                                                                                                           
pending.handler = match handler {
    Some(h) => wrap_custom_handler(manager, label, h),                                                                                                 
    None => build_default_reload_handler(manager, label),                                                                                           
};

This way tauri-runtime-wry just passes the handler through to wry without any default logic and the Tauri layer owns all the decisions.

let app_manager_ = manager.manager_owned();
if app_manager_
.webview
.on_web_content_process_terminate
.is_some()
{
let label_ = pending.label.clone();
pending
.on_web_content_process_terminate_handler
.replace(Box::new(move || {
if let Some(w) = app_manager_.get_webview(&label_) {
if let Some(on_web_content_process_terminate) =
&app_manager_.webview.on_web_content_process_terminate
{
on_web_content_process_terminate(&w);
}
}
}));
}
}

#[cfg(feature = "protocol-asset")]
if !registered_scheme_protocols.contains(&"asset".into()) {
let asset_scope = app_manager
Expand Down
Loading