From 57b4e0e3b84f514f0fb7e66188a1f5a47aac3f72 Mon Sep 17 00:00:00 2001 From: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Date: Wed, 13 May 2026 23:21:26 +0530 Subject: [PATCH] fix(core_process): demote expected port-clash + Windows bind ACL to warn (#2B, #AT, #BV, #BT) (#1628) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Steven Enamakel --- app/src-tauri/src/core_process.rs | 41 ++++++++++++++++++++----- app/src-tauri/src/core_process_tests.rs | 41 +++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/app/src-tauri/src/core_process.rs b/app/src-tauri/src/core_process.rs index df1d1e3b6..7c92002d8 100644 --- a/app/src-tauri/src/core_process.rs +++ b/app/src-tauri/src/core_process.rs @@ -172,7 +172,11 @@ impl CoreProcessHandle { "Core RPC port {} is in use by something that is not an OpenHuman core ({reason}). Refusing to attach (set OPENHUMAN_CORE_REUSE_EXISTING=1 to override) — quit the other process or set OPENHUMAN_CORE_PORT to a different port and relaunch.", self.port ); - log::error!("[core] {msg}"); + if is_expected_port_clash(&reason) { + log::warn!("[core] {msg}"); + } else { + log::error!("[core] {msg}"); + } return Err(msg); } } @@ -236,7 +240,11 @@ impl CoreProcessHandle { ) .await { - log::error!("[core] embedded core server exited with error: {e}"); + if is_expected_port_clash(&e.to_string()) { + log::warn!("[core] embedded core server exited with error: {e}"); + } else { + log::error!("[core] embedded core server exited with error: {e}"); + } } else { log::info!("[core] embedded core server exited cleanly"); } @@ -380,14 +388,19 @@ impl CoreProcessHandle { self.shutdown().await; if !had_managed_task && self.is_rpc_port_open().await { - log::error!( + let msg = format!( + "Core RPC port {} is already in use by another process (OpenHuman did not start it). Quit any `openhuman-core run` in a terminal or set OPENHUMAN_CORE_PORT to a different port, then relaunch the app.", + self.port + ); + // Precondition check: by the time we hit this branch we already + // know the port is held by something OpenHuman did not spawn, so + // the clash is always benign environment state — no need to gate + // through `is_expected_port_clash`. + log::warn!( "[core] restart: nothing to stop but port {} is in use — another process owns it", self.port ); - return Err(format!( - "Core RPC port {} is already in use by another process (OpenHuman did not start it). Quit any `openhuman-core run` in a terminal or set OPENHUMAN_CORE_PORT to a different port, then relaunch the app.", - self.port - )); + return Err(msg); } const POLL_MS: u64 = 50; @@ -586,6 +599,20 @@ fn is_openhuman_root_body(body: &str) -> bool { .unwrap_or(false) } +/// Returns true when a port conflict is deterministic environment state, not +/// a high-signal unknown squatter worth sending to Sentry at error level. +fn is_expected_port_clash(reason: &str) -> bool { + let reason = reason.to_ascii_lowercase(); + reason.contains("error sending request for url") + || reason.contains("connection refused") + || reason.contains("returned status 404") + || reason.contains("returned status 200") + || reason.contains("body did not identify as openhuman") + || reason.contains("already in use by another process") + || reason.contains("os error 10013") + || reason.contains("wsaeacces") +} + #[cfg(unix)] fn find_pid_on_port(port: u16) -> Option { let output = std::process::Command::new("lsof") diff --git a/app/src-tauri/src/core_process_tests.rs b/app/src-tauri/src/core_process_tests.rs index 6c3425656..ba115bc99 100644 --- a/app/src-tauri/src/core_process_tests.rs +++ b/app/src-tauri/src/core_process_tests.rs @@ -1,6 +1,6 @@ use super::{ - current_rpc_token, default_core_port, generate_rpc_token, is_openhuman_root_body, - parse_lsof_pid, parse_netstat_pid, CoreProcessHandle, + current_rpc_token, default_core_port, generate_rpc_token, is_expected_port_clash, + is_openhuman_root_body, parse_lsof_pid, parse_netstat_pid, CoreProcessHandle, }; use std::sync::{Mutex, MutexGuard, OnceLock}; @@ -127,6 +127,43 @@ fn is_openhuman_root_body_rejects_other_services() { assert!(!is_openhuman_root_body(r#"{"name": 42}"#)); } +#[test] +fn expected_port_clash_classifier_matches_benign_probe_shapes() { + assert!(is_expected_port_clash( + "probe GET / failed: error sending request for url (http://127.0.0.1:7788/)" + )); + assert!(is_expected_port_clash( + "probe GET / failed: connection refused" + )); + assert!(is_expected_port_clash( + "probe GET / returned status 404 Not Found" + )); + assert!(is_expected_port_clash("probe GET / returned status 200 OK")); + assert!(is_expected_port_clash( + "probe GET / body did not identify as openhuman (\"hello\")" + )); +} + +#[test] +fn expected_port_clash_classifier_matches_windows_acl_bind_shapes() { + assert!(is_expected_port_clash( + "Failed to bind to 127.0.0.1:7788: access denied (os error 10013)" + )); + assert!(is_expected_port_clash( + "Failed to bind to 127.0.0.1:7788: WSAEACCES" + )); +} + +#[test] +fn expected_port_clash_classifier_rejects_unknown_probe_shapes() { + assert!(!is_expected_port_clash( + "probe GET / failed: TLS handshake failed: protocol error" + )); + assert!(!is_expected_port_clash( + "probe GET / body read failed: unexpected eof" + )); +} + #[test] fn parse_lsof_pid_picks_first_pid() { assert_eq!(parse_lsof_pid("12345\n"), Some(12345));