mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-10 23:02:46 +02:00
Detect and surface plugin runtime crashes (#502)
This commit is contained in:
@@ -52,6 +52,9 @@ pub struct PluginManager {
|
||||
dev_mode: bool,
|
||||
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
|
||||
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)
|
||||
@@ -83,6 +86,12 @@ impl PluginManager {
|
||||
|
||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||
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 =
|
||||
PluginRuntimeServerWebsocket::new(events_tx, client_disconnect_tx, client_connect_tx);
|
||||
|
||||
@@ -96,6 +105,7 @@ impl PluginManager {
|
||||
installed_plugin_dir,
|
||||
dev_mode,
|
||||
init_errors: Default::default(),
|
||||
runtime_crash_rx: unexpected_exit_rx.clone(),
|
||||
};
|
||||
|
||||
// Forward events to subscribers
|
||||
@@ -132,16 +142,22 @@ impl PluginManager {
|
||||
let listener = TcpListener::bind(listen_addr).await.expect("Failed to bind TCP listener");
|
||||
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 {
|
||||
match client_connect_rx.changed().await {
|
||||
Ok(_) => {
|
||||
info!("Plugin runtime client connected!");
|
||||
// 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:?}");
|
||||
tokio::select! {
|
||||
result = client_connect_rx.changed() => match result {
|
||||
Ok(_) => {
|
||||
info!("Plugin runtime client connected!");
|
||||
// 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:?}");
|
||||
}
|
||||
},
|
||||
_ = 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,
|
||||
&kill_server_rx,
|
||||
killed_tx,
|
||||
unexpected_exit_tx,
|
||||
)
|
||||
.await?;
|
||||
info!("Waiting for plugins to initialize");
|
||||
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 db = query_manager.connect();
|
||||
for dir in &bundled_dirs {
|
||||
@@ -201,6 +223,12 @@ impl PluginManager {
|
||||
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)
|
||||
pub fn get_plugins_dir(&self) -> PathBuf {
|
||||
if self.dev_mode {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error::Result;
|
||||
use log::{info, warn};
|
||||
use log::{error, info, warn};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
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
|
||||
/// * `addr` - Socket address for the plugin runtime to connect to
|
||||
/// * `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(
|
||||
node_bin_path: &Path,
|
||||
plugin_runtime_main: &Path,
|
||||
addr: SocketAddr,
|
||||
kill_rx: &Receiver<bool>,
|
||||
killed_tx: oneshot::Sender<()>,
|
||||
unexpected_exit_tx: tokio::sync::watch::Sender<Option<String>>,
|
||||
) -> Result<()> {
|
||||
// HACK: Remove UNC prefix for Windows paths to pass to sidecar
|
||||
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();
|
||||
tokio::spawn(async move {
|
||||
if kill_rx.wait_for(|b| *b == true).await.is_err() {
|
||||
warn!("Kill channel closed before explicit shutdown; terminating plugin runtime");
|
||||
tokio::select! {
|
||||
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(())
|
||||
|
||||
Reference in New Issue
Block a user