mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-10 14:52:49 +02:00
Detect and surface plugin runtime crashes (#502)
This commit is contained in:
@@ -28,7 +28,7 @@ use yaak_plugins::api::{
|
|||||||
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
||||||
search_plugins,
|
search_plugins,
|
||||||
};
|
};
|
||||||
use yaak_plugins::events::PluginContext;
|
use yaak_plugins::events::{Color, PluginContext, ShowToastRequest};
|
||||||
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::plugin_meta::get_plugin_meta;
|
use yaak_plugins::plugin_meta::get_plugin_meta;
|
||||||
@@ -315,6 +315,30 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to start plugin runtime");
|
.expect("Failed to start plugin runtime");
|
||||||
|
|
||||||
|
// Surface unexpected runtime crashes to the user
|
||||||
|
let mut crash_rx = manager.runtime_crash_rx();
|
||||||
|
let app_handle_crash = app_handle_clone.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if crash_rx.wait_for(|status| status.is_some()).await.is_ok() {
|
||||||
|
let status = crash_rx.borrow().clone().unwrap_or_default();
|
||||||
|
// The crash may happen during startup, before any window or
|
||||||
|
// frontend listener exists — wait so the toast isn't lost
|
||||||
|
while app_handle_crash.webview_windows().is_empty() {
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||||
|
let _ = app_handle_crash.emit(
|
||||||
|
"show_toast",
|
||||||
|
ShowToastRequest {
|
||||||
|
message: format!("Plugin runtime crashed ({status})"),
|
||||||
|
color: Some(Color::Danger),
|
||||||
|
icon: None,
|
||||||
|
timeout: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app_handle_clone.manage(manager);
|
app_handle_clone.manage(manager);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ pub struct PluginManager {
|
|||||||
dev_mode: bool,
|
dev_mode: bool,
|
||||||
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
|
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
|
||||||
init_errors: Arc<Mutex<Vec<(String, String)>>>,
|
init_errors: Arc<Mutex<Vec<(String, String)>>>,
|
||||||
|
/// Set to the exit status if the runtime dies unexpectedly, observable
|
||||||
|
/// via `runtime_crash_rx`.
|
||||||
|
runtime_crash_rx: tokio::sync::watch::Receiver<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for plugin initialization events (e.g., toast notifications)
|
/// Callback for plugin initialization events (e.g., toast notifications)
|
||||||
@@ -83,6 +86,12 @@ impl PluginManager {
|
|||||||
|
|
||||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||||
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
||||||
|
// Set to the exit status if the runtime dies unexpectedly. The app is
|
||||||
|
// still usable without the plugin runtime (just missing features), so
|
||||||
|
// this unblocks startup and is surfaced to the user rather than
|
||||||
|
// taking the app down.
|
||||||
|
let (unexpected_exit_tx, unexpected_exit_rx) =
|
||||||
|
tokio::sync::watch::channel::<Option<String>>(None);
|
||||||
let ws_service =
|
let ws_service =
|
||||||
PluginRuntimeServerWebsocket::new(events_tx, client_disconnect_tx, client_connect_tx);
|
PluginRuntimeServerWebsocket::new(events_tx, client_disconnect_tx, client_connect_tx);
|
||||||
|
|
||||||
@@ -96,6 +105,7 @@ impl PluginManager {
|
|||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
dev_mode,
|
dev_mode,
|
||||||
init_errors: Default::default(),
|
init_errors: Default::default(),
|
||||||
|
runtime_crash_rx: unexpected_exit_rx.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward events to subscribers
|
// Forward events to subscribers
|
||||||
@@ -132,16 +142,22 @@ impl PluginManager {
|
|||||||
let listener = TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener");
|
let listener = TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener");
|
||||||
let addr = listener.local_addr().expect("Failed to get local address");
|
let addr = listener.local_addr().expect("Failed to get local address");
|
||||||
|
|
||||||
// 1. Wait for Node.js runtime to connect
|
// 1. Wait for the Node.js runtime to connect, or for it to die trying
|
||||||
|
let mut init_dead_rx = unexpected_exit_rx.clone();
|
||||||
let init_plugins_task = tokio::spawn(async move {
|
let init_plugins_task = tokio::spawn(async move {
|
||||||
match client_connect_rx.changed().await {
|
tokio::select! {
|
||||||
Ok(_) => {
|
result = client_connect_rx.changed() => match result {
|
||||||
info!("Plugin runtime client connected!");
|
Ok(_) => {
|
||||||
// Note: initialize_all_plugins is now called separately by the app
|
info!("Plugin runtime client connected!");
|
||||||
// after setting up the plugin list
|
// Note: initialize_all_plugins is now called separately by the app
|
||||||
}
|
// after setting up the plugin list
|
||||||
Err(e) => {
|
}
|
||||||
warn!("Failed to receive from client connection rx {e:?}");
|
Err(e) => {
|
||||||
|
warn!("Failed to receive from client connection rx {e:?}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = init_dead_rx.wait_for(|status| status.is_some()) => {
|
||||||
|
warn!("Plugin runtime exited before connecting; continuing without plugins");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -159,11 +175,17 @@ impl PluginManager {
|
|||||||
addr,
|
addr,
|
||||||
&kill_server_rx,
|
&kill_server_rx,
|
||||||
killed_tx,
|
killed_tx,
|
||||||
|
unexpected_exit_tx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
info!("Waiting for plugins to initialize");
|
info!("Waiting for plugins to initialize");
|
||||||
init_plugins_task.await.map_err(|e| PluginErr(e.to_string()))?;
|
init_plugins_task.await.map_err(|e| PluginErr(e.to_string()))?;
|
||||||
|
|
||||||
|
if unexpected_exit_rx.borrow().is_some() {
|
||||||
|
warn!("Skipping plugin initialization because the runtime is not running");
|
||||||
|
return Ok(plugin_manager);
|
||||||
|
}
|
||||||
|
|
||||||
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
||||||
let db = query_manager.connect();
|
let db = query_manager.connect();
|
||||||
for dir in &bundled_dirs {
|
for dir in &bundled_dirs {
|
||||||
@@ -201,6 +223,12 @@ impl PluginManager {
|
|||||||
std::mem::take(&mut *self.init_errors.lock().await)
|
std::mem::take(&mut *self.init_errors.lock().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A receiver that is set to the exit status if the plugin runtime dies
|
||||||
|
/// unexpectedly.
|
||||||
|
pub fn runtime_crash_rx(&self) -> tokio::sync::watch::Receiver<Option<String>> {
|
||||||
|
self.runtime_crash_rx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
||||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
pub fn get_plugins_dir(&self) -> PathBuf {
|
||||||
if self.dev_mode {
|
if self.dev_mode {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::{info, warn};
|
use log::{error, info, warn};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
@@ -15,12 +15,16 @@ use yaak_common::command::new_xplatform_command;
|
|||||||
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
||||||
/// * `addr` - Socket address for the plugin runtime to connect to
|
/// * `addr` - Socket address for the plugin runtime to connect to
|
||||||
/// * `kill_rx` - Channel to signal shutdown
|
/// * `kill_rx` - Channel to signal shutdown
|
||||||
|
/// * `killed_tx` - Notified once the runtime is killed after a shutdown signal
|
||||||
|
/// * `unexpected_exit_tx` - Set to the exit status if the runtime exits
|
||||||
|
/// without being asked to
|
||||||
pub async fn start_nodejs_plugin_runtime(
|
pub async fn start_nodejs_plugin_runtime(
|
||||||
node_bin_path: &Path,
|
node_bin_path: &Path,
|
||||||
plugin_runtime_main: &Path,
|
plugin_runtime_main: &Path,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
kill_rx: &Receiver<bool>,
|
kill_rx: &Receiver<bool>,
|
||||||
killed_tx: oneshot::Sender<()>,
|
killed_tx: oneshot::Sender<()>,
|
||||||
|
unexpected_exit_tx: tokio::sync::watch::Sender<Option<String>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// HACK: Remove UNC prefix for Windows paths to pass to sidecar
|
// HACK: Remove UNC prefix for Windows paths to pass to sidecar
|
||||||
let plugin_runtime_main_str =
|
let plugin_runtime_main_str =
|
||||||
@@ -65,18 +69,29 @@ pub async fn start_nodejs_plugin_runtime(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle kill signal
|
// Wait for either an explicit kill signal or the child exiting on its own.
|
||||||
|
// An unexpected exit is reported to the caller, which decides whether to
|
||||||
|
// abort startup or surface a user-facing error.
|
||||||
let mut kill_rx = kill_rx.clone();
|
let mut kill_rx = kill_rx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if kill_rx.wait_for(|b| *b == true).await.is_err() {
|
tokio::select! {
|
||||||
warn!("Kill channel closed before explicit shutdown; terminating plugin runtime");
|
status = child.wait() => {
|
||||||
|
let status = status.map(|s| s.to_string()).unwrap_or_else(|e| e.to_string());
|
||||||
|
error!("Plugin runtime exited unexpectedly ({status})");
|
||||||
|
let _ = unexpected_exit_tx.send(Some(status));
|
||||||
|
}
|
||||||
|
closed = async { kill_rx.wait_for(|b| *b == true).await.is_err() } => {
|
||||||
|
if closed {
|
||||||
|
warn!("Kill channel closed before explicit shutdown; terminating plugin runtime");
|
||||||
|
}
|
||||||
|
info!("Killing plugin runtime");
|
||||||
|
if let Err(e) = child.kill().await {
|
||||||
|
warn!("Failed to kill plugin runtime: {e}");
|
||||||
|
}
|
||||||
|
info!("Killed plugin runtime");
|
||||||
|
let _ = killed_tx.send(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
info!("Killing plugin runtime");
|
|
||||||
if let Err(e) = child.kill().await {
|
|
||||||
warn!("Failed to kill plugin runtime: {e}");
|
|
||||||
}
|
|
||||||
info!("Killed plugin runtime");
|
|
||||||
let _ = killed_tx.send(());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user