1 Commits

Author SHA1 Message Date
Steve Lau
1759e0e56e refactor: add a timeout to open()
Adds a 500ms timeout to open(), to prevent the open action that hangs
from hanging indefinitely.
2025-12-22 12:42:45 +08:00
2 changed files with 145 additions and 124 deletions

View File

@@ -17,6 +17,8 @@ Information about release notes of Coco App is provided here.
### ✈️ Improvements ### ✈️ Improvements
- refactor: add a timeout to open() #1025
## 0.10.0 (2025-12-19) ## 0.10.0 (2025-12-19)
### ❌ Breaking changes ### ❌ Breaking changes

View File

@@ -148,152 +148,171 @@ pub(crate) async fn open(
extra_args: Option<HashMap<String, Json>>, extra_args: Option<HashMap<String, Json>>,
) -> Result<(), String> { ) -> Result<(), String> {
use crate::util::open as homemade_tauri_shell_open; 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 { let on_opened_clone = on_opened.clone();
OnOpened::Application { app_path } => { // Put the main logic in an async closure so that we can `time::timeout()`
log::debug!("open application [{}]", app_path); // 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? homemade_tauri_shell_open(tauri_app_handle.clone(), app_path).await?
} }
OnOpened::Document { url } => { OnOpened::Document { url } => {
log::debug!("open document [{}]", url); log::debug!("open document [{}]", url);
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
OnOpened::WindowManagementAction { action } => { OnOpened::WindowManagementAction { action } => {
log::debug!("perform Window Management action [{:?}]", action); log::debug!("perform Window Management action [{:?}]", action);
crate::extension::built_in::window_management::perform_action_on_main_thread( crate::extension::built_in::window_management::perform_action_on_main_thread(
&tauri_app_handle, &tauri_app_handle,
action, action,
)?; )?;
} }
OnOpened::Extension(ext_on_opened) => { OnOpened::Extension(ext_on_opened) => {
// Apply the settings that would affect open behavior // Apply the settings that would affect open behavior
if let Some(settings) = ext_on_opened.settings { if let Some(settings) = ext_on_opened.settings {
if let Some(should_hide) = settings.hide_before_open { if let Some(should_hide) = settings.hide_before_open {
if should_hide { if should_hide {
crate::hide_coco(tauri_app_handle.clone()).await; 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 { match ext_on_opened.ty {
ExtensionOnOpenedType::Command { action } => { ExtensionOnOpenedType::Command { action } => {
log::debug!("open (execute) command [{:?}]", action); log::debug!("open (execute) command [{:?}]", action);
let mut cmd = Command::new(action.exec); let mut cmd = Command::new(action.exec);
if let Some(args) = action.args { if let Some(args) = action.args {
cmd.args(args); cmd.args(args);
} }
let output = cmd.output().map_err(|e| e.to_string())?; 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. // Sometimes, we wanna see the result in logs even though it doesn't fail.
log::debug!( log::debug!(
"executing open(Command) result, exit code: [{}], stdout: [{}], stderr: [{}]", "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: [{}]",
output.status, output.status,
String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
); );
if !output.status.success() {
return Err(format!( log::warn!(
"Command failed, stderr [{}]", "executing open(Command) failed, exit code: [{}], stdout: [{}], stderr: [{}]",
String::from_utf8_lossy(&output.stderr) output.status,
)); String::from_utf8_lossy(&output.stdout),
}
}
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,
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
)); );
}
} else { return Err(format!(
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await? "Command failed, stderr [{}]",
String::from_utf8_lossy(&output.stderr)
));
} }
} }
} ExtensionOnOpenedType::Quicklink {
ExtensionOnOpenedType::View { link,
name, open_with: opt_open_with,
icon, } => {
page, let url = link.concatenate_url(&extra_args);
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;
/* log::debug!("open quicklink [{}] with [{:?}]", url, opt_open_with);
* 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 cfg_if::cfg_if! {
.file_name() // The `open_with` functionality is only supported on macOS, provided
.unwrap_or_else(|| { // by the `open -a` command.
panic!("View extension page path should have a file name, but [{}] does not have one", page); if #[cfg(target_os = "macos")] {
}).to_string(); let mut cmd = Command::new("open");
url.push('/'); if let Some(ref open_with) = opt_open_with {
url.push_str(&html_filename); cmd.arg("-a");
cmd.arg(open_with.as_str());
}
cmd.arg(&url);
let html_file_url = url; let output = cmd.output().await.map_err(|e| format!("failed to spawn [open] due to error [{}]", e))?;
debug!("View extension listening on: {}", html_file_url);
let view_extension_opened: [Json; 5] = [ if !output.status.success() {
Json::String(name), return Err(format!(
Json::String(icon), "failed to open with app {:?}: {}",
Json::String(html_file_url), opt_open_with,
to_value(permission).unwrap(), String::from_utf8_lossy(&output.stderr)
to_value(ui).unwrap(), ));
]; }
tauri_app_handle } else {
.emit("open_view_extension", view_extension_opened) homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
.unwrap(); }
}
}
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)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]