cuprate_test_utils/
monerod.rs1use std::{
7 env::current_dir,
8 ffi::OsStr,
9 fs::read_dir,
10 io::Read,
11 net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
12 path::PathBuf,
13 process::{Child, Command, Stdio},
14 str::from_utf8,
15 thread::panicking,
16 time::Duration,
17};
18
19use tokio::{task::yield_now, time::timeout};
20
21const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
23
24const MONEROD_STARTUP_TEXT: &str =
26 "The daemon will start synchronizing with the network. This may take a long time to complete.";
27
28const MONEROD_SHUTDOWN_TEXT: &str = "Stopping cryptonote protocol";
30
31pub async fn monerod<T: AsRef<OsStr>>(flags: impl IntoIterator<Item = T>) -> SpawnedMoneroD {
35 let path_to_monerod = find_root().join("monerod");
36
37 let rpc_port = get_available_port(&[]);
38 let p2p_port = get_available_port(&[rpc_port]);
39 let zmq_port = get_available_port(&[rpc_port, p2p_port]);
40
41 let data_dir = tempfile::tempdir().unwrap();
42
43 let mut monerod = Command::new(path_to_monerod)
44 .stdout(Stdio::piped())
45 .stderr(Stdio::piped())
46 .args(flags)
47 .arg("--regtest")
48 .arg("--log-level=2")
49 .arg(format!("--p2p-bind-port={p2p_port}"))
50 .arg(format!("--rpc-bind-port={rpc_port}"))
51 .arg(format!("--zmq-rpc-bind-port={zmq_port}"))
52 .arg(format!("--data-dir={}", data_dir.path().display()))
53 .arg("--non-interactive")
54 .spawn()
55 .expect(
56 "Failed to start monerod, you need to have the monerod binary in the root of the repo",
57 );
58
59 let mut logs = String::new();
60
61 timeout(Duration::from_secs(30), async {
62 loop {
63 let mut next_str = [0];
64 let _ = monerod
65 .stdout
66 .as_mut()
67 .unwrap()
68 .read(&mut next_str)
69 .unwrap();
70
71 logs.push_str(from_utf8(&next_str).unwrap());
72
73 if logs.contains(MONEROD_SHUTDOWN_TEXT) {
74 panic!("Failed to start monerod, logs: \n {logs}");
75 } else if logs.contains(MONEROD_STARTUP_TEXT) {
76 break;
77 }
78 yield_now().await;
81 }
82 })
83 .await
84 .unwrap_or_else(|_| panic!("Failed to start monerod in time, logs: {logs}"));
85
86 SpawnedMoneroD {
87 process: monerod,
88 rpc_port,
89 p2p_port,
90 _data_dir: data_dir,
91 start_up_logs: logs,
92 }
93}
94
95fn find_root() -> PathBuf {
99 let mut current_dir = current_dir().unwrap();
100 loop {
101 if read_dir(current_dir.join("target")).is_ok() {
102 return current_dir;
103 } else if !current_dir.pop() {
104 panic!("Could not find ./target");
105 }
106 }
107}
108
109fn get_available_port(already_taken: &[u16]) -> u16 {
111 loop {
112 let port = TcpListener::bind("127.0.0.1:0")
114 .unwrap()
115 .local_addr()
116 .unwrap()
117 .port();
118
119 if !already_taken.contains(&port) {
120 return port;
121 }
122 }
123}
124
125pub struct SpawnedMoneroD {
127 process: Child,
129 rpc_port: u16,
131 p2p_port: u16,
133 _data_dir: tempfile::TempDir,
135 start_up_logs: String,
137}
138
139impl SpawnedMoneroD {
140 pub const fn p2p_addr(&self) -> SocketAddr {
142 SocketAddr::new(LOCALHOST, self.p2p_port)
143 }
144
145 pub const fn rpc_port(&self) -> SocketAddr {
147 SocketAddr::new(LOCALHOST, self.rpc_port)
148 }
149}
150
151impl Drop for SpawnedMoneroD {
152 fn drop(&mut self) {
153 let mut error = false;
154
155 if self.process.kill().is_err() {
156 error = true;
157 println!("Failed to kill monerod, process id: {}", self.process.id());
158 }
159
160 if panicking() {
161 let mut out = String::new();
164
165 if self
166 .process
167 .stdout
168 .as_mut()
169 .unwrap()
170 .read_to_string(&mut out)
171 .is_err()
172 {
173 println!("Failed to get monerod's logs.");
174 }
175
176 println!("-----START-MONEROD-LOGS-----");
177 println!("{}{out}", self.start_up_logs);
178 println!("------END-MONEROD-LOGS------");
179 }
180
181 #[expect(clippy::manual_assert, reason = "`if` is more clear")]
182 if error && !panicking() {
183 panic!("Error while dropping monerod");
187 }
188 }
189}