xbp 0.5.4

XBP is a build pack and deployment management tool to deploy, rust, nextjs etc and manage the NGINX configs below it
Documentation
//! Ports command module
//!
//! Lists active TCP sockets with optional port filtering, killing processes by PID,
//! and searching NGINX configs for references to a given port. This module
//! contains the high-level entrypoint used by the CLI and the lower-level
//! helpers that perform socket enumeration and optional actions.
use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::process::Output;
use std::time::{Duration, Instant};
use tokio::process::Command;

use crate::logging::{log_info, log_timed, LogLevel};

/// Execute the `ports` command.
///
/// Parses the provided `args` (from clap or manual vector) and executes the
/// sockets listing with optional flags:
/// - `-p <port>`: filter by a specific local port
/// - `--kill`: send `kill -9` to all PIDs bound to the matching sockets
/// - `-n`: search `/etc/nginx/sites-available` for references to the port
///
/// Returns `Ok(())` on success or a descriptive `Err(String)` on failures that
/// prevent execution (e.g., invalid arguments). Non-fatal issues are printed.
pub async fn run_ports(args: &[String], debug: bool) -> Result<(), String> {
    let mut port_filter: Option<String> = None;
    let mut kill: bool = false;
    let mut nginx_search: bool = false;

    let mut i: usize = 0;
    while i < args.len() {
        match args[i].as_str() {
            "-p" => {
                if let Some(p) = args.get(i + 1) {
                    port_filter = Some(p.clone());
                    i += 2;
                } else {
                    return Err("-p requires a port value".to_string());
                }
            }
            "--kill" => {
                kill = true;
                i += 1;
            }
            "-n" => {
                nginx_search = true;
                i += 1;
            }
            _ => {
                i += 1;
            }
        }
    }

    if debug {
        println!("\x1b[94mDebug mode enabled\x1b[0m");
        println!("[DEBUG] Args: {:?}", args);
    }

    let _ = log_info("ports", "Executing ports command", port_filter.as_deref()).await;

    let start: Instant = Instant::now();
    let command_output: String = execute_ports_command_netstat2(port_filter.clone(), debug, kill).await;
    let elapsed: Duration = start.elapsed();

    let _ = log_timed(
        LogLevel::Success,
        "ports",
        "Ports command completed",
        elapsed.as_millis() as u64,
    )
    .await;

    if debug {
        println!("[DEBUG] execute_ports_command took: {:.2?}", elapsed);
    }

    if command_output.trim().is_empty() {
        if let Some(port) = port_filter.clone() {
            println!("\x1b[94mNo active processes found on port: {}\x1b[0m", port);
        }
    } else {
        display_output(command_output);
    }

    if nginx_search {
        if let Some(port) = port_filter {
            println!("\nSearching NGINX configurations for port: {}\n", port);
            search_nginx_configs(&port).await;
        } else {
            eprintln!("\x1b[91mError: -n flag requires a port to be specified with -p.\x1b[0m");
        }
    }

    Ok(())
}

/// Enumerate TCP sockets and render a table.
///
/// - `port_filter`: when `Some`, only entries matching the given port are shown
/// - `debug`: enables verbose console logs
/// - `kill`: when `true`, attempts to `kill -9` for each associated PID
///
/// Returns a formatted table as a string. Errors reading sockets are rendered
/// into the returned string instead of failing the function.
async fn execute_ports_command_netstat2(
    port_filter: Option<String>,
    debug: bool,
    kill: bool,
) -> String {
    let start: Instant = Instant::now();
    let af_flags: AddressFamilyFlags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
    let proto_flags: ProtocolFlags = ProtocolFlags::TCP;

    let sockets: Vec<netstat2::SocketInfo> = match get_sockets_info(af_flags, proto_flags) {
        Ok(s) => s,
        Err(e) => {
            return format!("Failed to get sockets info: {}", e);
        }
    };

    if debug {
        println!("[DEBUG] Fetched {} TCP sockets", sockets.len());
    }

    let mut table_output: String = String::new();

    let mut port_map: BTreeMap<u16, Vec<(&netstat2::SocketInfo, &netstat2::TcpSocketInfo)>> =
        BTreeMap::new();

    for socket in &sockets {
        if let ProtocolSocketInfo::Tcp(ref tcp_info) = socket.protocol_socket_info {
            if let Some(ref filter) = port_filter {
                if filter != &tcp_info.local_port.to_string() {
                    continue;
                }
            }
            port_map
                .entry(tcp_info.local_port)
                .or_default()
                .push((socket, tcp_info));
        }
    }

    for (port, entries) in &port_map {
        table_output.push_str(&format!("Port: {}\n", port));
        table_output.push_str(&format!(
            "{:<10} {:<20} {:<20} {:<10} {:<10}\n",
            "PID", "LocalAddr", "RemoteAddr", "State", "Process"
        ));
        table_output.push_str(&format!("{:-<80}\n", ""));
        for (socket, tcp_info) in entries {
            let pids: String = if !socket.associated_pids.is_empty() {
                socket
                    .associated_pids
                    .iter()
                    .map(|pid| pid.to_string())
                    .collect::<Vec<_>>()
                    .join(",")
            } else {
                "-".to_string()
            };

            let process_names: String = "-".to_string();

            table_output.push_str(&format!(
                "{:<10} {:<20} {:<20} {:<10} {:<10}\n",
                pids,
                tcp_info.local_addr,
                tcp_info.remote_addr,
                format!("{:?}", tcp_info.state),
                process_names
            ));

            if kill {
                for pid in &socket.associated_pids {
                    let killed: bool = kill_process_with_debug(&pid.to_string(), debug).await;
                    if killed {
                        println!(
                            "\x1b[92mSuccessfully killed process with PID: {}\x1b[0m",
                            pid
                        );
                        table_output.push_str(&format!(
                            "\x1b[91mKilled process with PID: {}\x1b[0m\n",
                            pid
                        ));
                    } else {
                        eprintln!("\x1b[91mFailed to kill process with PID: {}\x1b[0m", pid);
                    }
                }
            }
        }
        table_output.push_str(&format!("{:-<80}\n\n", ""));
    }

    if debug {
        println!(
            "[DEBUG] execute_ports_command_netstat2 took: {:.2?}",
            start.elapsed()
        );
    }

    table_output
}

/// Attempt to `kill -9 <pid>` and print debug details when enabled.
///
/// Returns `true` when the kill command returns success; `false` otherwise.
async fn kill_process_with_debug(pid: &str, debug: bool) -> bool {
    if debug {
        println!("[DEBUG] Attempting to kill PID: {}", pid);
    }
    let start: Instant = Instant::now();
    let kill_output: Output = Command::new("sh")
        .arg("-c")
        .arg(format!("sudo kill -9 {}", pid))
        .output()
        .await
        .expect("Failed to execute kill command");
    let elapsed = start.elapsed();
    if debug {
        println!(
            "[DEBUG] kill_process output: status={:?}, stdout='{}', stderr='{}', took: {:.2?}",
            kill_output.status,
            String::from_utf8_lossy(&kill_output.stdout),
            String::from_utf8_lossy(&kill_output.stderr),
            elapsed
        );
    }
    kill_output.status.success()
}

/// Print the generated table output to stdout.
fn display_output(output: String) {
    println!("{}", output);
}

/// Search NGINX `sites-available` for references to the provided `port`.
///
/// This is a best-effort scan used for quick diagnostics; it skips unreadable
/// files and reports any I/O errors encountered for the directory.
async fn search_nginx_configs(port: &str) {
    let nginx_sites_available_path = PathBuf::from("/etc/nginx/sites-available");

    if !nginx_sites_available_path.exists() {
        println!("\x1b[93mWarning: /etc/nginx/sites-available/ not found. Skipping NGINX config search.\x1b[0m");
        return;
    }

    let mut found_configs = false;

    match fs::read_dir(&nginx_sites_available_path) {
        Ok(entries) => {
            for entry in entries {
                if let Ok(entry) = entry {
                    let path = entry.path();
                    if path.is_file() {
                        let config_content = match fs::read_to_string(&path) {
                            Ok(content) => content,
                            Err(_) => continue,
                        };

                        if config_content.contains(&format!("proxy_pass http://127.0.0.1:{}", port))
                            || config_content.contains(&format!("listen {}", port))
                        {
                            println!("\x1b[92mFound port {} in NGINX config: {}\x1b[0m", port, path.display());
                            found_configs = true;
                        }
                    }
                }
            }
        }
        Err(e) => {
            eprintln!("\x1b[91mError reading NGINX sites-available directory: {}\x1b[0m", e);
            return;
        }
    }

    if !found_configs {
        println!("\x1b[94mNo NGINX configurations found for port {}.\x1b[0m", port);
    }
}