Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/cli/src/state/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ impl StateFile {
// start_enter takeover means we can't waitpid the VM, so liveness
// polling alone would otherwise always yield exit 0.
if record.exit_code.is_none() {
if let Ok(contents) = std::fs::read_to_string(
record.box_dir.join("upper").join(".a3s_exit_code"),
) {
if let Ok(contents) =
std::fs::read_to_string(record.box_dir.join("upper").join(".a3s_exit_code"))
{
if let Ok(code) = contents.trim().parse::<i32>() {
record.exit_code = Some(code);
}
Expand Down
74 changes: 58 additions & 16 deletions src/core/src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ pub fn is_runtime_console_noise(line: &str) -> bool {
/// reads. Returns `None` only when `stop` is set AND EOF is reached — i.e. the
/// VM has exited and `console.log` is fully drained — flushing any final partial
/// line as the last value before the subsequent `None`.
fn tail_next_line(reader: &mut impl BufRead, buf: &mut String, stop: &AtomicBool) -> Option<String> {
fn tail_next_line(
reader: &mut impl BufRead,
buf: &mut String,
stop: &AtomicBool,
) -> Option<String> {
loop {
match reader.read_line(buf) {
Ok(0) | Err(_) => {
Expand All @@ -190,7 +194,7 @@ fn tail_next_line(reader: &mut impl BufRead, buf: &mut String, stop: &AtomicBool
return None;
}
let line = std::mem::take(buf);
return Some(line.trim_end_matches(|c| c == '\n' || c == '\r').to_string());
return Some(line.trim_end_matches(['\n', '\r']).to_string());
}
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
Expand All @@ -202,20 +206,29 @@ fn tail_next_line(reader: &mut impl BufRead, buf: &mut String, stop: &AtomicBool
continue;
}
let line = std::mem::take(buf);
return Some(line.trim_end_matches(|c| c == '\n' || c == '\r').to_string());
return Some(line.trim_end_matches(['\n', '\r']).to_string());
}
}

/// Run the log processor for a box, blocking until `stop` is set and the console
/// is drained. Intended to run on a dedicated thread for the VM's lifetime; set
/// `stop` after the VM exits, then join, to guarantee the final lines are
/// captured (no teardown race).
pub fn run_log_processor(console_log: &Path, log_dir: &Path, config: &LogConfig, stop: &AtomicBool) {
pub fn run_log_processor(
console_log: &Path,
log_dir: &Path,
config: &LogConfig,
stop: &AtomicBool,
) {
match config.driver {
LogDriver::None => {}
LogDriver::JsonFile => {
run_json_file_processor(console_log, log_dir, config.max_size(), config.max_file(), stop)
}
LogDriver::JsonFile => run_json_file_processor(
console_log,
log_dir,
config.max_size(),
config.max_file(),
stop,
),
LogDriver::Syslog => run_syslog_processor(
console_log,
config.syslog_address(),
Expand Down Expand Up @@ -383,9 +396,18 @@ struct RotatingWriter {

impl RotatingWriter {
fn new(path: &Path, max_size: u64, max_file: u32) -> std::io::Result<Self> {
let file = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let written = file.metadata()?.len();
Ok(Self { path: path.to_path_buf(), file, written, max_size, max_file })
Ok(Self {
path: path.to_path_buf(),
file,
written,
max_size,
max_file,
})
}

fn write_line(&mut self, line: &str) -> std::io::Result<()> {
Expand Down Expand Up @@ -414,7 +436,10 @@ impl RotatingWriter {
let rotated = rotated_path(&self.path, 1);
compress_file(&self.path, &rotated)?;
std::fs::remove_file(&self.path)?;
self.file = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
self.file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
self.written = 0;
Ok(())
}
Expand Down Expand Up @@ -558,8 +583,14 @@ mod tests {
let mut reader = BufReader::new(Cursor::new(b"alpha\r\nbeta\n".to_vec()));
let mut buf = String::new();
let stop = AtomicBool::new(true);
assert_eq!(tail_next_line(&mut reader, &mut buf, &stop), Some("alpha".to_string()));
assert_eq!(tail_next_line(&mut reader, &mut buf, &stop), Some("beta".to_string()));
assert_eq!(
tail_next_line(&mut reader, &mut buf, &stop),
Some("alpha".to_string())
);
assert_eq!(
tail_next_line(&mut reader, &mut buf, &stop),
Some("beta".to_string())
);
assert_eq!(tail_next_line(&mut reader, &mut buf, &stop), None);
assert!(buf.is_empty());
}
Expand All @@ -583,7 +614,9 @@ mod tests {
fn test_is_runtime_console_noise() {
assert!(is_runtime_console_noise("init.krun: mount_filesystems ok"));
assert!(!is_runtime_console_noise("L1"));
assert!(!is_runtime_console_noise("starting app (init.krun: ignored)"));
assert!(!is_runtime_console_noise(
"starting app (init.krun: ignored)"
));
assert!(!is_runtime_console_noise(""));
}

Expand All @@ -599,8 +632,14 @@ mod tests {
run_json_file_processor(&console, dir.path(), 10 * 1024 * 1024, 3, &stop);
let json = std::fs::read_to_string(json_log_path(dir.path())).unwrap();
assert!(json.contains("\"log\":\"AAA\\n\""), "AAA missing: {json}");
assert!(json.contains("\"log\":\"BBB\\n\""), "BBB (after a quiet line) missing: {json}");
assert!(!json.contains("init.krun"), "runtime noise must be filtered: {json}");
assert!(
json.contains("\"log\":\"BBB\\n\""),
"BBB (after a quiet line) missing: {json}"
);
assert!(
!json.contains("init.krun"),
"runtime noise must be filtered: {json}"
);
}

#[test]
Expand All @@ -611,6 +650,9 @@ mod tests {
for i in 0..10 {
w.write_line(&format!("line-{i}")).unwrap();
}
assert!(rotated_path(&path, 1).exists(), "expected a rotated .1.gz file");
assert!(
rotated_path(&path, 1).exists(),
"expected a rotated .1.gz file"
);
}
}
11 changes: 8 additions & 3 deletions src/cri/src/runtime_service/service_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,10 @@ mod cleanup_tests {
61 30 0:47 / /root/.a3s/images/cri-container-rootfs/sb/ctr/rootfs/data/deep rw - ext4 /dev/sda1 rw
62 30 0:48 / /root/.a3s/images/cri-container-rootfs/other/x rw - ext4 /dev/sda1 rw
";
let got =
submounts_under(mountinfo, "/root/.a3s/images/cri-container-rootfs/sb/ctr/rootfs");
let got = submounts_under(
mountinfo,
"/root/.a3s/images/cri-container-rootfs/sb/ctr/rootfs",
);
assert_eq!(
got,
vec![
Expand All @@ -505,6 +507,9 @@ mod cleanup_tests {
1 2 0:1 / /root/xy rw - ext4 d rw
";
// Exact root match included; a sibling sharing the string prefix is not.
assert_eq!(submounts_under(mountinfo, "/root/x"), vec!["/root/x".to_string()]);
assert_eq!(
submounts_under(mountinfo, "/root/x"),
vec!["/root/x".to_string()]
);
}
}
66 changes: 33 additions & 33 deletions src/cri/src/runtime_service/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,39 +321,6 @@ pub(super) fn metric_descriptors() -> Vec<MetricDescriptor> {
]
}

#[cfg(test)]
mod stats_tests {
use super::*;

#[test]
fn test_per_container_split_sums_to_total() {
let total = VmUsage {
cpu_core_nanos: 900,
memory_bytes: 300,
};
let per = total.per_container(3);
assert_eq!(per.cpu_core_nanos, 300);
assert_eq!(per.memory_bytes, 100);
// 0 or 1 container → the full usage (no divide-by-zero, single-container
// pods get the whole VM's usage).
assert_eq!(total.per_container(0).memory_bytes, 300);
assert_eq!(total.per_container(1).cpu_core_nanos, 900);
}

#[cfg(target_os = "linux")]
#[test]
fn test_read_vm_usage_reports_real_memory_for_self() {
// Reading the test process's own procfs must yield a non-zero RSS (CPU
// may legitimately round to 0 immediately after start, so only memory
// is asserted).
let usage = read_vm_usage(std::process::id());
assert!(
usage.memory_bytes > 0,
"expected a non-zero RSS reading the test process's own /proc"
);
}
}

fn pod_sandbox_metric_labels(sandbox: &PodSandbox) -> HashMap<String, String> {
HashMap::from([
("pod_sandbox_id".to_string(), sandbox.id.clone()),
Expand Down Expand Up @@ -432,3 +399,36 @@ pub(super) fn pod_sandbox_metrics(
],
}
}

#[cfg(test)]
mod stats_tests {
use super::*;

#[test]
fn test_per_container_split_sums_to_total() {
let total = VmUsage {
cpu_core_nanos: 900,
memory_bytes: 300,
};
let per = total.per_container(3);
assert_eq!(per.cpu_core_nanos, 300);
assert_eq!(per.memory_bytes, 100);
// 0 or 1 container → the full usage (no divide-by-zero, single-container
// pods get the whole VM's usage).
assert_eq!(total.per_container(0).memory_bytes, 300);
assert_eq!(total.per_container(1).cpu_core_nanos, 900);
}

#[cfg(target_os = "linux")]
#[test]
fn test_read_vm_usage_reports_real_memory_for_self() {
// Reading the test process's own procfs must yield a non-zero RSS (CPU
// may legitimately round to 0 immediately after start, so only memory
// is asserted).
let usage = read_vm_usage(std::process::id());
assert!(
usage.memory_bytes > 0,
"expected a non-zero RSS reading the test process's own /proc"
);
}
}
32 changes: 16 additions & 16 deletions src/guest/init/src/cgroup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,6 @@ fn shares_to_weight(shares: u64) -> u64 {
(1 + ((shares - 2) * 9999) / 262_142).clamp(1, 10_000)
}

#[cfg(test)]
mod tests {
use super::shares_to_weight;

#[test]
fn test_shares_to_weight_mapping() {
// Endpoints + the cgroup v1 default map to the runc-equivalent weights.
assert_eq!(shares_to_weight(2), 1);
assert_eq!(shares_to_weight(262_144), 10_000);
assert_eq!(shares_to_weight(1024), 39); // runc's mapping for the default
// Out-of-range inputs are clamped, never panic / overflow.
assert_eq!(shares_to_weight(0), 1);
assert_eq!(shares_to_weight(u64::MAX), 10_000);
}
}

/// A per-container cgroup v2 (memory + cpu limits). Dropping it removes the
/// cgroup directory.
pub struct ContainerCgroup {
Expand Down Expand Up @@ -202,3 +186,19 @@ impl Drop for ContainerCgroup {
}
}
}

#[cfg(test)]
mod tests {
use super::shares_to_weight;

#[test]
fn test_shares_to_weight_mapping() {
// Endpoints + the cgroup v1 default map to the runc-equivalent weights.
assert_eq!(shares_to_weight(2), 1);
assert_eq!(shares_to_weight(262_144), 10_000);
assert_eq!(shares_to_weight(1024), 39); // runc's mapping for the default
// Out-of-range inputs are clamped, never panic / overflow.
assert_eq!(shares_to_weight(0), 1);
assert_eq!(shares_to_weight(u64::MAX), 10_000);
}
}
4 changes: 2 additions & 2 deletions src/guest/init/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,8 +764,8 @@ fn mount_user_volumes() -> Result<(), Box<dyn std::error::Error>> {
let guest_path = parts[1];
// Flags after the guest path may appear in any order: "ro", "file".
// The host decides "file" (it can stat the source); the guest obeys.
let read_only = parts[2..].iter().any(|&m| m == "ro");
let is_file = parts[2..].iter().any(|&m| m == "file");
let read_only = parts[2..].contains(&"ro");
let is_file = parts[2..].contains(&"file");

let flags = if read_only {
MsFlags::MS_RDONLY
Expand Down
3 changes: 1 addition & 2 deletions src/runtime/src/oci/build/engine/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,7 @@ pub(super) fn handle_run(
}

let layer_path = layers_dir.join(format!("layer_{}.tar.gz", layer_index));
let layer_info =
create_layer_with_deletions(rootfs_dir, &changed, &deleted, &layer_path)?;
let layer_info = create_layer_with_deletions(rootfs_dir, &changed, &deleted, &layer_path)?;
Ok(Some(layer_info))
}

Expand Down
10 changes: 8 additions & 2 deletions src/runtime/src/oci/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ pub fn extract_layer(layer_path: &Path, target_dir: &Path) -> Result<()> {
))
})?;
file.seek(SeekFrom::Start(0)).map_err(|e| {
BoxError::OciImageError(format!("Failed to rewind layer {}: {e}", layer_path.display()))
BoxError::OciImageError(format!(
"Failed to rewind layer {}: {e}",
layer_path.display()
))
})?;

let decoder: Box<dyn Read> = if read >= 2 && magic[0] == 0x1f && magic[1] == 0x8b {
Expand Down Expand Up @@ -410,6 +413,9 @@ mod tests {
write_test_tar(File::create(&layer_path).unwrap(), &[("p.txt", b"plain")]);

extract_layer(&layer_path, &target_dir).unwrap();
assert_eq!(fs::read_to_string(target_dir.join("p.txt")).unwrap(), "plain");
assert_eq!(
fs::read_to_string(target_dir.join("p.txt")).unwrap(),
"plain"
);
}
}
10 changes: 8 additions & 2 deletions src/shim/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,10 @@ unsafe fn configure_and_start_vm(spec: &InstanceSpec) -> Result<()> {
use std::os::unix::io::AsRawFd;
let err_path = console_path.with_file_name("console.err.log");
let open = |p: &std::path::Path| {
std::fs::OpenOptions::new().create(true).append(true).open(p)
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(p)
};
if let (Ok(out_f), Ok(err_f)) = (open(console_path), open(&err_path)) {
ctx.add_split_console(-1, out_f.as_raw_fd(), err_f.as_raw_fd())?;
Expand All @@ -781,7 +784,10 @@ unsafe fn configure_and_start_vm(spec: &InstanceSpec) -> Result<()> {
}
}
if !split_done {
tracing::debug!(console_path = console_str, "Redirecting console output (merged)");
tracing::debug!(
console_path = console_str,
"Redirecting console output (merged)"
);
ctx.set_console_output(console_str)?;
}
}
Expand Down
Loading