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:
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user