kill_tree 0.2.2

🌳 Kill Tree is a library designed to terminate a specified process and all its child processes recursively, operating independently of other commands like kill or taskkill.
Documentation
use std::{process::Command, sync::mpsc, thread, time::Duration};
use tracing_test::traced_test;

fn get_node_script_infinite() -> String {
    r"
    // console.log('infinite. pid:', process.pid);
    setInterval(() => {}, 1000);
    "
    .to_string()
}

fn get_node_script_spawn_infinite_child() -> String {
    r#"
    // console.log('spawn child. pid:', process.pid);
    const { spawn } = require('child_process');
    // const child = spawn('node', ['-e', 'console.log("infinite. pid:", process.pid);setInterval(() => {}, 1000);'], {
    const child = spawn('node', ['-e', 'setInterval(() => {}, 1000);'], {
        stdio: 'inherit',
    });
    // child.on('exit', (code, signal) => {
    //     console.log('child process exited with ' +
    //                 `code ${code} and signal ${signal}`);
    // });
    setInterval(() => {}, 1000);
    "#
    .to_string()
}

#[traced_test]
#[test]
fn kill_tree_default() {
    let (tx, rx) = mpsc::channel();
    let thread = thread::spawn(move || {
        let mut child = Command::new("node")
            .arg("-e")
            .arg(get_node_script_infinite())
            .spawn()
            .unwrap();
        let target_process_id = child.id();
        thread::sleep(Duration::from_secs(1));
        tx.send(target_process_id).unwrap();
        let _ = child.wait();
    });
    let target_process_id = rx.recv().unwrap();
    let outputs = kill_tree::blocking::kill_tree(target_process_id).expect("Failed to kill");
    println!("{outputs:?}");
    assert_eq!(outputs.len(), 1);
    let output = &outputs[0];
    match output {
        kill_tree::Output::Killed {
            process_id,
            parent_process_id,
            name,
        } => {
            assert_eq!(*process_id, target_process_id);
            assert_eq!(*parent_process_id, std::process::id());
            // There are cases where the process does not start with node, so a log is left for confirmation.
            println!("name: {name}");
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    thread.join().unwrap();
}

#[test]
fn kill_tree_with_config_sigkill() {
    let (tx, rx) = mpsc::channel();
    let thread = thread::spawn(move || {
        let mut child = Command::new("node")
            .arg("-e")
            .arg(get_node_script_infinite())
            .spawn()
            .unwrap();
        let target_process_id = child.id();
        thread::sleep(Duration::from_secs(1));
        tx.send(target_process_id).unwrap();
        let _ = child.wait();
    });
    let target_process_id = rx.recv().unwrap();
    let config = kill_tree::Config {
        signal: String::from("SIGKILL"),
        ..Default::default()
    };
    let outputs = kill_tree::blocking::kill_tree_with_config(target_process_id, &config)
        .expect("Failed to kill");
    println!("{outputs:?}");
    assert_eq!(outputs.len(), 1);
    let output = &outputs[0];
    match output {
        kill_tree::Output::Killed {
            process_id,
            parent_process_id,
            name,
        } => {
            assert_eq!(*process_id, target_process_id);
            assert_eq!(*parent_process_id, std::process::id());
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    thread.join().unwrap();
}

#[traced_test]
#[test]
fn kill_tree_with_config_include_target_false() {
    let (tx, rx) = mpsc::channel();
    let thread = thread::spawn(move || {
        let mut child = Command::new("node")
            .arg("-e")
            .arg(get_node_script_spawn_infinite_child())
            .spawn()
            .unwrap();
        let target_process_id = child.id();
        thread::sleep(Duration::from_secs(1));
        tx.send(target_process_id).unwrap();
        let _ = child.wait();
    });
    let target_process_id = rx.recv().unwrap();
    let config = kill_tree::Config {
        include_target: false,
        ..Default::default()
    };
    let outputs = kill_tree::blocking::kill_tree_with_config(target_process_id, &config)
        .expect("Failed to kill");
    println!("{outputs:?}");
    assert!(!outputs.is_empty());
    let output = &outputs[0];
    match output {
        kill_tree::Output::Killed {
            process_id: _,
            parent_process_id,
            name,
        } => {
            assert_eq!(*parent_process_id, target_process_id);
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    thread::sleep(Duration::from_secs(1));
    let outputs = kill_tree::blocking::kill_tree(target_process_id).expect("Failed to kill");
    println!("{outputs:?}");
    assert_eq!(outputs.len(), 1);
    let output = &outputs[0];
    match output {
        kill_tree::Output::Killed {
            process_id,
            parent_process_id,
            name,
        } => {
            assert_eq!(*process_id, target_process_id);
            assert_eq!(*parent_process_id, std::process::id());
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    thread.join().unwrap();
}

#[test]
fn kill_tree_child_tree() {
    let (tx, rx) = mpsc::channel();
    let thread = thread::spawn(move || {
        let mut child = Command::new("node")
            .arg("-e")
            .arg(get_node_script_spawn_infinite_child())
            .spawn()
            .unwrap();
        thread::sleep(Duration::from_secs(1));
        let target_process_id = child.id();
        tx.send(target_process_id).unwrap();
        let _ = child.wait();
    });
    let target_process_id = rx.recv().unwrap();
    let outputs = kill_tree::blocking::kill_tree(target_process_id).expect("Failed to kill");
    assert_eq!(outputs.len(), 2);
    let target_output = outputs
        .iter()
        .find(|output| match output {
            kill_tree::Output::Killed {
                process_id: _,
                parent_process_id,
                name: _,
            } => *parent_process_id == std::process::id(),
            kill_tree::Output::MaybeAlreadyTerminated { .. } => false,
        })
        .unwrap();
    match target_output {
        kill_tree::Output::Killed {
            process_id,
            parent_process_id,
            name,
        } => {
            assert_eq!(*process_id, target_process_id);
            assert_eq!(*parent_process_id, std::process::id());
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    let child_output = outputs
        .iter()
        .find(|output| match output {
            kill_tree::Output::Killed {
                process_id: _,
                parent_process_id,
                name: _,
            } => *parent_process_id == target_process_id,
            kill_tree::Output::MaybeAlreadyTerminated { .. } => false,
        })
        .unwrap();
    match child_output {
        kill_tree::Output::Killed {
            process_id: _,
            parent_process_id,
            name,
        } => {
            assert_eq!(*parent_process_id, target_process_id);
            assert!(name.starts_with("node"));
        }
        kill_tree::Output::MaybeAlreadyTerminated { .. } => {
            panic!("This should not happen");
        }
    }
    thread.join().unwrap();
}