fix(plugins): diagnostics crash minimales pour l'install de plugin (backend)

Écran noir hello-plugin (#120) récidivant après 3 fixes déjà mergés sans
capturer la cause réelle : on ne pouvait pas savoir si le crash venait de
l'install, du reconcile MCP ou d'un panic silencieux. Ajoute un panic hook
qui logge thread/location/backtrace, trace les étapes install/reconcile/
asset-protocol dans idea.log, et remplace les `.expect()` du superviseur MCP
plugin par une erreur typée au lieu d'un panic sur mutex empoisonné.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 09:48:42 +02:00
parent 07df50f9de
commit 961bf4623f
4 changed files with 166 additions and 41 deletions

View File

@ -5,7 +5,7 @@ use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
@ -386,6 +386,8 @@ trait ExternalMcpServerBridge: Send + Sync {
) -> Result<Box<dyn ExternalMcpServerHandle>, PluginMcpError>;
}
type ExternalMcpChildren = HashMap<String, Box<dyn ExternalMcpServerHandle>>;
struct ProcessMcpServerHandle {
child: Child,
}
@ -433,7 +435,7 @@ impl ExternalMcpServerBridge for StdioExternalMcpServerBridge {
/// External process supervisor for plugin MCP servers.
pub struct ExternalMcpPluginSupervisor {
bridge: Arc<dyn ExternalMcpServerBridge>,
children: Mutex<HashMap<String, Box<dyn ExternalMcpServerHandle>>>,
children: Mutex<ExternalMcpChildren>,
}
impl ExternalMcpPluginSupervisor {
@ -453,6 +455,12 @@ impl ExternalMcpPluginSupervisor {
children: Mutex::new(HashMap::new()),
}
}
fn children(&self) -> Result<MutexGuard<'_, ExternalMcpChildren>, PluginMcpError> {
self.children
.lock()
.map_err(|_| PluginMcpError::Process("plugin mcp supervisor mutex poisoned".to_owned()))
}
}
impl Default for ExternalMcpPluginSupervisor {
@ -469,10 +477,7 @@ impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
) -> Result<PluginMcpStatusSet, PluginMcpError> {
let desired: HashSet<String> = active_servers.iter().map(|s| s.identity.clone()).collect();
let to_stop = {
let children = self
.children
.lock()
.expect("plugin mcp supervisor poisoned");
let children = self.children()?;
children
.keys()
.filter(|id| !desired.contains(*id))
@ -480,22 +485,14 @@ impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
.collect::<Vec<_>>()
};
for id in to_stop {
let child = self
.children
.lock()
.expect("plugin mcp supervisor poisoned")
.remove(&id);
let child = self.children()?.remove(&id);
if let Some(mut child) = child {
let _ = child.stop().await;
}
}
let mut statuses = Vec::new();
for spec in active_servers {
let already = self
.children
.lock()
.expect("plugin mcp supervisor poisoned")
.contains_key(&spec.identity);
let already = self.children()?.contains_key(&spec.identity);
if already {
statuses.push(PluginMcpStatus {
identity: spec.identity,
@ -506,10 +503,12 @@ impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
}
match self.bridge.start(&spec).await {
Ok(handle) => {
self.children
.lock()
.expect("plugin mcp supervisor poisoned")
.insert(spec.identity.clone(), handle);
match self.children() {
Ok(mut children) => {
children.insert(spec.identity.clone(), handle);
}
Err(err) => return Err(err),
}
statuses.push(PluginMcpStatus {
identity: spec.identity,
running: true,
@ -529,10 +528,7 @@ impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
async fn stop_plugin(&self, plugin_id: &PluginId) -> Result<(), PluginMcpError> {
let prefix = format!("plugin:{}:", plugin_id.as_str());
let ids = {
let children = self
.children
.lock()
.expect("plugin mcp supervisor poisoned");
let children = self.children()?;
children
.keys()
.filter(|id| id.starts_with(&prefix))
@ -540,11 +536,7 @@ impl PluginMcpSupervisor for ExternalMcpPluginSupervisor {
.collect::<Vec<_>>()
};
for id in ids {
let child = self
.children
.lock()
.expect("plugin mcp supervisor poisoned")
.remove(&id);
let child = self.children()?.remove(&id);
if let Some(mut child) = child {
child.stop().await?;
}
@ -751,4 +743,20 @@ mod tests {
assert_eq!(statuses.servers[0].identity, other.identity);
assert_eq!(bridge.started.lock().unwrap().len(), 2);
}
#[tokio::test]
async fn supervisor_poisoned_registry_returns_typed_error() {
let supervisor = ExternalMcpPluginSupervisor::new();
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = supervisor.children.lock().unwrap();
panic!("poison plugin mcp supervisor registry");
}));
let err = supervisor.reconcile(Vec::new()).await.unwrap_err();
assert_eq!(
err,
PluginMcpError::Process("plugin mcp supervisor mutex poisoned".to_owned())
);
}
}