Skip to main content

alloy_node_bindings/
utils.rs

1//! Utility functions for the node bindings.
2
3use std::{
4    borrow::Cow,
5    future::Future,
6    net::{SocketAddr, TcpListener},
7    path::PathBuf,
8    process::Child,
9    time::{Duration, Instant},
10};
11use tempfile::TempDir;
12
13#[cfg(unix)]
14use libc;
15
16/// Helper for graceful process shutdown.
17pub(crate) struct GracefulShutdown;
18
19impl GracefulShutdown {
20    /// Attempts graceful shutdown with SIGTERM, then SIGKILL after timeout.
21    pub(crate) fn shutdown(child: &mut Child, timeout_secs: u64, process_name: &str) {
22        #[cfg(unix)]
23        {
24            unsafe {
25                libc::kill(child.id() as i32, libc::SIGTERM);
26            }
27
28            let timeout = Duration::from_secs(timeout_secs);
29            let start = Instant::now();
30
31            while start.elapsed() < timeout {
32                match child.try_wait() {
33                    Ok(Some(_)) => return,
34                    Ok(None) => std::thread::sleep(Duration::from_millis(100)),
35                    Err(_) => break,
36                }
37            }
38        }
39
40        child.kill().unwrap_or_else(|_| panic!("could not kill {}", process_name));
41        let _ = child.wait();
42    }
43}
44
45/// A bit of hack to find an unused TCP port.
46///
47/// Does not guarantee that the given port is unused after the function exists, just that it was
48/// unused before the function started (i.e., it does not reserve a port).
49pub(crate) fn unused_port() -> u16 {
50    let listener = TcpListener::bind("127.0.0.1:0")
51        .expect("Failed to create TCP listener to find unused port");
52
53    let local_addr =
54        listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port");
55    local_addr.port()
56}
57
58/// Extracts the value for the given key from the line of text.
59///
60/// It supports keys that end with '=' or ': '.
61/// For keys end with '=', find value until ' ' is encountered or end of line
62/// For keys end with ':', find value until ',' is encountered or end of line
63pub(crate) fn extract_value<'a>(key: &str, line: &'a str) -> Option<&'a str> {
64    let mut key_equal = Cow::from(key);
65    let mut key_colon = Cow::from(key);
66
67    // Prepare both key variants
68    if !key_equal.ends_with('=') {
69        key_equal = format!("{key}=").into();
70    }
71    if !key_colon.ends_with(": ") {
72        key_colon = format!("{key}: ").into();
73    }
74
75    // Try to find the key with '='
76    if let Some(pos) = line.find(key_equal.as_ref()) {
77        let start = pos + key_equal.len();
78        let end = line[start..].find(' ').map(|i| start + i).unwrap_or(line.len());
79        return Some(line[start..end].trim());
80    }
81
82    // If not found, try to find the key with ': '
83    if let Some(pos) = line.find(key_colon.as_ref()) {
84        let start = pos + key_colon.len();
85        let end = line[start..].find(',').map(|i| start + i).unwrap_or(line.len()); // Assuming comma or end of line
86        return Some(line[start..end].trim());
87    }
88
89    // If neither variant matches, return None
90    None
91}
92
93/// Extracts the endpoint from the given line.
94pub(crate) fn extract_endpoint(key: &str, line: &str) -> Option<SocketAddr> {
95    extract_value(key, line)
96        .map(|val| val.trim_start_matches("Some(").trim_end_matches(')'))
97        .and_then(|val| val.parse().ok())
98}
99
100/// Runs the given closure with a temporary directory.
101pub fn run_with_tempdir_sync(prefix: &str, f: impl FnOnce(PathBuf)) {
102    let temp_dir = TempDir::with_prefix(prefix).unwrap();
103    let temp_dir_path = temp_dir.path().to_path_buf();
104    f(temp_dir_path);
105}
106
107/// Runs the given async closure with a temporary directory.
108pub async fn run_with_tempdir<F, Fut>(prefix: &str, f: F)
109where
110    F: FnOnce(PathBuf) -> Fut,
111    Fut: Future<Output = ()>,
112{
113    let temp_dir = TempDir::with_prefix(prefix).unwrap();
114    let temp_dir_path = temp_dir.path().to_path_buf();
115    f(temp_dir_path).await;
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use std::net::SocketAddr;
122
123    #[test]
124    fn test_extract_value_with_equals() {
125        let line = "key=value some other text";
126        assert_eq!(extract_value("key", line), Some("value"));
127    }
128
129    #[test]
130    fn test_extract_value_with_colon() {
131        let line = "key: value, more text here";
132        assert_eq!(extract_value("key", line), Some("value"));
133    }
134
135    #[test]
136    fn test_extract_value_not_found() {
137        let line = "unrelated text";
138        assert_eq!(extract_value("key", line), None);
139    }
140
141    #[test]
142    fn test_extract_value_equals_no_space() {
143        let line = "INFO key=";
144        assert_eq!(extract_value("key", line), Some(""))
145    }
146
147    #[test]
148    fn test_extract_value_colon_no_comma() {
149        let line = "INFO key: value";
150        assert_eq!(extract_value("key", line), Some("value"))
151    }
152
153    #[test]
154    fn test_extract_http_address() {
155        let line = "INFO [07-01|13:20:42.774] HTTP server started                      endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost";
156        assert_eq!(
157            extract_endpoint("endpoint=", line),
158            Some(SocketAddr::from(([127, 0, 0, 1], 8545)))
159        );
160    }
161
162    #[test]
163    fn test_extract_udp_address() {
164        let line = "Updated local ENR enr=Enr { id: Some(\"v4\"), seq: 2, NodeId: 0x04dad428038b4db230fc5298646e137564fc6861662f32bdbf220f31299bdde7, signature: \"416520d69bfd701d95f4b77778970a5c18fa86e4dd4dc0746e80779d986c68605f491c01ef39cd3739fdefc1e3558995ad2f5d325f9e1db795896799e8ee94a3\", IpV4 UDP Socket: Some(0.0.0.0:30303), IpV6 UDP Socket: None, IpV4 TCP Socket: Some(0.0.0.0:30303), IpV6 TCP Socket: None, Other Pairs: [(\"eth\", \"c984fc64ec0483118c30\"), (\"secp256k1\", \"a103aa181e8fd5df651716430f1d4b504b54d353b880256f56aa727beadd1b7a9766\")], .. }";
165        assert_eq!(
166            extract_endpoint("IpV4 TCP Socket: ", line),
167            Some(SocketAddr::from(([0, 0, 0, 0], 30303)))
168        );
169    }
170
171    #[test]
172    fn test_unused_port() {
173        let port = unused_port();
174        assert!(port > 0);
175    }
176
177    #[test]
178    fn test_run_with_tempdir_sync() {
179        run_with_tempdir_sync("test_prefix", |path| {
180            assert!(path.exists(), "Temporary directory should exist");
181            assert!(path.is_dir(), "Temporary directory should be a directory");
182        });
183    }
184
185    #[tokio::test]
186    async fn test_run_with_tempdir_async() {
187        run_with_tempdir("test_prefix", |path| async move {
188            assert!(path.exists(), "Temporary directory should exist");
189            assert!(path.is_dir(), "Temporary directory should be a directory");
190        })
191        .await;
192    }
193
194    #[cfg(unix)]
195    #[test]
196    fn graceful_shutdown_reaps_after_force_kill() {
197        let mut child = std::process::Command::new("sh")
198            .arg("-c")
199            .arg("trap '' TERM; while :; do :; done")
200            .spawn()
201            .unwrap();
202
203        GracefulShutdown::shutdown(&mut child, 0, "sh");
204
205        assert!(child.try_wait().unwrap().is_some());
206    }
207}