diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 16af4660..4673b04b 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -19,6 +19,8 @@ Information about release notes of Coco App is provided here. ### ✈️ Improvements +- refactor: add a timeout to open() #1025 + ## 0.10.0 (2025-12-19) ### ❌ Breaking changes diff --git a/src-tauri/src/common/document.rs b/src-tauri/src/common/document.rs index d4570933..875aa630 100644 --- a/src-tauri/src/common/document.rs +++ b/src-tauri/src/common/document.rs @@ -148,152 +148,171 @@ pub(crate) async fn open( extra_args: Option>, ) -> Result<(), String> { use crate::util::open as homemade_tauri_shell_open; - use std::process::Command; + use tokio::process::Command; + use tokio::time::Duration; + use tokio::time::timeout; - match on_opened { - OnOpened::Application { app_path } => { - log::debug!("open application [{}]", app_path); + let on_opened_clone = on_opened.clone(); + // Put the main logic in an async closure so that we can `time::timeout()` + // it + let async_closure = async move { + match on_opened_clone { + OnOpened::Application { app_path } => { + log::debug!("open application [{}]", app_path); - homemade_tauri_shell_open(tauri_app_handle.clone(), app_path).await? - } - OnOpened::Document { url } => { - log::debug!("open document [{}]", url); + homemade_tauri_shell_open(tauri_app_handle.clone(), app_path).await? + } + OnOpened::Document { url } => { + log::debug!("open document [{}]", url); - homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? - } - #[cfg(target_os = "macos")] - OnOpened::WindowManagementAction { action } => { - log::debug!("perform Window Management action [{:?}]", action); + homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? + } + #[cfg(target_os = "macos")] + OnOpened::WindowManagementAction { action } => { + log::debug!("perform Window Management action [{:?}]", action); - crate::extension::built_in::window_management::perform_action_on_main_thread( - &tauri_app_handle, - action, - )?; - } - OnOpened::Extension(ext_on_opened) => { - // Apply the settings that would affect open behavior - if let Some(settings) = ext_on_opened.settings { - if let Some(should_hide) = settings.hide_before_open { - if should_hide { - crate::hide_coco(tauri_app_handle.clone()).await; + crate::extension::built_in::window_management::perform_action_on_main_thread( + &tauri_app_handle, + action, + )?; + } + OnOpened::Extension(ext_on_opened) => { + // Apply the settings that would affect open behavior + if let Some(settings) = ext_on_opened.settings { + if let Some(should_hide) = settings.hide_before_open { + if should_hide { + crate::hide_coco(tauri_app_handle.clone()).await; + } } } - } - let permission = ext_on_opened.permission; + let permission = ext_on_opened.permission; - match ext_on_opened.ty { - ExtensionOnOpenedType::Command { action } => { - log::debug!("open (execute) command [{:?}]", action); + match ext_on_opened.ty { + ExtensionOnOpenedType::Command { action } => { + log::debug!("open (execute) command [{:?}]", action); - let mut cmd = Command::new(action.exec); - if let Some(args) = action.args { - cmd.args(args); - } - let output = cmd.output().map_err(|e| e.to_string())?; - // Sometimes, we wanna see the result in logs even though it doesn't fail. - log::debug!( - "executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - if !output.status.success() { - log::warn!( - "executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]", + let mut cmd = Command::new(action.exec); + if let Some(args) = action.args { + cmd.args(args); + } + let output = cmd.output().await.map_err(|e| e.to_string())?; + // Sometimes, we wanna see the result in logs even though it doesn't fail. + log::debug!( + "executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]", output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - - return Err(format!( - "Command failed, stderr [{}]", - String::from_utf8_lossy(&output.stderr) - )); - } - } - ExtensionOnOpenedType::Quicklink { - link, - open_with: opt_open_with, - } => { - let url = link.concatenate_url(&extra_args); - - log::debug!("open quicklink [{}] with [{:?}]", url, opt_open_with); - - cfg_if::cfg_if! { - // The `open_with` functionality is only supported on macOS, provided - // by the `open -a` command. - if #[cfg(target_os = "macos")] { - let mut cmd = Command::new("open"); - if let Some(ref open_with) = opt_open_with { - cmd.arg("-a"); - cmd.arg(open_with.as_str()); - } - cmd.arg(&url); - - let output = cmd.output().map_err(|e| format!("failed to spawn [open] due to error [{}]", e))?; - - if !output.status.success() { - return Err(format!( - "failed to open with app {:?}: {}", - opt_open_with, + if !output.status.success() { + log::warn!( + "executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]", + output.status, + String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) - )); - } - } else { - homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? + ); + + return Err(format!( + "Command failed, stderr [{}]", + String::from_utf8_lossy(&output.stderr) + )); } } - } - ExtensionOnOpenedType::View { - name, - icon, - page, - ui, - } => { - let page_path = Utf8Path::new(&page); - let directory = page_path.parent().unwrap_or_else(|| { - panic!("View extension page path should have a parent, i.e., it should be under a directory, but [{}] does not", page); - }); - let mut url = serve_files_in(directory.as_ref()).await; + ExtensionOnOpenedType::Quicklink { + link, + open_with: opt_open_with, + } => { + let url = link.concatenate_url(&extra_args); - /* - * Emit an event to let the frontend code open this extension. - * - * Payload `view_extension_opened` contains the information needed - * to do that. - * - * See "src/pages/main/index.tsx" for more info. - */ - use camino::Utf8Path; - use serde_json::Value as Json; - use serde_json::to_value; + log::debug!("open quicklink [{}] with [{:?}]", url, opt_open_with); - let html_filename = page_path - .file_name() - .unwrap_or_else(|| { - panic!("View extension page path should have a file name, but [{}] does not have one", page); - }).to_string(); - url.push('/'); - url.push_str(&html_filename); + cfg_if::cfg_if! { + // The `open_with` functionality is only supported on macOS, provided + // by the `open -a` command. + if #[cfg(target_os = "macos")] { + let mut cmd = Command::new("open"); + if let Some(ref open_with) = opt_open_with { + cmd.arg("-a"); + cmd.arg(open_with.as_str()); + } + cmd.arg(&url); - let html_file_url = url; - debug!("View extension listening on: {}", html_file_url); - let view_extension_opened: [Json; 5] = [ - Json::String(name), - Json::String(icon), - Json::String(html_file_url), - to_value(permission).unwrap(), - to_value(ui).unwrap(), - ]; - tauri_app_handle - .emit("open_view_extension", view_extension_opened) - .unwrap(); + let output = cmd.output().await.map_err(|e| format!("failed to spawn [open] due to error [{}]", e))?; + + if !output.status.success() { + return Err(format!( + "failed to open with app {:?}: {}", + opt_open_with, + String::from_utf8_lossy(&output.stderr) + )); + } + } else { + homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? + } + } + } + ExtensionOnOpenedType::View { + name, + icon, + page, + ui, + } => { + let page_path = Utf8Path::new(&page); + let directory = page_path.parent().unwrap_or_else(|| { + panic!("View extension page path should have a parent, i.e., it should be under a directory, but [{}] does not", page); + }); + let mut url = serve_files_in(directory.as_ref()).await; + + /* + * Emit an event to let the frontend code open this extension. + * + * Payload `view_extension_opened` contains the information needed + * to do that. + * + * See "src/pages/main/index.tsx" for more info. + */ + use camino::Utf8Path; + use serde_json::Value as Json; + use serde_json::to_value; + + let html_filename = page_path + .file_name() + .unwrap_or_else(|| { + panic!("View extension page path should have a file name, but [{}] does not have one", page); + }).to_string(); + url.push('/'); + url.push_str(&html_filename); + + let html_file_url = url; + debug!("View extension listening on: {}", html_file_url); + let view_extension_opened: [Json; 5] = [ + Json::String(name), + Json::String(icon), + Json::String(html_file_url), + to_value(permission).unwrap(), + to_value(ui).unwrap(), + ]; + tauri_app_handle + .emit("open_view_extension", view_extension_opened) + .unwrap(); + } } } } - } - Ok(()) + Ok(()) + }; + + match timeout(Duration::from_millis(500), async_closure).await { + Ok(res) => res, + Err(_timed_out) => { + log::warn!("executing open(on_opened: [{:?}]) timed out", on_opened); + + Err(format!( + "executing open(on_opened: {:?}) timed out", + on_opened + )) + } + } } #[derive(Debug, Clone, Serialize, Deserialize, Default)]