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) <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
oxoxDev
2026-05-13 10:51:26 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 Steven Enamakel
parent 737314e7b6
commit 57b4e0e3b8
2 changed files with 73 additions and 9 deletions
+34 -7
View File
@@ -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<u32> {
let output = std::process::Command::new("lsof")
+39 -2
View File
@@ -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));