use crate::IntoCString;
use crate::{Aeron, AeronArchive, AeronArchiveAsyncConnect, AeronArchiveContext, AeronContext};
use log::info;
use log::{error, warn};
use regex::Regex;
use std::backtrace::Backtrace;
use std::ffi::CString;
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::thread::sleep;
use std::time::{Duration, Instant};
use std::{fs, io, panic, process};
pub(crate) fn running_under_valgrind() -> bool {
std::env::var_os("RUSTERON_VALGRIND").is_some()
}
pub(crate) fn valgrind_timeout(base_secs: u64) -> Duration {
if running_under_valgrind() {
Duration::from_secs(base_secs.saturating_mul(3))
} else {
Duration::from_secs(base_secs)
}
}
pub struct EmbeddedArchiveMediaDriverProcess {
child: Child,
pub aeron_dir: CString,
pub archive_dir: CString,
pub control_request_channel: String,
pub control_response_channel: String,
pub recording_events_channel: String,
}
impl EmbeddedArchiveMediaDriverProcess {
pub fn build_and_start(
aeron_dir: &str,
archive_dir: &str,
control_request_channel: &str,
control_response_channel: &str,
recording_events_channel: &str,
) -> io::Result<Self> {
let path = std::path::MAIN_SEPARATOR;
let gradle = if cfg!(target_os = "windows") {
&format!("{}{path}aeron{path}gradlew.bat", env!("CARGO_MANIFEST_DIR"),)
} else {
"./gradlew"
};
let dir = format!("{}{path}aeron", env!("CARGO_MANIFEST_DIR"),);
info!("running {} in {}", gradle, dir);
if !Path::new(&format!(
"{}{path}aeron{path}aeron-all{path}build{path}libs",
env!("CARGO_MANIFEST_DIR")
))
.exists()
{
Command::new(&gradle)
.current_dir(dir)
.args([
":aeron-agent:jar",
":aeron-samples:jar",
":aeron-archive:jar",
":aeron-all:build",
])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?
.wait()?;
}
return Self::start(
&aeron_dir,
archive_dir,
control_request_channel,
control_response_channel,
recording_events_channel,
);
}
pub fn run_aeron_stats(&self) -> std::io::Result<Child> {
let main_dir = env!("CARGO_MANIFEST_DIR");
let dir = format!("{}/{}", main_dir, &self.aeron_dir.to_str().unwrap());
info!("running 'just aeron-stat {}'", dir);
Command::new("just")
.args(["aeron-stat", dir.as_str()])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
}
pub fn archive_connect(&self) -> Result<(AeronArchive, Aeron), io::Error> {
let start = Instant::now();
let deadline = valgrind_timeout(30);
while start.elapsed() < deadline {
if let Ok(aeron_context) = AeronContext::new() {
aeron_context.set_dir(&self.aeron_dir).expect("invalid dir");
aeron_context
.set_client_name(&CString::new("unit_test_client")?)
.expect("invalid client name");
if let Ok(aeron) = Aeron::new(&aeron_context) {
if aeron.start().is_ok() {
if let Ok(archive_context) = AeronArchiveContext::new() {
archive_context.set_aeron(&aeron).expect("invalid aeron");
archive_context
.set_control_request_channel(&self.control_request_channel.as_str().into_c_string())
.expect("invalid control request channel");
archive_context
.set_control_response_channel(&self.control_response_channel.as_str().into_c_string())
.expect("invalid control response channel");
archive_context
.set_recording_events_channel(&self.recording_events_channel.as_str().into_c_string())
.expect("invalid recording events channel");
if let Ok(connect) = AeronArchiveAsyncConnect::new_with_aeron(&archive_context, &aeron) {
if let Ok(archive) = connect.poll_blocking(valgrind_timeout(10)) {
let i = archive.get_archive_id();
assert!(i > 0);
info!("aeron archive media driver is up [connected with archive id {i}]");
sleep(Duration::from_millis(100));
return Ok((archive, aeron));
};
}
}
error!("aeron error: {}", Aeron::errmsg());
}
}
}
info!("waiting for aeron to start up, retrying...");
}
assert!(start.elapsed() < deadline, "failed to start up aeron media driver");
return Err(std::io::Error::other("unable to start up aeron media driver client"));
}
pub fn start(
aeron_dir: &str,
archive_dir: &str,
control_request_channel: &str,
control_response_channel: &str,
recording_events_channel: &str,
) -> io::Result<Self> {
Self::clean_directory(aeron_dir)?;
Self::clean_directory(archive_dir)?;
fs::create_dir_all(aeron_dir)?;
fs::create_dir_all(archive_dir)?;
let binding = fs::read_dir(format!("{}/aeron/aeron-all/build/libs", env!("CARGO_MANIFEST_DIR")))?
.filter(|f| f.is_ok())
.map(|f| f.unwrap())
.filter(|f| f.file_name().to_string_lossy().to_string().ends_with(".jar"))
.next()
.unwrap()
.path();
let mut jar_path = binding.to_str().unwrap();
let agent_jar = jar_path.replace("aeron-all", "aeron-agent");
assert!(fs::exists(jar_path).unwrap_or_default());
let mut args = vec![];
if fs::exists(&agent_jar).unwrap_or_default() {
args.push(format!("-javaagent:{}", agent_jar));
}
let separator = if cfg!(target_os = "windows") { ";" } else { ":" };
let combined_jars = format!(
"{}{separator}{}",
jar_path,
jar_path.replace("aeron-all", "aeron-archive")
);
jar_path = &combined_jars;
args.push("--add-opens".to_string());
args.push("java.base/jdk.internal.misc=ALL-UNNAMED".to_string());
args.push("-cp".to_string());
args.push(jar_path.to_string());
args.push(format!("-Daeron.dir={}", aeron_dir));
args.push(format!("-Daeron.archive.dir={}", archive_dir));
args.push("-Daeron.spies.simulate.connection=true".to_string());
args.push("-Daeron.event.log=all".to_string());
args.push("-Daeron.event.log.disable=FRAME_IN,FRAME_OUT".to_string());
args.push("-Daeron.event.archive.log=all".to_string());
args.push("-Daeron.event.cluster.log=all".to_string());
args.push("-Dagrona.disable.bounds.checks=true".to_string());
args.push(format!("-Daeron.archive.control.channel={}", control_request_channel));
args.push(format!(
"-Daeron.archive.control.response.channel={}",
control_response_channel
));
args.push(format!(
"-Daeron.archive.recording.events.channel={}",
recording_events_channel
));
args.push("-Daeron.archive.replication.channel=aeron:udp?endpoint=localhost:0".to_string());
args.push("-Daeron.client.liveness.timeout=60000000000".to_string());
args.push("-Daeron.image.liveness.timeout=60000000000".to_string());
args.push("-Daeron.publication.unblock.timeout=65000000000".to_string());
args.push("io.aeron.archive.ArchivingMediaDriver".to_string());
info!("starting archive media driver [\n\tjava {}\n]", args.join(" "));
let child = Command::new("java")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
info!(
"started archive media driver [{:?}",
fs::read_dir(aeron_dir)?.collect::<Vec<_>>()
);
Ok(EmbeddedArchiveMediaDriverProcess {
child,
aeron_dir: aeron_dir.into_c_string(),
archive_dir: archive_dir.into_c_string(),
control_request_channel: control_request_channel.to_string(),
control_response_channel: control_response_channel.to_string(),
recording_events_channel: recording_events_channel.to_string(),
})
}
fn clean_directory(dir: &str) -> io::Result<()> {
info!("cleaning directory {}", dir);
let path = Path::new(dir);
if path.exists() {
fs::remove_dir_all(path)?;
}
Ok(())
}
pub fn kill_all_java_processes() -> io::Result<ExitStatus> {
if cfg!(not(target_os = "windows")) {
return Ok(std::process::Command::new("pkill")
.args(["-9", "java"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?
.wait()?);
}
Ok(ExitStatus::default())
}
}
impl Drop for EmbeddedArchiveMediaDriverProcess {
fn drop(&mut self) {
warn!("WARN: stopping aeron archive media driver!!!!");
if let Err(e) = self.child.kill() {
error!("Failed to kill Java process: {}", e);
}
if let Err(e) = Self::clean_directory(&self.aeron_dir.to_str().unwrap()) {
error!("Failed to clean up Aeron directory: {}", e);
}
if let Err(e) = Self::clean_directory(&self.archive_dir.to_str().unwrap()) {
error!("Failed to clean up Archive directory: {}", e);
}
}
}
pub fn java_available() -> bool {
Command::new("java")
.arg("-version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
}
#[macro_export]
macro_rules! skip_unless_java {
() => {
if !$crate::testing::java_available() {
eprintln!("skipping: java not available on PATH");
return Ok(());
}
};
}
pub fn set_panic_hook() {
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::force_capture();
error!("Stack trace: {backtrace:#?}");
let backtrace = format!("{:?}", backtrace);
let re = Regex::new(r#"fn: "([^"]+)", file: "([^"]+)", line: (\d+)"#).unwrap();
for cap in re.captures_iter(&backtrace) {
let function = &cap[1];
let file = &cap[2];
let line = &cap[3];
info!("{file}:{line} in {function}");
}
error!("Panic occurred: {:#?}", info);
if let Some(payload) = info.payload().downcast_ref::<&str>() {
error!("Panic message: {}", payload);
} else if let Some(payload) = info.payload().downcast_ref::<String>() {
error!("Panic message: {}", payload);
} else {
error!("Panic with non-standard payload: {:?}", info.payload().type_id());
}
warn!("shutdown");
process::abort();
}))
}
pub fn find_unused_udp_port(start: u16) -> Option<u16> {
(start..65535).find(|p| std::net::UdpSocket::bind(("127.0.0.1", *p)).is_ok())
}
pub fn find_counter_id_by_session_blocking(
counters_reader: &crate::AeronCountersReader,
session_id: i32,
wait: Duration,
) -> Result<i32, crate::AeronCError> {
let start = Instant::now();
loop {
let counter_id = crate::RecordingPos::find_counter_id_by_session(counters_reader, session_id);
if counter_id >= 0 {
return Ok(counter_id);
}
if start.elapsed() >= wait {
return Err(crate::AeronCError::from_code(-1));
}
std::thread::sleep(Duration::from_millis(10));
}
}