use std::{
path::{Path, PathBuf},
process::Stdio,
sync::{atomic::AtomicBool, Arc},
};
use cfg::{CliConfig, ResolvedConfig, RunCmdConfig, RunnerConfig};
use coverage::CoverageError;
use defmt_json_schema::v1::JsonFrame;
use path_clean::PathClean;
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter};
pub mod cfg;
pub mod collect;
pub mod coverage;
pub mod defmt;
pub mod path;
pub const DEFAULT_RTT_PORT: u16 = 19021;
pub const TIMEOUT_SEC: u64 = 15;
#[derive(Debug, thiserror::Error)]
pub enum RunnerError {
#[error("Timeout waiting for rtt connection to start.")]
RttTimeout,
#[error("Error from gdb: {}", .0)]
Gdb(String),
#[error("Error setting up the gdb script: {}", .0)]
GdbScript(String),
#[error("{}", .0)]
Config(#[from] cfg::ConfigError),
#[error("Error reading defmt logs: {}", .0)]
Defmt(String),
#[error("Failed setting up embedded-runner. Cause: {}", .0)]
Setup(String),
#[error("Failed executing pre runner. Cause: {}", .0)]
PreRunner(String),
#[error("Failed executing post runner. Cause: {}", .0)]
PostRunner(String),
#[error("Could not create coverage data. Cause: {}", .0)]
Coverage(CoverageError),
}
pub async fn run(cli_cfg: CliConfig) -> Result<(), RunnerError> {
let cfg = cfg::get_cfg(&cli_cfg)?;
match cli_cfg.cmd {
cfg::Cmd::Run(run_cfg) => run_cmd(&cfg, run_cfg).await,
cfg::Cmd::Collect(collect_cfg) => collect::run(collect_cfg).await,
}
}
pub async fn run_cmd(main_cfg: &ResolvedConfig, run_cfg: RunCmdConfig) -> Result<(), RunnerError> {
let output_dir = match run_cfg.output_dir {
Some(dir) => dir,
None => {
let mut dir = run_cfg.binary.clone();
dir.set_file_name(format!(
"{}_runner",
run_cfg
.binary
.file_name()
.expect("Binary name must be a valid filename.")
.to_string_lossy()
));
dir
}
};
if !output_dir.exists() {
tokio::fs::create_dir_all(&output_dir)
.await
.map_err(|err| {
RunnerError::Setup(format!(
"Could not create directory '{}'. Cause: {}",
output_dir.display(),
err
))
})?;
}
let log_filepath = output_dir.join("defmt.log");
let binary_str = run_cfg.binary.display().to_string();
let rel_binary_path = run_cfg
.binary
.strip_prefix(
crate::path::get_cargo_root().unwrap_or(std::env::current_dir().unwrap_or_default()),
)
.map(|p| p.to_path_buf())
.unwrap_or(run_cfg.binary.clone());
let rel_binary_str = rel_binary_path.display().to_string();
#[cfg(target_os = "windows")]
let pre_command = main_cfg
.runner_cfg
.pre_runner_windows
.as_ref()
.or(main_cfg.runner_cfg.pre_runner.as_ref());
#[cfg(not(target_os = "windows"))]
let pre_command = main_cfg.runner_cfg.pre_runner.as_ref();
if let Some(pre_command) = pre_command {
println!("--------------- Pre Runner --------------------");
let mut args = pre_command.args.clone();
args.push(binary_str.clone());
let output = tokio::process::Command::new(&pre_command.name)
.args(args)
.current_dir(&main_cfg.workspace_dir)
.output()
.await
.map_err(|err| RunnerError::PreRunner(err.to_string()))?;
print!(
"{}",
String::from_utf8(output.stdout).expect("Stdout must be valid utf8.")
);
eprint!(
"{}",
String::from_utf8(output.stderr).expect("Stderr must be valid utf8.")
);
if !output.status.success() {
return Err(RunnerError::PreRunner(format!(
"Returned with exit code: {}",
output.status,
)));
}
}
let gdb_script = main_cfg
.runner_cfg
.gdb_script(&run_cfg.binary, &output_dir)
.map_err(|_err| RunnerError::GdbScript(String::new()))?;
let gdb_script_file = output_dir.join("embedded.gdb");
tokio::fs::write(&gdb_script_file, gdb_script)
.await
.map_err(|err| RunnerError::GdbScript(err.to_string()))?;
let (defmt_frames, gdb_result) = run_gdb_sequence(
run_cfg.binary,
&main_cfg.workspace_dir,
&gdb_script_file,
&main_cfg.runner_cfg,
)
.await?;
let gdb_status = gdb_result?;
if !gdb_status.success() {
return Err(RunnerError::Gdb(format!(
"GDB did not run successfully. Exit code: '{gdb_status}'"
)));
}
println!("--------------- Logs --------------------");
let log_file = tokio::fs::File::create(&log_filepath)
.await
.map_err(|err| {
RunnerError::Setup(format!(
"Could not create file '{}'. Cause: {}",
log_filepath.display(),
err
))
})?;
let mut writer = BufWriter::new(log_file);
for frame in &defmt_frames {
let _ = writer
.write_all(
serde_json::to_string(frame)
.expect("DefmtFrame is valid JSON.")
.as_bytes(),
)
.await;
let _ = writer.write_all("\n".as_bytes()).await;
let location = if frame.location.file.is_some()
&& frame.location.line.is_some()
&& frame.location.module_path.is_some()
{
let mod_path = frame.location.module_path.as_ref().unwrap();
format!(
"{}:{} in {}::{}::{}",
frame.location.file.as_ref().unwrap(),
frame.location.line.unwrap(),
mod_path.crate_name,
mod_path.modules.join("::"),
mod_path.function,
)
} else {
"no-location".to_string()
};
match frame.level {
Some(level) => log::log!(level, "{}\n@{}", frame.data, location),
None => println!("{}\n@{}", frame.data, location),
}
}
println!("------------------ Output ---------------");
println!("Logs written to '{}'.", log_filepath.display());
let run_name = run_cfg
.run_name
.unwrap_or(rel_binary_path.display().to_string());
let meta_path = run_cfg
.meta_filepath
.unwrap_or(main_cfg.embedded_dir.join("meta.json"));
let meta = if meta_path.exists() {
let meta_content = tokio::fs::read_to_string(&meta_path).await.map_err(|err| {
RunnerError::Setup(format!(
"Could not read metadata '{}'. Cause: {}",
meta_path.display(),
err
))
})?;
let mut meta: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&meta_content).map_err(|err| {
RunnerError::Setup(format!(
"Could not deserialize metadata '{}'. Cause: {}",
meta_path.display(),
err
))
})?;
meta.insert(
"binary".to_string(),
serde_json::Value::String(rel_binary_str),
);
serde_json::Value::Object(meta)
} else {
json!({
"binary": rel_binary_str
})
};
let logs = serde_json::to_string(&defmt_frames).expect("DefmtFrames were deserialized before.");
let coverage =
coverage::coverage_from_defmt_frames(run_name, Some(meta), &defmt_frames, Some(logs))
.map_err(RunnerError::Coverage)?;
let coverage_file = output_dir.join("coverage.json");
tokio::fs::write(
&coverage_file,
serde_json::to_string(&coverage).expect("Coverage schema is valid JSON."),
)
.await
.map_err(|err| {
RunnerError::Setup(format!(
"Could not write to file '{}'. Cause: {}",
coverage_file.display(),
err
))
})?;
println!("Coverage written to '{}'.", coverage_file.display());
let coverages_filepath = coverage::coverages_filepath();
if !coverages_filepath.exists() {
let _ = tokio::fs::write(coverages_filepath, coverage_file.display().to_string()).await;
} else {
let mut file = tokio::fs::OpenOptions::new()
.append(true)
.read(true)
.open(coverages_filepath)
.await
.expect("Coverages file exists.");
let mut content = String::new();
file.read_to_string(&mut content)
.await
.expect("Reading coverages");
let mut exists = false;
for line in content.lines() {
if line == coverage_file.display().to_string() {
exists = true;
break;
}
}
if !exists {
let _ = file.write_all("\n".as_bytes()).await;
let _ = file
.write_all(coverage_file.display().to_string().as_bytes())
.await;
}
}
#[cfg(target_os = "windows")]
let post_command = main_cfg
.runner_cfg
.post_runner_windows
.as_ref()
.or(main_cfg.runner_cfg.post_runner.as_ref());
#[cfg(not(target_os = "windows"))]
let post_command = main_cfg.runner_cfg.post_runner.as_ref();
if let Some(post_command) = post_command {
println!("--------------- Post Runner --------------------");
let mut args = post_command.args.clone();
args.push(binary_str);
let output = tokio::process::Command::new(&post_command.name)
.args(args)
.current_dir(&main_cfg.workspace_dir)
.output()
.await
.map_err(|err| RunnerError::PostRunner(err.to_string()))?;
print!(
"{}",
String::from_utf8(output.stdout).expect("Stdout must be valid utf8.")
);
eprint!(
"{}",
String::from_utf8(output.stderr).expect("Stderr must be valid utf8.")
);
if !output.status.success() {
return Err(RunnerError::PostRunner(format!(
"Returned with exit code: {}",
output.status,
)));
}
}
Ok(())
}
pub async fn run_gdb_sequence(
binary: PathBuf,
workspace_dir: &Path,
tmp_gdb_file: &Path,
runner_cfg: &RunnerConfig,
) -> Result<
(
Vec<JsonFrame>,
Result<std::process::ExitStatus, RunnerError>,
),
RunnerError,
> {
let mut gdb_cmd = tokio::process::Command::new("arm-none-eabi-gdb");
let mut gdb = gdb_cmd
.args([
"-x",
&tmp_gdb_file.to_string_lossy(),
&binary.to_string_lossy(),
])
.current_dir(workspace_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut open_ocd_output = gdb.stderr.take().unwrap();
let mut buf = [0; 100];
let mut content = Vec::new();
let rtt_start = b"for rtt connection";
let mut rtt_found = false;
println!("--------------- OpenOCD --------------------");
'outer: while let Ok(Ok(n)) = tokio::time::timeout(
std::time::Duration::from_secs(TIMEOUT_SEC),
open_ocd_output.read(&mut buf),
)
.await
{
if n > 0 {
content.extend_from_slice(&buf[..n]);
print!("{}", String::from_utf8_lossy(&buf[..n]));
for i in 0..n {
let slice_end = content.len().saturating_sub(i);
if content[..slice_end].ends_with(rtt_start) {
rtt_found = true;
break 'outer;
}
}
}
}
if !rtt_found {
log::error!("Timeout while waiting for rtt connection.");
let _ = gdb.kill().await;
return Err(RunnerError::RttTimeout);
}
println!();
let end_signal = Arc::new(AtomicBool::new(false));
let thread_signal = end_signal.clone();
let rtt_port = runner_cfg.rtt_port.unwrap_or(DEFAULT_RTT_PORT);
let workspace_root = workspace_dir.to_path_buf();
let defmt_thread = tokio::spawn(async move {
defmt::read_defmt_frames(&binary, &workspace_root, rtt_port, thread_signal)
});
let gdb_result =
match tokio::time::timeout(std::time::Duration::from_secs(TIMEOUT_SEC), gdb.wait()).await {
Ok(Ok(status)) => Ok(status),
Ok(Err(err)) => Err(RunnerError::Gdb(format!(
"Error waiting for gdb to finish. Cause: {err}"
))),
Err(_) => {
log::error!("Timeout while waiting for gdb to end.");
let _ = gdb.kill().await;
return Err(RunnerError::RttTimeout);
}
};
end_signal.store(true, std::sync::atomic::Ordering::Relaxed);
let defmt_result = defmt_thread
.await
.map_err(|_| RunnerError::Defmt("Failed waiting for defmt logs.".to_string()))?;
let defmt_frames = defmt_result
.map_err(|err| RunnerError::Defmt(format!("Failed extracting defmt logs. Cause: {err}")))?;
Ok((defmt_frames, gdb_result))
}
pub fn absolute_path(path: &Path) -> std::io::Result<PathBuf> {
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
crate::path::get_cargo_root()
.or_else(|_| std::env::current_dir())?
.join(path)
}
.clean();
Ok(absolute_path)
}