feat(diagnose): detect and inform about @homebridge/ciao cmd popup bug (#250)

* feat(diagnose): detect and inform about @homebridge/ciao cmd popup bug

On Windows, OpenClaw's transitive dependency @homebridge/ciao (<=1.3.6)
calls child_process.exec('arp -a ...') every 15-30 seconds without
passing windowsHide:true, causing a cmd.exe popup to flash.

This is an upstream library bug:
- Issue: homebridge/ciao#64
- PR:    homebridge/ciao#65 (open, not merged)

ClawPanel deliberately chooses 'detect and inform' rather than silently
patching the user's node_modules. We respect the user's control over
their own machine.

Changes:
- src-tauri/src/commands/diagnose.rs: new check_ciao_windowshide_bug
  command; scans openclaw's @homebridge/ciao/lib/NetworkManager.js and
  reports whether the buggy exec pattern is present
- src-tauri/src/lib.rs: register the new command
- scripts/dev-api.js: Web-mode stub (returns affected:false since the
  bug does not manifest off-Windows)
- src/lib/tauri-api.js: add api.checkCiaoWindowsHideBug
- src/lib/ciao-bug-warning.js: new module with toast + modal flow,
  version-scoped dismiss (localStorage)
- src/locales/modules/ciaoBug.js: translations in 5 primary languages
- src/locales/index.js: register the ciaoBug module
- src/main.js: call checker 3s after splash hides

Non-Windows users see nothing; Windows users see a single warning toast
(version-dismissible) linking to three fix paths: wait for upstream,
apply patch-package, or edit NetworkManager.js manually.

* fix(diagnose): gate helper with cfg(windows), drop unneeded return

CI failures on Linux + macOS:
- openclaw_module_root was dead code when target_os != windows
  since the only caller is the #[cfg(target_os = "windows")] block
  inside check_ciao_windowshide_bug
- Explicit `return CiaoCheckResult {...};` in the non-Windows branch
  triggered clippy::needless_return

Fix:
- Add #[cfg(target_os = "windows")] to openclaw_module_root so it
  is not compiled on other platforms
- Convert the non-Windows early exit to a tail expression
This commit is contained in:
晴天
2026-04-24 19:36:20 +08:00
committed by GitHub
parent da5adc5843
commit 11cd6218dc
8 changed files with 439 additions and 1 deletions

View File

@@ -364,3 +364,153 @@ pub async fn diagnose_gateway_connection() -> DiagnoseResult {
summary,
}
}
// =============================================================================
// @homebridge/ciao Windows cmd popup bug detection
//
// Upstream issue: https://github.com/homebridge/ciao/issues/64
// Upstream PR: https://github.com/homebridge/ciao/pull/65 (still open)
//
// Symptom on Windows: every 15-30s a cmd.exe / conhost.exe window flashes while
// Gateway is running. Root cause is @homebridge/ciao < 1.3.7 calling
// `child_process.exec("arp -a ...", callback)` without `{ windowsHide: true }`.
//
// This is NOT a ClawPanel bug — we only expose a detection command so the
// dashboard can surface a clear, actionable hint to users rather than silently
// inheriting third-party noise.
// =============================================================================
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CiaoCheckResult {
/// Whether the bug is affecting the current installation
pub affected: bool,
/// Platform quick-check (non-Windows installations can never be affected)
pub platform: String,
/// Detected @homebridge/ciao version if the package is installed
pub version: Option<String>,
/// Absolute path to NetworkManager.js (when detected)
pub network_manager_path: Option<String>,
/// Human-readable detail for the UI
pub detail: String,
}
/// Resolve the openclaw CLI module root — directory containing the installed
/// package's `package.json`. Returns None when the CLI cannot be located.
/// Only compiled on Windows since ciao bug detection is Windows-only.
#[cfg(target_os = "windows")]
fn openclaw_module_root() -> Option<std::path::PathBuf> {
let cli = crate::utils::resolve_openclaw_cli_path()?;
let cli_path = std::path::PathBuf::from(&cli);
// The CLI entrypoint is typically `<module_root>/dist/entry.js` or
// similar. Walk up until we find a `package.json`, stopping at the
// nearest node_modules boundary.
let mut current = cli_path.parent()?.to_path_buf();
for _ in 0..6 {
if current.join("package.json").is_file() {
return Some(current);
}
current = current.parent()?.to_path_buf();
}
None
}
/// Check the `@homebridge/ciao` package bundled with openclaw. Only runs on
/// Windows since the bug does not manifest on other platforms.
#[tauri::command]
pub fn check_ciao_windowshide_bug() -> CiaoCheckResult {
let platform = std::env::consts::OS.to_string();
#[cfg(not(target_os = "windows"))]
{
CiaoCheckResult {
affected: false,
platform,
version: None,
network_manager_path: None,
detail: "Non-Windows platform — bug does not manifest here.".into(),
}
}
#[cfg(target_os = "windows")]
{
let Some(root) = openclaw_module_root() else {
return CiaoCheckResult {
affected: false,
platform,
version: None,
network_manager_path: None,
detail: "openclaw CLI not installed; nothing to check.".into(),
};
};
let ciao_dir = root.join("node_modules").join("@homebridge").join("ciao");
if !ciao_dir.is_dir() {
return CiaoCheckResult {
affected: false,
platform,
version: None,
network_manager_path: None,
detail: "@homebridge/ciao not found in openclaw dependencies.".into(),
};
}
// Read version for reporting only — we do not key off it to avoid
// lying to the user if someone backports the fix without bumping.
let version = std::fs::read_to_string(ciao_dir.join("package.json"))
.ok()
.and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
.and_then(|v| v.get("version").and_then(|s| s.as_str()).map(String::from));
let nm_path = ciao_dir.join("lib").join("NetworkManager.js");
if !nm_path.is_file() {
return CiaoCheckResult {
affected: false,
platform,
version,
network_manager_path: None,
detail: "NetworkManager.js not found; skipping scan.".into(),
};
}
let content = match std::fs::read_to_string(&nm_path) {
Ok(text) => text,
Err(err) => {
return CiaoCheckResult {
affected: false,
platform,
version,
network_manager_path: Some(nm_path.to_string_lossy().to_string()),
detail: format!("Unable to read NetworkManager.js: {err}"),
};
}
};
// Detection heuristic: look for the Windows ARP call and check whether
// the third argument is an options object or the callback. A fixed
// version uses exec("arp -a ...", { windowsHide: true }, callback).
// The buggy version uses exec("arp -a ...", (error, stdout) => ...).
let affected = content.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.contains(".exec(\"arp -a")
&& !trimmed.contains("windowsHide")
&& !trimmed.contains("windows_hide")
});
let detail = if affected {
"Detected @homebridge/ciao without windowsHide option — cmd.exe will flash every 15-30s while Gateway runs. See upstream issues #64 / #65."
.into()
} else {
"No buggy @homebridge/ciao pattern detected.".into()
};
CiaoCheckResult {
affected,
platform,
version,
network_manager_path: Some(nm_path.to_string_lossy().to_string()),
detail,
}
}
}

View File

@@ -130,6 +130,7 @@ pub fn run() {
service::guardian_status,
// 诊断
diagnose::diagnose_gateway_connection,
diagnose::check_ciao_windowshide_bug,
// 日志
logs::read_log_tail,
logs::search_log,