alloy_node_bindings/
utils.rs1use 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
16pub(crate) struct GracefulShutdown;
18
19impl GracefulShutdown {
20 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 }
42}
43
44pub(crate) fn unused_port() -> u16 {
49 let listener = TcpListener::bind("127.0.0.1:0")
50 .expect("Failed to create TCP listener to find unused port");
51
52 let local_addr =
53 listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port");
54 local_addr.port()
55}
56
57pub(crate) fn extract_value<'a>(key: &str, line: &'a str) -> Option<&'a str> {
63 let mut key_equal = Cow::from(key);
64 let mut key_colon = Cow::from(key);
65
66 if !key_equal.ends_with('=') {
68 key_equal = format!("{key}=").into();
69 }
70 if !key_colon.ends_with(": ") {
71 key_colon = format!("{key}: ").into();
72 }
73
74 if let Some(pos) = line.find(key_equal.as_ref()) {
76 let start = pos + key_equal.len();
77 let end = line[start..].find(' ').map(|i| start + i).unwrap_or(line.len());
78 if start <= line.len() && end <= line.len() {
79 return Some(line[start..end].trim());
80 }
81 }
82
83 if let Some(pos) = line.find(key_colon.as_ref()) {
85 let start = pos + key_colon.len();
86 let end = line[start..].find(',').map(|i| start + i).unwrap_or(line.len()); if start <= line.len() && end <= line.len() {
88 return Some(line[start..end].trim());
89 }
90 }
91
92 None
94}
95
96pub(crate) fn extract_endpoint(key: &str, line: &str) -> Option<SocketAddr> {
98 extract_value(key, line)
99 .map(|val| val.trim_start_matches("Some(").trim_end_matches(')'))
100 .and_then(|val| val.parse().ok())
101}
102
103pub fn run_with_tempdir_sync(prefix: &str, f: impl FnOnce(PathBuf)) {
105 let temp_dir = TempDir::with_prefix(prefix).unwrap();
106 let temp_dir_path = temp_dir.path().to_path_buf();
107 f(temp_dir_path);
108 #[cfg(not(windows))]
109 {
110 let _ = temp_dir.close();
111 }
112}
113
114pub async fn run_with_tempdir<F, Fut>(prefix: &str, f: F)
116where
117 F: FnOnce(PathBuf) -> Fut,
118 Fut: Future<Output = ()>,
119{
120 let temp_dir = TempDir::with_prefix(prefix).unwrap();
121 let temp_dir_path = temp_dir.path().to_path_buf();
122 f(temp_dir_path).await;
123 #[cfg(not(windows))]
124 {
125 let _ = temp_dir.close();
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::net::SocketAddr;
133
134 #[test]
135 fn test_extract_value_with_equals() {
136 let line = "key=value some other text";
137 assert_eq!(extract_value("key", line), Some("value"));
138 }
139
140 #[test]
141 fn test_extract_value_with_colon() {
142 let line = "key: value, more text here";
143 assert_eq!(extract_value("key", line), Some("value"));
144 }
145
146 #[test]
147 fn test_extract_value_not_found() {
148 let line = "unrelated text";
149 assert_eq!(extract_value("key", line), None);
150 }
151
152 #[test]
153 fn test_extract_value_equals_no_space() {
154 let line = "INFO key=";
155 assert_eq!(extract_value("key", line), Some(""))
156 }
157
158 #[test]
159 fn test_extract_value_colon_no_comma() {
160 let line = "INFO key: value";
161 assert_eq!(extract_value("key", line), Some("value"))
162 }
163
164 #[test]
165 fn test_extract_http_address() {
166 let line = "INFO [07-01|13:20:42.774] HTTP server started endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost";
167 assert_eq!(
168 extract_endpoint("endpoint=", line),
169 Some(SocketAddr::from(([127, 0, 0, 1], 8545)))
170 );
171 }
172
173 #[test]
174 fn test_extract_udp_address() {
175 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\")], .. }";
176 assert_eq!(
177 extract_endpoint("IpV4 TCP Socket: ", line),
178 Some(SocketAddr::from(([0, 0, 0, 0], 30303)))
179 );
180 }
181
182 #[test]
183 fn test_unused_port() {
184 let port = unused_port();
185 assert!(port > 0);
186 }
187
188 #[test]
189 fn test_run_with_tempdir_sync() {
190 run_with_tempdir_sync("test_prefix", |path| {
191 assert!(path.exists(), "Temporary directory should exist");
192 assert!(path.is_dir(), "Temporary directory should be a directory");
193 });
194 }
195
196 #[tokio::test]
197 async fn test_run_with_tempdir_async() {
198 run_with_tempdir("test_prefix", |path| async move {
199 assert!(path.exists(), "Temporary directory should exist");
200 assert!(path.is_dir(), "Temporary directory should be a directory");
201 })
202 .await;
203 }
204}