#[allow(
clippy::all,
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals
)]
mod bpf_skel;
pub mod cache;
pub mod cgroup;
pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
match errno {
libc::EPERM => Some("EPERM"),
libc::ENOENT => Some("ENOENT"),
libc::ESRCH => Some("ESRCH"),
libc::EINTR => Some("EINTR"),
libc::EIO => Some("EIO"),
libc::ENXIO => Some("ENXIO"),
libc::E2BIG => Some("E2BIG"),
libc::ENOEXEC => Some("ENOEXEC"),
libc::EBADF => Some("EBADF"),
libc::ECHILD => Some("ECHILD"),
libc::EAGAIN => Some("EAGAIN"),
libc::ENOMEM => Some("ENOMEM"),
libc::EACCES => Some("EACCES"),
libc::EFAULT => Some("EFAULT"),
libc::EBUSY => Some("EBUSY"),
libc::EEXIST => Some("EEXIST"),
libc::ENODEV => Some("ENODEV"),
libc::ENOTDIR => Some("ENOTDIR"),
libc::EISDIR => Some("EISDIR"),
libc::EINVAL => Some("EINVAL"),
libc::ENFILE => Some("ENFILE"),
libc::EMFILE => Some("EMFILE"),
libc::ENOSPC => Some("ENOSPC"),
libc::ESPIPE => Some("ESPIPE"),
libc::EROFS => Some("EROFS"),
libc::EPIPE => Some("EPIPE"),
libc::EDOM => Some("EDOM"),
libc::ERANGE => Some("ERANGE"),
libc::EDEADLK => Some("EDEADLK"),
libc::ENAMETOOLONG => Some("ENAMETOOLONG"),
libc::ENOSYS => Some("ENOSYS"),
libc::ENOTEMPTY => Some("ENOTEMPTY"),
libc::ELOOP => Some("ELOOP"),
libc::ENOTSUP => Some("ENOTSUP"),
libc::EADDRINUSE => Some("EADDRINUSE"),
libc::ECONNREFUSED => Some("ECONNREFUSED"),
libc::ETIMEDOUT => Some("ETIMEDOUT"),
_ => None,
}
}
pub(crate) fn read_kmsg() -> String {
match rmesg::log_entries(rmesg::Backend::Default, false) {
Ok(entries) => entries
.iter()
.map(|e| e.message.as_str())
.collect::<Vec<_>>()
.join("\n"),
Err(_) => String::new(),
}
}
pub mod assert;
pub(crate) mod budget;
#[cfg(feature = "cli")]
pub mod cli;
#[cfg(not(feature = "cli"))]
#[allow(dead_code)]
pub(crate) mod cli;
#[cfg(feature = "fetch")]
pub mod fetch;
pub mod kernel_path;
#[allow(dead_code)]
pub(crate) mod monitor;
#[allow(dead_code)]
pub(crate) mod probe;
#[cfg(feature = "cli")]
pub mod runner;
#[cfg(not(feature = "cli"))]
#[allow(dead_code)]
pub(crate) mod runner;
pub mod scenario;
#[allow(dead_code)]
pub(crate) mod stats;
pub mod test_support;
#[allow(dead_code)]
pub(crate) mod timeline;
pub mod topology;
#[cfg(feature = "gha-cache")]
pub mod remote_cache;
pub mod verifier;
#[allow(dead_code)]
pub(crate) mod vm;
#[allow(dead_code)]
pub(crate) mod vmm;
pub mod workload;
#[allow(dead_code)]
pub(crate) const BUSYBOX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/busybox"));
pub const GIT_HASH: &str = match option_env!("KTSTR_GIT_HASH") {
Some(v) => v,
None => env!("CARGO_PKG_VERSION"),
};
pub const GIT_FULL_HASH: &str = match option_env!("KTSTR_GIT_FULL_HASH") {
Some(v) => v,
None => env!("CARGO_PKG_VERSION"),
};
pub const GIT_BRANCH: &str = match option_env!("KTSTR_GIT_BRANCH") {
Some(v) => v,
None => "release",
};
pub const EMBEDDED_KCONFIG: &str = include_str!("../ktstr.kconfig");
pub fn kconfig_hash() -> String {
format!("{:08x}", crc32fast::hash(EMBEDDED_KCONFIG.as_bytes()))
}
pub fn cache_key_suffix() -> String {
let git = &GIT_FULL_HASH[..7.min(GIT_FULL_HASH.len())];
format!("{}-{git}", kconfig_hash())
}
pub use ktstr_macros::Scheduler;
pub use ktstr_macros::ktstr_test;
#[doc(hidden)]
pub use ctor as __ctor;
#[doc(hidden)]
pub use linkme as __linkme;
#[doc(hidden)]
pub use serde_json as __serde_json;
#[cfg(feature = "integration")]
pub use crate::probe::process::resolve_func_ip;
pub mod prelude {
pub use anyhow::Result;
pub use crate::Scheduler;
pub use crate::assert::{Assert, AssertResult};
pub use crate::cgroup::CgroupManager;
pub use crate::ktstr_test;
pub use crate::scenario::flags::FlagDecl;
pub use crate::scenario::ops::{
CgroupDef, CpusetSpec, HoldSpec, Op, Setup, Step, execute_defs, execute_steps,
execute_steps_with,
};
pub use crate::scenario::scenarios;
pub use crate::scenario::{CgroupGroup, Ctx, collect_all, spawn_diverse};
pub use crate::test_support::{BpfMapWrite, Scheduler, SchedulerSpec};
pub use crate::topology::{LlcInfo, TestTopology};
pub use crate::workload::{
AffinityKind, AffinityMode, Phase, SchedPolicy, Work, WorkType, WorkerReport,
WorkloadConfig, WorkloadHandle,
};
}
pub fn find_kernel() -> anyhow::Result<Option<std::path::PathBuf>> {
use kernel_path::KernelId;
let release = nix::sys::utsname::uname()
.ok()
.map(|u| u.release().to_string_lossy().into_owned());
let release_ref = release.as_deref();
let mut skip_cache_scan = false;
if let Some(val) = std::env::var("KTSTR_KERNEL")
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
{
match KernelId::parse(&val) {
KernelId::Path(_) => match kernel_path::find_image(Some(&val), release_ref) {
Some(p) => return Ok(Some(p)),
None => anyhow::bail!("KTSTR_KERNEL={val} does not contain a kernel image"),
},
KernelId::Version(ref ver) => {
let cache = cache::CacheDir::new().map_err(|e| {
anyhow::anyhow!(
"KTSTR_KERNEL={val} requires cache access, \
but cache directory could not be opened: {e}"
)
})?;
let arch = std::env::consts::ARCH;
let key = format!("{ver}-tarball-{arch}-kc{}", cache_key_suffix());
if let Some(entry) = cache.lookup(&key)
&& let Some(ref meta) = entry.metadata
{
return Ok(Some(entry.path.join(&meta.image_name)));
}
skip_cache_scan = true;
}
KernelId::CacheKey(ref key) => {
let cache = cache::CacheDir::new().map_err(|e| {
anyhow::anyhow!(
"KTSTR_KERNEL={val} requires cache access, \
but cache directory could not be opened: {e}"
)
})?;
if let Some(entry) = cache.lookup(key)
&& let Some(ref meta) = entry.metadata
{
return Ok(Some(entry.path.join(&meta.image_name)));
}
skip_cache_scan = true;
}
}
}
if !skip_cache_scan
&& let Ok(cache) = cache::CacheDir::new()
&& let Ok(entries) = cache.list()
{
let kc_hash = kconfig_hash();
for entry in &entries {
if let Some(ref meta) = entry.metadata {
if entry.has_stale_kconfig(&kc_hash) {
continue;
}
let image = entry.path.join(&meta.image_name);
if image.exists() {
return Ok(Some(image));
}
}
}
}
Ok(kernel_path::find_image(None, release_ref))
}
pub fn build_and_find_binary(package: &str) -> anyhow::Result<std::path::PathBuf> {
let output = std::process::Command::new("cargo")
.args(["build", "-p", package, "--message-format=json"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| anyhow::anyhow!("cargo build: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cargo build -p {package} failed:\n{stderr}");
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line)
&& msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact")
&& msg
.get("profile")
.and_then(|p| p.get("test"))
.and_then(|t| t.as_bool())
== Some(false)
&& msg
.get("target")
.and_then(|t| t.get("kind"))
.and_then(|k| k.as_array())
.is_some_and(|kinds| kinds.iter().any(|k| k.as_str() == Some("bin")))
&& let Some(filenames) = msg.get("filenames").and_then(|f| f.as_array())
&& let Some(path) = filenames.first().and_then(|f| f.as_str())
{
return Ok(std::path::PathBuf::from(path));
}
}
anyhow::bail!("no binary artifact found for package '{package}'")
}
pub(crate) fn resolve_current_exe() -> anyhow::Result<std::path::PathBuf> {
use anyhow::Context;
let exe = std::env::current_exe().context("resolve current exe")?;
if exe.exists() {
return Ok(exe);
}
let proc_exe = std::path::PathBuf::from("/proc/self/exe");
anyhow::ensure!(
proc_exe.exists(),
"current exe not found: {}",
exe.display()
);
Ok(proc_exe)
}
#[allow(clippy::too_many_arguments)]
pub fn run_shell(
kernel: std::path::PathBuf,
sockets: u32,
cores: u32,
threads: u32,
include_files: &[(&str, &std::path::Path)],
memory_mb: Option<u32>,
dmesg: bool,
exec: Option<&str>,
) -> anyhow::Result<()> {
let payload = resolve_current_exe()?;
let owned_includes: Vec<(String, std::path::PathBuf)> = include_files
.iter()
.map(|(a, p)| (a.to_string(), p.to_path_buf()))
.collect();
let mut cmdline = format!("KTSTR_MODE=shell KTSTR_TOPO={sockets},{cores},{threads}");
if dmesg {
cmdline.push_str(" loglevel=7");
}
if let Ok(val) = std::env::var("RUST_LOG") {
cmdline.push_str(&format!(" RUST_LOG={val}"));
}
if let Ok(term) = std::env::var("TERM") {
cmdline.push_str(&format!(" KTSTR_TERM={term}"));
}
if let Ok(ct) = std::env::var("COLORTERM") {
cmdline.push_str(&format!(" KTSTR_COLORTERM={ct}"));
}
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) == 0
&& ws.ws_col > 0
&& ws.ws_row > 0
{
cmdline.push_str(&format!(
" KTSTR_COLS={} KTSTR_ROWS={}",
ws.ws_col, ws.ws_row
));
}
}
let mut builder = vmm::KtstrVm::builder()
.kernel(&kernel)
.init_binary(&payload)
.topology(sockets, cores, threads)
.cmdline(&cmdline)
.include_files(owned_includes)
.busybox(true)
.dmesg(dmesg);
if let Some(cmd) = exec {
builder = builder.exec_cmd(cmd.to_string());
}
builder = match memory_mb {
Some(mb) => builder.memory_mb(mb),
None => builder.memory_deferred(),
};
let vm = builder.build()?;
vm.run_interactive()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_current_exe_happy_path() {
let exe = resolve_current_exe().unwrap();
let std_exe = std::env::current_exe().unwrap();
if std_exe.exists() {
assert_eq!(exe, std_exe);
} else {
assert_eq!(exe, std::path::PathBuf::from("/proc/self/exe"));
}
}
#[test]
fn errno_name_known_values() {
assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
assert_eq!(errno_name(libc::ENOENT), Some("ENOENT"));
assert_eq!(errno_name(libc::EINVAL), Some("EINVAL"));
assert_eq!(errno_name(libc::ENOMEM), Some("ENOMEM"));
assert_eq!(errno_name(libc::EBUSY), Some("EBUSY"));
assert_eq!(errno_name(libc::EACCES), Some("EACCES"));
assert_eq!(errno_name(libc::EAGAIN), Some("EAGAIN"));
assert_eq!(errno_name(libc::ENOSYS), Some("ENOSYS"));
assert_eq!(errno_name(libc::ETIMEDOUT), Some("ETIMEDOUT"));
}
#[test]
fn errno_name_unknown() {
assert_eq!(errno_name(9999), None);
assert_eq!(errno_name(0), None);
assert_eq!(errno_name(-1), None);
}
}