1use crate::IntoCString;
2use crate::{Aeron, AeronArchive, AeronArchiveAsyncConnect, AeronArchiveContext, AeronContext};
3use log::info;
4use log::{error, warn};
5use regex::Regex;
6use std::backtrace::Backtrace;
7use std::ffi::CString;
8use std::path::Path;
9use std::process::{Child, Command, ExitStatus, Stdio};
10use std::thread::sleep;
11use std::time::{Duration, Instant};
12use std::{fs, io, panic, process};
13
14pub(crate) fn running_under_valgrind() -> bool {
17 std::env::var_os("RUSTERON_VALGRIND").is_some()
18}
19
20pub(crate) fn valgrind_timeout(base_secs: u64) -> Duration {
23 if running_under_valgrind() {
24 Duration::from_secs(base_secs.saturating_mul(3))
25 } else {
26 Duration::from_secs(base_secs)
27 }
28}
29
30pub struct EmbeddedArchiveMediaDriverProcess {
31 child: Child,
32 pub aeron_dir: CString,
33 pub archive_dir: CString,
34 pub control_request_channel: String,
35 pub control_response_channel: String,
36 pub recording_events_channel: String,
37}
38
39impl EmbeddedArchiveMediaDriverProcess {
40 pub fn build_and_start(
81 aeron_dir: &str,
82 archive_dir: &str,
83 control_request_channel: &str,
84 control_response_channel: &str,
85 recording_events_channel: &str,
86 ) -> io::Result<Self> {
87 let path = std::path::MAIN_SEPARATOR;
88 let gradle = if cfg!(target_os = "windows") {
89 &format!("{}{path}aeron{path}gradlew.bat", env!("CARGO_MANIFEST_DIR"),)
90 } else {
91 "./gradlew"
92 };
93 let dir = format!("{}{path}aeron", env!("CARGO_MANIFEST_DIR"),);
94 info!("running {} in {}", gradle, dir);
95
96 if !Path::new(&format!(
97 "{}{path}aeron{path}aeron-all{path}build{path}libs",
98 env!("CARGO_MANIFEST_DIR")
99 ))
100 .exists()
101 {
102 Command::new(&gradle)
103 .current_dir(dir)
104 .args([
105 ":aeron-agent:jar",
106 ":aeron-samples:jar",
107 ":aeron-archive:jar",
108 ":aeron-all:build",
109 ])
110 .stdout(Stdio::inherit())
111 .stderr(Stdio::inherit())
112 .spawn()?
113 .wait()?;
114 }
115
116 return Self::start(
117 &aeron_dir,
118 archive_dir,
119 control_request_channel,
120 control_response_channel,
121 recording_events_channel,
122 );
123 }
124
125 pub fn run_aeron_stats(&self) -> std::io::Result<Child> {
126 let main_dir = env!("CARGO_MANIFEST_DIR");
127 let dir = format!("{}/{}", main_dir, &self.aeron_dir.to_str().unwrap());
128 info!("running 'just aeron-stat {}'", dir);
129 Command::new("just")
130 .args(["aeron-stat", dir.as_str()])
131 .stdout(Stdio::inherit())
132 .stderr(Stdio::inherit())
133 .spawn()
134 }
135
136 pub fn archive_connect(&self) -> Result<(AeronArchive, Aeron), io::Error> {
137 let start = Instant::now();
138 let deadline = valgrind_timeout(30);
139 while start.elapsed() < deadline {
140 if let Ok(aeron_context) = AeronContext::new() {
141 aeron_context.set_dir(&self.aeron_dir).expect("invalid dir");
142 aeron_context
143 .set_client_name(&CString::new("unit_test_client")?)
144 .expect("invalid client name");
145 if let Ok(aeron) = Aeron::new(&aeron_context) {
146 if aeron.start().is_ok() {
147 if let Ok(archive_context) = AeronArchiveContext::new() {
148 archive_context.set_aeron(&aeron).expect("invalid aeron");
149 archive_context
150 .set_control_request_channel(&self.control_request_channel.as_str().into_c_string())
151 .expect("invalid control request channel");
152 archive_context
153 .set_control_response_channel(&self.control_response_channel.as_str().into_c_string())
154 .expect("invalid control response channel");
155 archive_context
156 .set_recording_events_channel(&self.recording_events_channel.as_str().into_c_string())
157 .expect("invalid recording events channel");
158 if let Ok(connect) = AeronArchiveAsyncConnect::new_with_aeron(&archive_context, &aeron) {
159 if let Ok(archive) = connect.poll_blocking(valgrind_timeout(10)) {
160 let i = archive.get_archive_id();
161 assert!(i > 0);
162 info!("aeron archive media driver is up [connected with archive id {i}]");
163 sleep(Duration::from_millis(100));
164 return Ok((archive, aeron));
165 };
166 }
167 }
168 error!("aeron error: {}", Aeron::errmsg());
169 }
170 }
171 }
172 info!("waiting for aeron to start up, retrying...");
173 }
174
175 assert!(start.elapsed() < deadline, "failed to start up aeron media driver");
176
177 return Err(std::io::Error::other("unable to start up aeron media driver client"));
178 }
179
180 pub fn start(
220 aeron_dir: &str,
221 archive_dir: &str,
222 control_request_channel: &str,
223 control_response_channel: &str,
224 recording_events_channel: &str,
225 ) -> io::Result<Self> {
226 Self::clean_directory(aeron_dir)?;
227 Self::clean_directory(archive_dir)?;
228
229 fs::create_dir_all(aeron_dir)?;
231 fs::create_dir_all(archive_dir)?;
232
233 let binding = fs::read_dir(format!("{}/aeron/aeron-all/build/libs", env!("CARGO_MANIFEST_DIR")))?
234 .filter(|f| f.is_ok())
235 .map(|f| f.unwrap())
236 .filter(|f| f.file_name().to_string_lossy().to_string().ends_with(".jar"))
237 .next()
238 .unwrap()
239 .path();
240 let mut jar_path = binding.to_str().unwrap();
241 let agent_jar = jar_path.replace("aeron-all", "aeron-agent");
242
243 assert!(fs::exists(jar_path).unwrap_or_default());
244 let mut args = vec![];
245
246 if fs::exists(&agent_jar).unwrap_or_default() {
247 args.push(format!("-javaagent:{}", agent_jar));
248 }
249 let separator = if cfg!(target_os = "windows") { ";" } else { ":" };
250
251 let combined_jars = format!(
252 "{}{separator}{}",
253 jar_path,
254 jar_path.replace("aeron-all", "aeron-archive")
255 );
256 jar_path = &combined_jars;
257
258 args.push("--add-opens".to_string());
259 args.push("java.base/jdk.internal.misc=ALL-UNNAMED".to_string());
260 args.push("-cp".to_string());
261 args.push(jar_path.to_string());
262 args.push(format!("-Daeron.dir={}", aeron_dir));
263 args.push(format!("-Daeron.archive.dir={}", archive_dir));
264 args.push("-Daeron.spies.simulate.connection=true".to_string());
265 args.push("-Daeron.event.log=all".to_string());
266 args.push("-Daeron.event.log.disable=FRAME_IN,FRAME_OUT".to_string());
267 args.push("-Daeron.event.archive.log=all".to_string());
268 args.push("-Daeron.event.cluster.log=all".to_string());
269 args.push("-Dagrona.disable.bounds.checks=true".to_string());
270 args.push(format!("-Daeron.archive.control.channel={}", control_request_channel));
271 args.push(format!(
272 "-Daeron.archive.control.response.channel={}",
273 control_response_channel
274 ));
275 args.push(format!(
276 "-Daeron.archive.recording.events.channel={}",
277 recording_events_channel
278 ));
279 args.push("-Daeron.archive.replication.channel=aeron:udp?endpoint=localhost:0".to_string());
280 args.push("-Daeron.client.liveness.timeout=60000000000".to_string());
281 args.push("-Daeron.image.liveness.timeout=60000000000".to_string());
282 args.push("-Daeron.publication.unblock.timeout=65000000000".to_string());
283 args.push("io.aeron.archive.ArchivingMediaDriver".to_string());
284
285 info!("starting archive media driver [\n\tjava {}\n]", args.join(" "));
286
287 let child = Command::new("java")
288 .args(args)
289 .stdout(Stdio::inherit())
290 .stderr(Stdio::inherit())
291 .spawn()?;
292
293 info!(
294 "started archive media driver [{:?}",
295 fs::read_dir(aeron_dir)?.collect::<Vec<_>>()
296 );
297
298 Ok(EmbeddedArchiveMediaDriverProcess {
299 child,
300 aeron_dir: aeron_dir.into_c_string(),
301 archive_dir: archive_dir.into_c_string(),
302 control_request_channel: control_request_channel.to_string(),
303 control_response_channel: control_response_channel.to_string(),
304 recording_events_channel: recording_events_channel.to_string(),
305 })
306 }
307
308 fn clean_directory(dir: &str) -> io::Result<()> {
309 info!("cleaning directory {}", dir);
310 let path = Path::new(dir);
311 if path.exists() {
312 fs::remove_dir_all(path)?;
313 }
314 Ok(())
315 }
316
317 pub fn kill_all_java_processes() -> io::Result<ExitStatus> {
318 if cfg!(not(target_os = "windows")) {
319 return Ok(std::process::Command::new("pkill")
320 .args(["-9", "java"])
321 .stdout(Stdio::inherit())
322 .stderr(Stdio::inherit())
323 .spawn()?
324 .wait()?);
325 }
326 Ok(ExitStatus::default())
327 }
328}
329
330impl Drop for EmbeddedArchiveMediaDriverProcess {
332 fn drop(&mut self) {
333 warn!("WARN: stopping aeron archive media driver!!!!");
334 if let Err(e) = self.child.kill() {
336 error!("Failed to kill Java process: {}", e);
337 }
338
339 if let Err(e) = Self::clean_directory(&self.aeron_dir.to_str().unwrap()) {
341 error!("Failed to clean up Aeron directory: {}", e);
342 }
343 if let Err(e) = Self::clean_directory(&self.archive_dir.to_str().unwrap()) {
344 error!("Failed to clean up Archive directory: {}", e);
345 }
346 }
347}
348
349pub fn java_available() -> bool {
356 Command::new("java")
357 .arg("-version")
358 .stdout(Stdio::null())
359 .stderr(Stdio::null())
360 .status()
361 .is_ok()
362}
363
364#[macro_export]
368macro_rules! skip_unless_java {
369 () => {
370 if !$crate::testing::java_available() {
371 eprintln!("skipping: java not available on PATH");
372 return Ok(());
373 }
374 };
375}
376
377pub fn set_panic_hook() {
378 panic::set_hook(Box::new(|info| {
379 let backtrace = Backtrace::force_capture();
381 error!("Stack trace: {backtrace:#?}");
382
383 let backtrace = format!("{:?}", backtrace);
384 let re = Regex::new(r#"fn: "([^"]+)", file: "([^"]+)", line: (\d+)"#).unwrap();
386
387 for cap in re.captures_iter(&backtrace) {
389 let function = &cap[1];
390 let file = &cap[2];
391 let line = &cap[3];
392 info!("{file}:{line} in {function}");
393 }
394
395 error!("Panic occurred: {:#?}", info);
396
397 if let Some(payload) = info.payload().downcast_ref::<&str>() {
398 error!("Panic message: {}", payload);
399 } else if let Some(payload) = info.payload().downcast_ref::<String>() {
400 error!("Panic message: {}", payload);
401 } else {
402 error!("Panic with non-standard payload: {:?}", info.payload().type_id());
404 }
405
406 warn!("shutdown");
407
408 process::abort();
409 }))
410}
411
412pub fn find_unused_udp_port(start: u16) -> Option<u16> {
414 (start..65535).find(|p| std::net::UdpSocket::bind(("127.0.0.1", *p)).is_ok())
415}
416
417pub fn find_counter_id_by_session_blocking(
418 counters_reader: &crate::AeronCountersReader,
419 session_id: i32,
420 wait: Duration,
421) -> Result<i32, crate::AeronCError> {
422 let start = Instant::now();
423 loop {
424 let counter_id = crate::RecordingPos::find_counter_id_by_session(counters_reader, session_id);
425 if counter_id >= 0 {
426 return Ok(counter_id);
427 }
428 if start.elapsed() >= wait {
429 return Err(crate::AeronCError::from_code(-1));
430 }
431 std::thread::sleep(Duration::from_millis(10));
432 }
433}