From c4d456878322b0598495260cc58fd99416e4db77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 5 May 2026 22:20:03 +0800 Subject: [PATCH] fix: prevent stale IM adapter sidecars Telegram long polling can be consumed or disrupted by orphaned adapter sidecar processes left behind after desktop restarts or IM settings refreshes. Kill stale Unix claude-sidecar adapters processes before launching a new managed adapter sidecar so only one Telegram consumer remains active. Constraint: Telegram Bot API long polling should have a single active consumer for a token Rejected: Only restart the current tracked child | orphaned sidecars with PPID 1 are outside the tracked Tauri child handle Confidence: high Scope-risk: narrow Directive: Keep adapter sidecar startup single-instance across platforms when adding new IM adapters Tested: cargo fmt --check Tested: cargo check Tested: bun run check:native Tested: git diff --check Not-tested: Packaged macOS app relaunch with a real stale sidecar during automated test --- desktop/src-tauri/src/lib.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 7df4a847..84537cfa 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1084,6 +1084,9 @@ fn stop_server_sidecar(app: &AppHandle) { /// 启动 adapter sidecar。返回 Result 主要为了把"无法 spawn"和"spawn 后立刻 /// 退出(凭据缺失)"区分开 —— 后者不算错误,是正常 default 状态。 fn start_adapters_sidecar(app: &AppHandle) -> Result { + #[cfg(unix)] + kill_stale_unix_adapter_sidecars(); + let app_root = resolve_app_root(app)?; let app_root_arg = app_root.to_string_lossy().to_string(); @@ -1194,6 +1197,39 @@ fn stop_adapters_sidecar(app: &AppHandle) { } } +#[cfg(unix)] +fn kill_stale_unix_adapter_sidecars() { + let current_pid = std::process::id(); + let Ok(output) = StdCommand::new("ps") + .args(["-axo", "pid=,command="]) + .output() + else { + return; + }; + + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + let mut parts = line.trim_start().splitn(2, char::is_whitespace); + let Some(pid_text) = parts.next() else { + continue; + }; + let Some(command) = parts.next() else { + continue; + }; + let Ok(pid) = pid_text.parse::() else { + continue; + }; + if pid == current_pid { + continue; + } + if !command.contains("claude-sidecar") || !command.contains(" adapters") { + continue; + } + + let _ = StdCommand::new("kill").arg(pid.to_string()).status(); + } +} + #[cfg(target_os = "windows")] fn kill_windows_sidecars() { for image_name in [