proc-ctl 0.4.1

A helper library for querying and manipulating processes
Documentation
#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
fn create_command_for_sample(name: &str) -> std::process::Command {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("release")
        .join(name);
    #[cfg(target_os = "windows")]
    let path = path.with_extension("exe");

    if !path.exists() {
        panic!(
            "{} does not exist, try running `cargo build --release --bins`",
            path.display()
        );
    }

    std::process::Command::new(path)
}

#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
struct DropChild(std::process::Child);

#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
impl DropChild {
    fn spawn(mut cmd: std::process::Command) -> Self {
        DropChild(cmd.spawn().expect("Failed to spawn child process"))
    }
}

#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
impl Drop for DropChild {
    fn drop(&mut self) {
        self.0.kill().expect("Failed to kill child process");
    }
}

#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
impl std::ops::Deref for DropChild {
    type Target = std::process::Child;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[cfg(any(
    feature = "proc",
    target_os = "linux",
    target_os = "windows",
    target_os = "macos"
))]
impl std::ops::DerefMut for DropChild {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn tcp_port_query_v4() {
    use retry::delay::Fixed;

    let binder = create_command_for_sample("port-binder");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .tcp_only()
        .ip_v4_only()
        .process_id(handle.id())
        .expect_min_num_ports(1);

    let ports = retry::retry(Fixed::from_millis(100).take(10), move || query.execute()).unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn tcp_port_query_v6() {
    use retry::delay::Fixed;

    let binder = create_command_for_sample("port-binder-v6");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .tcp_only()
        .ip_v6_only()
        .process_id(handle.id())
        .expect_min_num_ports(1);

    let ports = retry::retry(Fixed::from_millis(100).take(10), move || query.execute()).unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn udp_port_query_v4() {
    use retry::delay::Fixed;

    let binder = create_command_for_sample("udp-port-binder");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .udp_only()
        .ip_v4_only()
        .process_id(handle.id())
        .expect_min_num_ports(1);

    let ports = retry::retry(Fixed::from_millis(100).take(10), move || query.execute()).unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn udp_port_query_v6() {
    use retry::delay::Fixed;

    let binder = create_command_for_sample("udp-port-binder-v6");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .udp_only()
        .ip_v6_only()
        .process_id(handle.id())
        .expect_min_num_ports(1);

    let ports = retry::retry(Fixed::from_millis(100).take(10), move || query.execute()).unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
#[test]
fn port_query_which_expects_too_many_ports() {
    use retry::delay::Fixed;

    let binder = create_command_for_sample("port-binder");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .tcp_only()
        .ip_v4_only()
        .process_id_from_child(&handle)
        .expect_min_num_ports(2);

    // Only retry once, getting no ports is still a valid test if the child program hasn't bound yet
    let result = retry::retry(Fixed::from_millis(100).take(1), move || query.execute());

    handle.kill().unwrap();

    result.expect_err("Should have had an error about too few ports");
}

#[cfg(all(
    feature = "resilience",
    any(target_os = "linux", target_os = "windows", target_os = "macos")
))]
#[test]
fn port_query_with_sync_retry() {
    use std::time::Duration;

    let binder = create_command_for_sample("port-binder");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .tcp_only()
        .ip_v4_only()
        .process_id_from_child(&handle)
        .expect_min_num_ports(1);

    let ports = query
        .execute_with_retry_sync(Duration::from_millis(100), 10)
        .unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(all(
    feature = "async",
    any(target_os = "linux", target_os = "windows", target_os = "macos")
))]
#[tokio::test]
async fn port_query_with_async_retry() {
    use std::time::Duration;

    let binder = create_command_for_sample("port-binder");
    let mut handle = DropChild::spawn(binder);

    let query = proc_ctl::PortQuery::new()
        .tcp_only()
        .ip_v4_only()
        .process_id_from_child(&handle)
        .expect_min_num_ports(1);

    let ports = query
        .execute_with_retry(Duration::from_millis(100), 10)
        .await
        .unwrap();

    handle.kill().unwrap();

    assert_eq!(1, ports.len());
}

#[cfg(feature = "proc")]
#[test]
fn proc_query_by_name() {
    use proc_ctl::ProcQuery;
    use std::process::Stdio;

    let mut cmd = create_command_for_sample("waiter")
        .stdin(Stdio::piped())
        .stdout(Stdio::inherit())
        .spawn()
        .unwrap();

    let query = ProcQuery::new().process_name("waiter");

    let processes = query.list_processes().unwrap();

    cmd.kill().unwrap();

    assert_eq!(1, processes.len());
}

#[cfg(feature = "proc")]
#[test]
fn proc_query_for_children() {
    use proc_ctl::ProcQuery;
    use retry::delay::Fixed;

    let binder = create_command_for_sample("port-binder");
    let port_binder_path = binder.get_program();

    let mut runner = create_command_for_sample("proc-runner");
    runner.args([port_binder_path]);
    let mut handle = DropChild::spawn(runner);

    let query = ProcQuery::new()
        .process_id_from_child(&handle)
        .expect_min_num_children(1);

    let process_names = retry::retry(Fixed::from_millis(100).take(10), move || {
        query
            .children()
            .map(|v| v.into_iter().map(|p| p.name).collect::<Vec<String>>())
    })
    .unwrap();

    handle.kill().unwrap();

    assert_eq!(1, process_names.len());

    #[cfg(target_os = "windows")]
    assert_eq!("port-binder.exe", process_names.first().unwrap());
    #[cfg(not(target_os = "windows"))]
    assert_eq!("port-binder", process_names.first().unwrap());
}

#[cfg(all(feature = "proc", feature = "resilience"))]
#[test]
fn proc_query_for_children_with_retry() {
    use proc_ctl::ProcQuery;
    use std::time::Duration;

    let binder = create_command_for_sample("port-binder");
    let port_binder_path = binder.get_program();

    let mut runner = create_command_for_sample("proc-runner");
    runner.args([port_binder_path]);
    let mut handle = DropChild::spawn(runner);

    let process_names = ProcQuery::new()
        .process_id_from_child(&handle)
        .expect_min_num_children(1)
        .children_with_retry_sync(Duration::from_millis(100), 10)
        .unwrap()
        .into_iter()
        .map(|p| p.name)
        .collect::<Vec<String>>();

    handle.kill().unwrap();

    assert_eq!(1, process_names.len());

    #[cfg(target_os = "windows")]
    assert_eq!("port-binder.exe", process_names.first().unwrap());
    #[cfg(not(target_os = "windows"))]
    assert_eq!("port-binder", process_names.first().unwrap());
}

#[cfg(all(feature = "proc", feature = "async"))]
#[tokio::test]
async fn proc_query_for_children_async_with_retry() {
    use proc_ctl::ProcQuery;
    use std::time::Duration;

    let binder = create_command_for_sample("port-binder");
    let port_binder_path = binder.get_program();

    let mut runner = create_command_for_sample("proc-runner");
    runner.args([port_binder_path]);
    let mut handle = DropChild::spawn(runner);

    let process_names = ProcQuery::new()
        .process_id_from_child(&handle)
        .expect_min_num_children(1)
        .children_with_retry(Duration::from_millis(100), 10)
        .await
        .unwrap()
        .into_iter()
        .map(|p| p.name)
        .collect::<Vec<String>>();

    handle.kill().unwrap();

    assert_eq!(1, process_names.len());

    #[cfg(target_os = "windows")]
    assert_eq!("port-binder.exe", process_names.first().unwrap());
    #[cfg(not(target_os = "windows"))]
    assert_eq!("port-binder", process_names.first().unwrap());
}