use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Output;
use std::time::Duration;
use std::time::Instant;
use tokio::process::Command;
use xbp::commands::install_package;
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
let debug: bool = args.contains(&"--debug".to_string());
if args.len() > 1 {
match args[1].as_str() {
"ports" => {
let port_filter: Option<String> = if args.len() > 3 && args[2] == "-p" {
Some(args[3].clone())
} else {
None
};
let kill: bool = args.contains(&"--kill".to_string());
if debug {
println!("\x1b[94mDebug mode enabled\x1b[0m");
println!("[DEBUG] Args: {:?}", args);
}
let start: Instant = Instant::now();
let command_output: String =
execute_ports_command_nesttstat2(port_filter.clone(), debug, kill).await;
let elapsed: Duration = start.elapsed();
if debug {
println!("[DEBUG] execute_ports_command took: {:.2?}", elapsed);
}
if command_output.trim().is_empty() {
if let Some(port) = port_filter {
println!("\x1b[94mNo active processes found on port: {}\x1b[0m", port);
}
} else {
display_output(command_output);
}
}
"setup" => {
let start: Instant = Instant::now();
run_setup_with_debug(debug).await;
if debug {
println!("[DEBUG] run_setup took: {:.2?}", start.elapsed());
}
}
"redeploy" | "-r" => {
let start: Instant = Instant::now();
redeploy_with_debug(debug).await;
if debug {
println!("[DEBUG] redeploy took: {:.2?}", start.elapsed());
}
}
"config" => {
let start: Instant = Instant::now();
read_config_file_with_debug(debug).await;
if debug {
println!("[DEBUG] read_config_file took: {:.2?}", start.elapsed());
}
}
"help" | "--help" | "-h" => {
print_help().await;
}
"install" => {
let package_name: String = args[2].clone();
let debug: bool = args.contains(&"--debug".to_string());
let result: Result<(), String> = install_package(&package_name, debug).await;
match result {
Ok(()) => println!("Installation successful"),
Err(e) => eprintln!("Installation failed: {}", e),
}
}
_ => {
println!("Usage: xbp ports [-p <port>] [--debug] [--kill] | xbp setup");
}
}
} else {
println!("Usage: xbp ports [-p <port>] [--debug] [--kill] | xbp setup");
}
}
async fn print_help() {
println!("\x1b[96mUsage: xbp <command> [options]\x1b[0m");
println!("\n\x1b[93mCommands:\x1b[0m");
println!(" \x1b[92mports\x1b[0m [-p <port>] [--debug] [--kill] : List active ports and processes, optionally filter by port, enable debug mode, or kill processes.");
println!(
" \x1b[92msetup\x1b[0m : Run initial setup commands."
);
println!(" \x1b[92mredeploy\x1b[0m : Redeploy the application by running redeploy.sh in the closest .xbp folder.");
println!(" \x1b[92mconfig\x1b[0m : Read and display the contents of xbp.json in the .xbp folder.");
println!(" \x1b[92minstall\x1b[0m : Install a package using the corresponding installation script.");
}
async fn read_config_file_with_debug(debug: bool) {
let current_dir: PathBuf = env::current_dir().expect("Failed to get current directory");
let xbp_json_path_dotfolder: PathBuf = current_dir.join(".xbp/xbp.json");
let xbp_json_path_root: PathBuf = current_dir.join("xbp.json");
if debug {
println!("[DEBUG] Current dir: {}", current_dir.display());
println!(
"[DEBUG] Checking for: {}",
xbp_json_path_dotfolder.display()
);
println!("[DEBUG] Checking for: {}", xbp_json_path_root.display());
}
let (found_path, found_location) = if xbp_json_path_dotfolder.exists() {
(Some(xbp_json_path_dotfolder), Some(".xbp/xbp.json"))
} else if xbp_json_path_root.exists() {
(Some(xbp_json_path_root), Some("xbp.json"))
} else {
(None, None)
};
if let (Some(path), Some(location)) = (found_path, found_location) {
println!("Found xbp.json at: {}", path.display());
match fs::read_to_string(&path) {
Ok(contents) => {
if debug {
println!("[DEBUG] xbp.json contents: {}", contents);
}
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&contents) {
for (key, value) in json_data.as_object().unwrap() {
let value_str: String = value.to_string().replace("\"", "");
println!("{:<15} | {}", key, value_str);
}
} else {
eprintln!(
"\x1b[91mFailed to parse {} contents as JSON.\x1b[0m",
location
);
}
}
Err(e) => {
eprintln!("\x1b[91mFailed to read {}: {}\x1b[0m", location, e);
}
}
} else {
println!("\x1b[91mNo .xbp/xbp.json or xbp.json found in the current directory.\x1b[0m");
}
}
async fn redeploy_with_debug(debug: bool) {
println!("\x1b[93mRedeploying application...\x1b[0m");
let current_dir: PathBuf = env::current_dir().expect("Failed to get current directory");
let mut dir: &Path = current_dir.as_path();
let mut xbp_path: Option<PathBuf> = None;
for _ in 0..2 {
let potential_path: PathBuf = dir.join(".xbp").join("redeploy.sh");
if debug {
println!(
"[DEBUG] Checking for redeploy.sh at: {}",
potential_path.display()
);
}
if potential_path.exists() {
xbp_path = Some(potential_path);
if debug {
println!(
"[DEBUG] Found redeploy.sh at: {}",
xbp_path.as_ref().unwrap().display()
);
}
break;
}
if let Some(parent) = dir.parent() {
dir = parent;
} else {
break;
}
}
if let Some(xbp_path) = xbp_path {
println!("Found redeploy.sh at: {}", xbp_path.display());
let chmod_start: Instant = Instant::now();
let chmod_output: Output = match Command::new("sudo")
.arg("chmod")
.arg("+x")
.arg(&xbp_path)
.output()
.await
{
Ok(output) => output,
Err(e) => {
eprintln!("\x1b[91mFailed to execute chmod command: {}\x1b[0m", e);
return;
}
};
let chmod_elapsed: Duration = chmod_start.elapsed();
println!("\x1b[94mMaking redeploy.sh executable...\x1b[0m");
if debug {
println!("[DEBUG] chmod output: {:?}", chmod_output);
println!("[DEBUG] chmod took: {:.2?}", chmod_elapsed);
}
if !chmod_output.status.success() {
eprintln!(
"\x1b[91mFailed to make redeploy.sh executable: {}\x1b[0m",
String::from_utf8_lossy(&chmod_output.stderr)
);
return;
}
let redeploy_start: Instant = Instant::now();
let redeploy_output: Output = Command::new(&xbp_path)
.output()
.await
.expect("Failed to execute redeploy.sh");
let redeploy_elapsed = redeploy_start.elapsed();
if debug {
println!("[DEBUG] redeploy.sh output: {:?}", redeploy_output);
println!("[DEBUG] redeploy.sh took: {:.2?}", redeploy_elapsed);
}
if !redeploy_output.status.success() {
eprintln!(
"\x1b[91mFailed to run redeploy.sh: {}\x1b[0m",
String::from_utf8_lossy(&redeploy_output.stderr)
);
return;
}
println!(
"\x1b[92mRedeploy output:\x1b[0m {}",
String::from_utf8_lossy(&redeploy_output.stdout)
);
} else {
println!("\x1b[91mNo .xbp/redeploy.sh found in the directory hierarchy.\x1b[0m");
}
}
async fn run_setup_with_debug(debug: bool) {
let setup_cmd: String = "sudo apt install -y net-tools nginx pkg-config libssl-dev build-essential plocate sshpass neofetch certbot python3-certbot-nginx".to_string();
if debug {
println!("[DEBUG] Running setup command: {}", setup_cmd);
}
let start: Instant = Instant::now();
let output: Output = Command::new("sh")
.arg("-c")
.arg(setup_cmd)
.output()
.await
.expect("Failed to execute setup command");
let elapsed = start.elapsed();
if debug {
println!("[DEBUG] Setup command output: {:?}", output);
println!("[DEBUG] Setup command took: {:.2?}", elapsed);
}
if !output.status.success() {
eprintln!(
"\x1b[91mSetup failed: {}\x1b[0m",
String::from_utf8_lossy(&output.stderr)
);
return;
}
if !output.stdout.is_empty() {
println!(
"\x1b[92mSetup output:\x1b[0m {}",
String::from_utf8_lossy(&output.stdout)
);
}
println!("\x1b[92mSetup completed successfully!\x1b[0m");
}
async fn execute_ports_command_nesttstat2(
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_nesttstat2 took: {:.2?}",
start.elapsed()
);
}
table_output
}
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()
}
fn display_output(output: String) {
println!("{}", output);
}