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

@ -47,6 +47,7 @@ use uuid::Uuid;
use state::AppState;
static EXIT_GUARD_CONFIRMED: AtomicBool = AtomicBool::new(false);
static PANIC_HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MainCloseAction {
@ -69,6 +70,33 @@ fn should_install_exit_guard(window_label: &str) -> bool {
window_label == "main"
}
fn install_panic_diagnostics_hook() {
if PANIC_HOOK_INSTALLED.swap(true, Ordering::SeqCst) {
return;
}
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let message = info
.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
.unwrap_or("<non-string panic payload>");
let location = info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "<unknown>".to_owned());
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
application::diag!("[panic] thread={thread_name} location={location} message={message}");
application::diag!(
"[panic] backtrace:\n{}",
std::backtrace::Backtrace::force_capture()
);
previous(info);
}));
}
fn apply_main_close_decision(
guard: application::AppExitWorkGuardState,
already_confirmed: bool,
@ -151,6 +179,7 @@ pub fn run() {
// click-launched AppImage (whose stderr is otherwise discarded). Best-effort:
// if the file can't be opened the beacons simply stay on stderr.
application::diag::set_log_path(app_data_dir.join("logs").join("idea.log"));
install_panic_diagnostics_hook();
application::diag!("[startup] IdeA launched; diagnostics log armed");
let app_state = AppState::build_with_resource_dir(app_data_dir, resource_dir);

View File

@ -57,12 +57,28 @@ pub async fn plugin_install_from_archive(
path: String,
state: State<'_, AppState>,
) -> Result<PluginInstallResultDto, ErrorDto> {
state
application::diag!("[plugins] install archive start path={path}");
let result = state
.install_plugin_from_archive
.execute(path)
.await
.map(PluginInstallResultDto::from)
.map_err(ErrorDto::from)
.map_err(ErrorDto::from);
match &result {
Ok(result) => application::diag!(
"[plugins] install archive ok plugin={} version={} hash={} lifecycle={:?}",
result.plugin.id,
result.plugin.version,
result.review.content_hash,
result.plugin.lifecycle_state
),
Err(err) => application::diag!(
"[plugins] install archive failed code={} message={}",
err.code,
err.message
),
}
result
}
/// Installs a plugin from a local directory snapshot.
@ -71,12 +87,28 @@ pub async fn plugin_install_from_directory(
path: String,
state: State<'_, AppState>,
) -> Result<PluginInstallResultDto, ErrorDto> {
state
application::diag!("[plugins] install directory start path={path}");
let result = state
.install_plugin_from_directory
.execute(path)
.await
.map(PluginInstallResultDto::from)
.map_err(ErrorDto::from)
.map_err(ErrorDto::from);
match &result {
Ok(result) => application::diag!(
"[plugins] install directory ok plugin={} version={} hash={} lifecycle={:?}",
result.plugin.id,
result.plugin.version,
result.review.content_hash,
result.plugin.lifecycle_state
),
Err(err) => application::diag!(
"[plugins] install directory failed code={} message={}",
err.code,
err.message
),
}
result
}
/// Enables or disables a plugin.
@ -145,13 +177,30 @@ pub fn plugin_asset_protocol(
app: &AppHandle,
request: http::Request<Vec<u8>>,
) -> Response<Vec<u8>> {
let uri = request.uri().to_string();
match plugin_asset_response(app, request) {
Ok(response) => response,
Err((status, message)) => Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(message.into_bytes())
.expect("valid protocol error response"),
Ok(response) => {
application::diag!(
"[plugins] asset ok uri={} status={} bytes={}",
uri,
response.status(),
response.body().len()
);
response
}
Err((status, message)) => {
application::diag!(
"[plugins] asset failed uri={uri} status={status} message={message}"
);
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(message.into_bytes())
.unwrap_or_else(|e| {
application::diag!("[plugins] asset error response build failed: {e}");
Response::new(Vec::new())
})
}
}
}