Skip to main content

futu_server/
bind_hint.rs

1//! Operator-facing listener bind error hints shared by TCP/WS/Telnet/REST/gRPC.
2
3use std::fmt;
4
5use futu_core::log_redact::endpoint_log_fingerprint;
6
7fn listen_port(addr: &str) -> Option<&str> {
8    let trimmed = addr.trim();
9    if let Some((_, port)) = trimmed.rsplit_once("]:") {
10        return (!port.is_empty()).then_some(port);
11    }
12    trimmed
13        .rsplit_once(':')
14        .and_then(|(_, port)| (!port.is_empty()).then_some(port))
15}
16
17/// Build a stable, grep-friendly bind failure message.
18#[must_use]
19pub fn bind_error_message(
20    surface: &str,
21    flag_name: &str,
22    listen_addr: &str,
23    error: impl fmt::Display,
24) -> String {
25    let inspect = listen_port(listen_addr)
26        .map(|port| format!("lsof -nP -iTCP:{port} -sTCP:LISTEN"))
27        .unwrap_or_else(|| "lsof -nP -iTCP -sTCP:LISTEN".to_string());
28
29    format!(
30        "{surface} listener failed to bind {listen_addr}: {error}. \
31         Hint: another futu-opend or local process may already own this port. \
32         Change {flag_name}, stop the old process, or inspect with `{inspect}`."
33    )
34}
35
36fn io_bind_error_message(
37    surface: &str,
38    flag_name: &str,
39    listen_addr: &str,
40    error: &std::io::Error,
41) -> String {
42    let endpoint_fingerprint = endpoint_log_fingerprint(listen_addr);
43
44    format!(
45        "{surface} listener failed to bind endpoint_fingerprint={endpoint_fingerprint} \
46         error_kind={:?}: {error}. \
47         Hint: another futu-opend or local process may already own this port. \
48         Change {flag_name}, stop the old process, or inspect with \
49         `lsof -nP -iTCP -sTCP:LISTEN`.",
50        error.kind(),
51    )
52}
53
54/// Build a redacted invalid-listener-address error without echoing user input.
55pub fn invalid_addr_error(
56    surface: &str,
57    flag_name: &str,
58    listen_addr: &str,
59    error: impl fmt::Display,
60) -> std::io::Error {
61    let endpoint_fingerprint = endpoint_log_fingerprint(listen_addr);
62    std::io::Error::new(
63        std::io::ErrorKind::InvalidInput,
64        format!(
65            "{surface} listener address is invalid \
66             endpoint_fingerprint={endpoint_fingerprint} error_kind=InvalidInput: {error}. \
67             Hint: correct {flag_name} and restart."
68        ),
69    )
70}
71
72/// Preserve a post-bind listener runtime failure without echoing its address.
73pub fn listener_runtime_error(
74    surface: &str,
75    listen_addr: &str,
76    error: impl fmt::Display,
77) -> std::io::Error {
78    let endpoint_fingerprint = endpoint_log_fingerprint(listen_addr);
79    std::io::Error::other(format!(
80        "{surface} listener runtime failed endpoint_fingerprint={endpoint_fingerprint}: {error}"
81    ))
82}
83
84/// Build a warning for the pre-bind startup probe that sees an existing
85/// listener on the configured port.
86#[must_use]
87pub fn port_conflict_message(surface: &str, flag_name: &str, listen_addr: &str) -> String {
88    let inspect = listen_port(listen_addr)
89        .map(|port| format!("lsof -nP -iTCP:{port} -sTCP:LISTEN"))
90        .unwrap_or_else(|| "lsof -nP -iTCP -sTCP:LISTEN".to_string());
91
92    format!(
93        "{surface} port {listen_addr} already accepts TCP connections. \
94         Hint: another futu-opend or local process may be running. \
95         Change {flag_name}, stop the old process, or inspect with `{inspect}`."
96    )
97}
98
99/// Preserve the original `std::io::ErrorKind` while replacing the message with
100/// an operator-actionable hint.
101pub fn io_bind_error(
102    surface: &str,
103    flag_name: &str,
104    listen_addr: &str,
105    error: std::io::Error,
106) -> std::io::Error {
107    let kind = error.kind();
108    std::io::Error::new(
109        kind,
110        io_bind_error_message(surface, flag_name, listen_addr, &error),
111    )
112}
113
114#[cfg(test)]
115mod tests;