use common::{Limit, Result};
use rlm_core::CgroupManager;
use std::os::unix::process::ExitStatusExt;
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub struct RunOutcome {
pub exit: u8,
pub cgroup: String,
pub left_running: usize,
}
pub fn run_in_cgroup(
manager: &CgroupManager,
limit: &Limit,
command: &[String],
) -> Result<RunOutcome> {
let (program, args) = command
.split_first()
.ok_or_else(|| common::Error::InvalidArgs("command is required".into()))?;
let uniq = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let name = format!("run-{}-{}", std::process::id(), uniq);
let prepared = manager.prepare_cgroup(&name, limit)?;
for w in &prepared.warnings {
eprintln!("warning: {w}");
}
let cgroup_path = prepared.path;
let terminated = Arc::new(AtomicBool::new(false));
let terminated_clone = Arc::clone(&terminated);
ctrlc::set_handler(move || {
terminated_clone.store(true, Ordering::SeqCst);
})
.ok();
let mut cmd = manager.placement_command(&cgroup_path, program);
cmd.args(args);
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
let _ = manager.remove_if_empty(&name);
return Err(e.into());
}
};
let pid = child.id();
if let Err(e) = manager.add_to_cgroup(&cgroup_path, pid) {
eprintln!("warning: failed to apply limits: {e}");
}
let mut sigterm_sent = false;
let status = loop {
if terminated.load(Ordering::SeqCst) && !sigterm_sent {
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
sigterm_sent = true;
}
match child.try_wait()? {
Some(status) => break status,
None => std::thread::sleep(std::time::Duration::from_millis(100)),
}
};
let oom = manager.oom_kills(&name).unwrap_or(0);
let (exit, messages) = rlm_core::exit::exit_report(
status.code(),
status.signal(),
oom,
manager.memory_max(&name),
);
for m in &messages {
eprintln!("rlm: {m}");
}
let left_running = if manager.is_populated(&name) == Some(true) {
let n = manager.pids_in_cgroup(&name).len();
eprintln!(
"rlm: the command exited but {n} process(es) it started are still running in cgroup '{name}'. \
Their limits stay in place. Remove them with: rlm unlimit --cgroup {name}"
);
n
} else {
if let Err(e) = manager.remove_if_empty(&name) {
eprintln!("warning: failed to remove cgroup '{name}': {e}");
}
0
};
Ok(RunOutcome {
exit,
cgroup: name,
left_running,
})
}
pub fn run_with_limits(
manager: &CgroupManager,
limit: &Limit,
command: &[String],
) -> Result<ExitCode> {
let outcome = run_in_cgroup(manager, limit, command)?;
tracing::debug!(
cgroup = %outcome.cgroup,
left_running = outcome.left_running,
exit = outcome.exit,
"run finished"
);
Ok(ExitCode::from(outcome.exit))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires cgroup v2 delegation; run manually"]
fn launcher_that_exits_leaves_limited_children_in_place() {
let manager = CgroupManager::new().unwrap();
let limit = common::build_limit(Some("64M"), None, None, None).unwrap();
let cmd: Vec<String> = ["sh", "-c", "sleep 30 & exit 0"]
.iter()
.map(|s| s.to_string())
.collect();
let out = run_in_cgroup(&manager, &limit, &cmd).unwrap();
let pids = manager.pids_in_cgroup(&out.cgroup);
for pid in &pids {
unsafe { libc::kill(*pid as i32, libc::SIGKILL) };
}
std::thread::sleep(std::time::Duration::from_millis(200));
let _ = manager.remove_if_empty(&out.cgroup);
assert_eq!(out.exit, 0);
assert_eq!(out.left_running, 1, "the background sleep keeps its limits");
}
}