Gracefully handle plugin init failures (#424)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-11 16:55:46 -07:00
committed by GitHub
parent 8a330ad1ec
commit c8ba35e268
5 changed files with 59 additions and 11 deletions

View File

@@ -1383,13 +1383,12 @@ async fn cmd_reload_plugins<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> YaakResult<()> { ) -> YaakResult<Vec<(String, String)>> {
let plugins = app_handle.db().list_plugins()?; let plugins = app_handle.db().list_plugins()?;
let plugin_context = let plugin_context =
PluginContext::new(Some(window.label().to_string()), window.workspace_id()); PluginContext::new(Some(window.label().to_string()), window.workspace_id());
let _errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await; let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await;
// Note: errors are returned but we don't show toasts here since this is a manual reload Ok(errors)
Ok(())
} }
#[tauri::command] #[tauri::command]
@@ -1731,6 +1730,7 @@ pub fn run() {
git_ext::cmd_git_rm_remote, git_ext::cmd_git_rm_remote,
// //
// Plugin commands // Plugin commands
plugins_ext::cmd_plugin_init_errors,
plugins_ext::cmd_plugins_install_from_directory, plugins_ext::cmd_plugins_install_from_directory,
plugins_ext::cmd_plugins_search, plugins_ext::cmd_plugins_search,
plugins_ext::cmd_plugins_install, plugins_ext::cmd_plugins_install,

View File

@@ -198,6 +198,13 @@ pub async fn cmd_plugins_uninstall<R: Runtime>(
Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?) Ok(delete_and_uninstall(plugin_manager, &query_manager, &plugin_context, plugin_id).await?)
} }
#[command]
pub async fn cmd_plugin_init_errors(
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<(String, String)>> {
Ok(plugin_manager.take_init_errors().await)
}
#[command] #[command]
pub async fn cmd_plugins_updates<R: Runtime>( pub async fn cmd_plugins_updates<R: Runtime>(
app_handle: AppHandle<R>, app_handle: AppHandle<R>,
@@ -306,7 +313,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
dev_mode, dev_mode,
) )
.await .await
.expect("Failed to initialize plugins"); .expect("Failed to start plugin runtime");
app_handle_clone.manage(manager); app_handle_clone.manage(manager);
}); });

View File

@@ -50,6 +50,8 @@ pub struct PluginManager {
vendored_plugin_dir: PathBuf, vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf,
dev_mode: bool, dev_mode: bool,
/// Errors from plugin initialization, retrievable once via `take_init_errors`.
init_errors: Arc<Mutex<Vec<(String, String)>>>,
} }
/// Callback for plugin initialization events (e.g., toast notifications) /// Callback for plugin initialization events (e.g., toast notifications)
@@ -93,6 +95,7 @@ impl PluginManager {
vendored_plugin_dir, vendored_plugin_dir,
installed_plugin_dir, installed_plugin_dir,
dev_mode, dev_mode,
init_errors: Default::default(),
}; };
// Forward events to subscribers // Forward events to subscribers
@@ -183,17 +186,21 @@ impl PluginManager {
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await; let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
if !init_errors.is_empty() { if !init_errors.is_empty() {
let joined = init_errors for (dir, err) in &init_errors {
.into_iter() warn!("Plugin failed to initialize: {dir}: {err}");
.map(|(dir, err)| format!("{dir}: {err}")) }
.collect::<Vec<_>>() *plugin_manager.init_errors.lock().await = init_errors;
.join("; ");
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
} }
Ok(plugin_manager) Ok(plugin_manager)
} }
/// Take any initialization errors, clearing them from the manager.
/// Returns a list of `(plugin_directory, error_message)` pairs.
pub async fn take_init_errors(&self) -> Vec<(String, String)> {
std::mem::take(&mut *self.init_errors.lock().await)
}
/// 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 {

View File

@@ -123,6 +123,39 @@ export function initGlobalListeners() {
console.log('Got plugin updates event', payload); console.log('Got plugin updates event', payload);
showPluginUpdatesToast(payload); showPluginUpdatesToast(payload);
}); });
// Check for plugin initialization errors
invokeCmd<[string, string][]>('cmd_plugin_init_errors').then((errors) => {
for (const [dir, message] of errors) {
const dirBasename = dir.split('/').pop() ?? dir;
showToast({
id: `plugin-init-error-${dirBasename}`,
color: 'warning',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Plugin failed to load</h2>
<p className="text-text-subtle text-sm">
{dirBasename}: {message}
</p>
</VStack>
),
action: ({ hide }) => (
<Button
size="xs"
color="warning"
variant="border"
onClick={() => {
hide();
openSettings.mutate('plugins:installed');
}}
>
View Plugins
</Button>
),
});
}
});
} }
function showUpdateInstalledToast(version: string) { function showUpdateInstalledToast(version: string) {

View File

@@ -42,6 +42,7 @@ type TauriCmd =
| 'cmd_new_child_window' | 'cmd_new_child_window'
| 'cmd_new_main_window' | 'cmd_new_main_window'
| 'cmd_plugin_info' | 'cmd_plugin_info'
| 'cmd_plugin_init_errors'
| 'cmd_reload_plugins' | 'cmd_reload_plugins'
| 'cmd_render_template' | 'cmd_render_template'
| 'cmd_save_response' | 'cmd_save_response'