use super::{Ancestor, SourceType};
const SHELLS: &[&str] = &["bash", "sh", "zsh", "fish", "tcsh", "dash", "ksh", "csh"];
const SUPERVISORS: &[(&str, SourceType)] = &[
("pm2", SourceType::Pm2),
("supervisord", SourceType::Supervisord),
("supervisor", SourceType::Supervisord),
("gunicorn", SourceType::Gunicorn),
("runsv", SourceType::Runit),
("runsvdir", SourceType::Runit),
("s6-svscan", SourceType::S6),
("s6-supervise", SourceType::S6),
];
const MULTIPLEXERS: &[(&str, SourceType)] = &[
("tmux", SourceType::Tmux),
("tmux: server", SourceType::Tmux),
("screen", SourceType::Screen),
("nohup", SourceType::Nohup),
];
const CRON_NAMES: &[&str] = &["cron", "crond", "anacron"];
pub fn detect_source(chain: &[Ancestor], cgroup: Option<&str>) -> SourceType {
if let Some(cg) = cgroup {
if cg.contains("/docker/")
|| cg.contains("/containerd/")
|| cg.contains("/kubepods/")
|| cg.contains("/podman-")
{
return SourceType::Docker;
}
}
if let Some(cg) = cgroup {
if cg.contains(".service") {
return SourceType::Systemd;
}
}
for ancestor in chain.iter().rev() {
let name_lower = ancestor.name.to_lowercase();
for (supervisor_name, source_type) in SUPERVISORS {
if name_lower == *supervisor_name {
return source_type.clone();
}
}
}
for ancestor in chain.iter().rev() {
let name_lower = ancestor.name.to_lowercase();
for (mux_name, source_type) in MULTIPLEXERS {
if name_lower == *mux_name || name_lower.starts_with(mux_name) {
return source_type.clone();
}
}
}
for ancestor in chain.iter().rev() {
let name_lower = ancestor.name.to_lowercase();
if CRON_NAMES.contains(&name_lower.as_str()) {
return SourceType::Cron;
}
}
if chain.len() >= 2 {
let direct_parent = &chain[1];
let parent_lower = direct_parent.name.to_lowercase();
if SHELLS.contains(&parent_lower.as_str()) {
return SourceType::Shell;
}
}
if let Some(root) = chain.last() {
if root.name == "launchd" {
return SourceType::Launchd;
}
}
SourceType::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
fn make_chain(names: &[(&str, u32)]) -> Vec<Ancestor> {
let mut chain = Vec::new();
for (i, (name, pid)) in names.iter().enumerate() {
let ppid = if i + 1 < names.len() {
names[i + 1].1
} else {
0
};
chain.push(Ancestor {
pid: *pid,
name: name.to_string(),
ppid,
});
}
chain
}
#[test]
fn test_systemd_via_cgroup() {
let chain = make_chain(&[("nginx", 500), ("bash", 100), ("systemd", 1)]);
let cgroup = "0::/system.slice/nginx.service\n";
assert_eq!(detect_source(&chain, Some(cgroup)), SourceType::Systemd);
}
#[test]
fn test_systemd_bash_node_not_shell() {
let chain = make_chain(&[("node", 500), ("bash", 100), ("systemd", 1)]);
let cgroup = "0::/system.slice/node-app.service\n";
assert_eq!(detect_source(&chain, Some(cgroup)), SourceType::Systemd);
}
#[test]
fn test_docker_via_cgroup() {
let chain = make_chain(&[("node", 500), ("containerd-shim", 100), ("systemd", 1)]);
let cgroup = "0::/docker/abc123def456\n";
assert_eq!(detect_source(&chain, Some(cgroup)), SourceType::Docker);
}
#[test]
fn test_docker_cgroup_beats_supervisor_name() {
let chain = make_chain(&[("gunicorn", 500), ("containerd-shim", 100), ("systemd", 1)]);
let cgroup = "0::/docker/abc123\n";
assert_eq!(detect_source(&chain, Some(cgroup)), SourceType::Docker);
}
#[test]
fn test_pm2_supervisor() {
let chain = make_chain(&[("node", 500), ("PM2", 100), ("systemd", 1)]);
assert_eq!(detect_source(&chain, None), SourceType::Pm2);
}
#[test]
fn test_supervisord() {
let chain = make_chain(&[("myapp", 500), ("supervisord", 100), ("systemd", 1)]);
assert_eq!(detect_source(&chain, None), SourceType::Supervisord);
}
#[test]
fn test_gunicorn() {
let chain = make_chain(&[
("worker", 501),
("gunicorn", 500),
("bash", 100),
("systemd", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::Gunicorn);
}
#[test]
fn test_tmux_multiplexer() {
let chain = make_chain(&[
("node", 500),
("bash", 300),
("tmux: server", 200),
("systemd", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::Tmux);
}
#[test]
fn test_screen_multiplexer() {
let chain = make_chain(&[
("python", 500),
("bash", 300),
("screen", 200),
("systemd", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::Screen);
}
#[test]
fn test_cron_detection() {
let chain = make_chain(&[
("backup.sh", 500),
("sh", 400),
("cron", 100),
("systemd", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::Cron);
}
#[test]
fn test_shell_direct_parent() {
let chain = make_chain(&[("node", 500), ("zsh", 100), ("init", 1)]);
assert_eq!(detect_source(&chain, None), SourceType::Shell);
}
#[test]
fn test_shell_not_detected_if_not_direct_parent() {
let chain = make_chain(&[("node", 500), ("wrapper", 200), ("bash", 100), ("init", 1)]);
assert_eq!(detect_source(&chain, None), SourceType::Unknown);
}
#[test]
fn test_launchd_fallback() {
let chain = make_chain(&[("node", 500), ("launchd", 1)]);
assert_eq!(detect_source(&chain, None), SourceType::Launchd);
}
#[test]
fn test_empty_chain() {
assert_eq!(detect_source(&[], None), SourceType::Unknown);
}
#[test]
fn test_single_entry_chain() {
let chain = make_chain(&[("node", 500)]);
assert_eq!(detect_source(&chain, None), SourceType::Unknown);
}
#[test]
fn test_runit_detection() {
let chain = make_chain(&[
("myapp", 500),
("runsv", 200),
("runsvdir", 100),
("init", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::Runit);
}
#[test]
fn test_s6_detection() {
let chain = make_chain(&[
("myapp", 500),
("s6-supervise", 200),
("s6-svscan", 100),
("init", 1),
]);
assert_eq!(detect_source(&chain, None), SourceType::S6);
}
#[test]
fn test_kubepods_cgroup() {
let chain = make_chain(&[("app", 500)]);
let cgroup = "0::/kubepods/burstable/pod123/container456\n";
assert_eq!(detect_source(&chain, Some(cgroup)), SourceType::Docker);
}
}