use crate::api::models::*;
use crate::commands::service::load_xbp_config;
use crate::commands::{
install_package as install_package_cmd, pm2_delete, pm2_save, pm2_stop, run_config, run_ports,
run_redeploy, run_redeploy_service, run_service_command as run_service_cmd, run_setup,
};
use crate::strategies::get_all_services;
use crate::utils::command_exists;
use actix_web::{web, HttpResponse, Responder};
use reqwest;
use std::fs;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::{error, info};
pub async fn health() -> impl Responder {
HttpResponse::Ok().json(HealthResponse {
status: "ok".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
})
}
pub async fn list_ports() -> impl Responder {
match get_ports_data(None).await {
Ok(ports) => HttpResponse::Ok().json(PortsResponse { ports }),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn get_port(path: web::Path<u16>) -> impl Responder {
let port = path.into_inner();
match get_ports_data(Some(port)).await {
Ok(ports) => HttpResponse::Ok().json(PortsResponse { ports }),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn kill_port(path: web::Path<u16>) -> impl Responder {
let port = path.into_inner();
let args = vec!["-p".to_string(), port.to_string(), "--kill".to_string()];
match run_ports(&args, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Killed processes on port {}", port)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
async fn get_ports_data(port_filter: Option<u16>) -> Result<Vec<PortInfo>, String> {
use netstat2::{get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
use std::collections::BTreeMap;
let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
let proto_flags = ProtocolFlags::TCP;
let sockets = get_sockets_info(af_flags, proto_flags)
.map_err(|e| format!("Failed to get sockets info: {}", e))?;
let mut port_map: BTreeMap<u16, Vec<PortInfo>> = BTreeMap::new();
for socket in sockets {
if let ProtocolSocketInfo::Tcp(ref tcp_info) = socket.protocol_socket_info {
if let Some(filter) = port_filter {
if tcp_info.local_port != filter {
continue;
}
}
let pid = if !socket.associated_pids.is_empty() {
Some(socket.associated_pids[0].to_string())
} else {
None
};
let port_info = PortInfo {
port: tcp_info.local_port,
pid,
local_addr: tcp_info.local_addr.to_string(),
remote_addr: tcp_info.remote_addr.to_string(),
state: format!("{:?}", tcp_info.state),
process: "-".to_string(),
};
port_map
.entry(tcp_info.local_port)
.or_default()
.push(port_info);
}
}
let mut result = Vec::new();
for ports in port_map.values() {
result.extend(ports.iter().cloned());
}
Ok(result)
}
pub async fn list_systemctl() -> impl Responder {
match get_systemctl_data(None).await {
Ok(services) => HttpResponse::Ok().json(SystemctlResponse { services }),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn get_systemctl_service(path: web::Path<String>) -> impl Responder {
let service_name = path.into_inner();
match get_systemctl_data(Some(&service_name)).await {
Ok(services) => HttpResponse::Ok().json(SystemctlResponse { services }),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
async fn get_systemctl_data(filter: Option<&str>) -> Result<Vec<SystemctlService>, String> {
if !cfg!(target_os = "linux") || !command_exists("systemctl") {
return Err(
"systemctl is only available on Linux hosts where systemd is installed.".to_string(),
);
}
let mut cmd = Command::new("systemctl");
cmd.arg("list-units");
cmd.arg("--type=service");
cmd.arg("--no-pager");
cmd.arg("--no-legend");
let output = cmd
.output()
.await
.map_err(|e| format!("Failed to run systemctl: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut services = Vec::new();
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 {
continue;
}
let name = parts[0].to_string();
if let Some(filter_name) = filter {
if !name.contains(filter_name) {
continue;
}
}
let status = parts[2].to_string();
let active = status == "active";
let enabled = parts[3] == "enabled";
services.push(SystemctlService {
name,
status,
active,
enabled,
});
}
Ok(services)
}
pub async fn list_pm2() -> impl Responder {
match get_pm2_data().await {
Ok(processes) => HttpResponse::Ok().json(Pm2Response { processes }),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
async fn get_pm2_data() -> Result<Vec<Pm2Process>, String> {
let mut cmd = Command::new("pm2");
cmd.arg("jlist");
let output = cmd
.output()
.await
.map_err(|e| format!("Failed to run pm2 jlist: {}", e))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let processes: Vec<serde_json::Value> =
serde_json::from_str(&stdout).map_err(|e| format!("Failed to parse pm2 output: {}", e))?;
let mut result = Vec::new();
for proc in processes {
let name = proc["name"].as_str().unwrap_or("unknown").to_string();
let pid = proc["pid"].as_u64().map(|p| p as u32);
let status = proc["pm2_env"]["status"]
.as_str()
.unwrap_or("unknown")
.to_string();
let cpu = proc["monit"]["cpu"].as_f64();
let memory = proc["monit"]["memory"]
.as_f64()
.map(|m| m / 1024.0 / 1024.0);
let uptime = proc["pm2_env"]["pm_uptime"].as_u64().map(|u| {
let seconds = u / 1000;
format!("{}s", seconds)
});
result.push(Pm2Process {
name,
pid,
status,
cpu,
memory,
uptime,
});
}
Ok(result)
}
pub async fn delete_pm2(path: web::Path<String>) -> impl Responder {
let name = path.into_inner();
match pm2_delete(&name, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Deleted PM2 process: {}", name)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn start_pm2(path: web::Path<String>) -> impl Responder {
let name = path.into_inner();
let mut cmd = Command::new("pm2");
cmd.arg("start").arg(&name);
match cmd.output().await {
Ok(output) => {
if output.status.success() {
HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Started PM2 process: {}", name)}))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("PM2 start failed: {}", stderr),
})
}
}
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("Failed to start PM2 process: {}", e),
}),
}
}
pub async fn stop_pm2(path: web::Path<String>) -> impl Responder {
let name = path.into_inner();
match pm2_stop(&name, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Stopped PM2 process: {}", name)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn restart_pm2(path: web::Path<String>) -> impl Responder {
let name = path.into_inner();
let mut cmd = Command::new("pm2");
cmd.arg("restart").arg(&name);
match cmd.output().await {
Ok(output) => {
if output.status.success() {
HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Restarted PM2 process: {}", name)}))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("PM2 restart failed: {}", stderr),
})
}
}
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("Failed to restart PM2 process: {}", e),
}),
}
}
pub async fn list_services() -> impl Responder {
match load_xbp_config().await {
Ok(config) => {
let services = get_all_services(&config);
let service_infos: Vec<ServiceInfo> = services
.iter()
.map(|s| ServiceInfo {
name: s.name.clone(),
target: s.target.clone(),
port: s.port,
branch: s.branch.clone(),
url: s.url.clone(),
})
.collect();
HttpResponse::Ok().json(ServicesResponse {
services: service_infos,
})
}
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn run_service_command(path: web::Path<(String, String)>) -> impl Responder {
let (name, command) = path.into_inner();
match run_service_cmd(&command, &name, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Executed {} on service {}", command, name)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn get_config() -> impl Responder {
match run_config(false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn get_logs() -> impl Responder {
HttpResponse::Ok().json(serde_json::json!({"message": "Logs endpoint - use /logs?command=<command> for specific logs"}))
}
pub async fn install_package(path: web::Path<String>) -> impl Responder {
let package = path.into_inner();
match install_package_cmd(&package, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Installed package: {}", package)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn setup() -> impl Responder {
match run_setup(false).await {
Ok(_) => HttpResponse::Ok()
.json(serde_json::json!({"success": true, "message": "Setup completed"})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn redeploy() -> impl Responder {
match run_redeploy().await {
Ok(_) => HttpResponse::Ok()
.json(serde_json::json!({"success": true, "message": "Redeploy completed"})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn redeploy_service(path: web::Path<String>) -> impl Responder {
let service_name = path.into_inner();
match run_redeploy_service(&service_name, false).await {
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"success": true, "message": format!("Redeployed service: {}", service_name)})),
Err(e) => HttpResponse::InternalServerError().json(ErrorResponse { error: e }),
}
}
pub async fn download_and_run_binary(req: web::Json<BinaryDownloadRequest>) -> impl Responder {
let download_req = req.into_inner();
info!("Downloading binary from: {}", download_req.url);
let client = reqwest::Client::new();
let response = match client.get(&download_req.url).send().await {
Ok(resp) => resp,
Err(e) => {
error!("Failed to download binary: {}", e);
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("Failed to download binary: {}", e),
});
}
};
let bytes = match response.bytes().await {
Ok(b) => b,
Err(e) => {
error!("Failed to read binary data: {}", e);
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("Failed to read binary data: {}", e),
});
}
};
let binary_path = format!("/tmp/{}", download_req.name);
match fs::write(&binary_path, &bytes) {
Ok(_) => {
info!("Binary saved to: {}", binary_path);
}
Err(e) => {
error!("Failed to save binary: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("Failed to save binary: {}", e),
});
}
}
let chmod_output = Command::new("chmod")
.arg("+x")
.arg(&binary_path)
.output()
.await;
if let Err(e) = chmod_output {
error!("Failed to make binary executable: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("Failed to make binary executable: {}", e),
});
}
let mut pm2_cmd = Command::new("pm2");
pm2_cmd.arg("start");
pm2_cmd.arg(&binary_path);
pm2_cmd.arg("--name");
pm2_cmd.arg(&download_req.name);
if let Some(ref args) = download_req.args {
for arg in args {
pm2_cmd.arg(arg);
}
}
match pm2_cmd.output().await {
Ok(output) => {
if output.status.success() {
let _ = pm2_save(false).await;
HttpResponse::Ok().json(BinaryDownloadResponse {
success: true,
message: format!(
"Binary downloaded and started as PM2 process: {}",
download_req.name
),
pm2_name: Some(download_req.name),
})
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("PM2 start failed: {}", stderr);
HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("PM2 start failed: {}", stderr),
})
}
}
Err(e) => {
error!("Failed to start PM2 process: {}", e);
HttpResponse::InternalServerError().json(ErrorResponse {
error: format!("Failed to start PM2 process: {}", e),
})
}
}
}
pub async fn download_openapi() -> impl Responder {
let openapi_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("openapi.yaml");
match fs::read(&openapi_path) {
Ok(bytes) => HttpResponse::Ok()
.insert_header(("Content-Type", "application/yaml"))
.insert_header((
"Content-Disposition",
"attachment; filename=\"openapi.yaml\"",
))
.body(bytes),
Err(_) => HttpResponse::NotFound().json(ErrorResponse {
error: "OpenAPI spec not found".to_string(),
}),
}
}