diff --git a/src/app_manager.rs b/src/app_manager.rs index 9be2aae..594c776 100644 --- a/src/app_manager.rs +++ b/src/app_manager.rs @@ -1,146 +1,242 @@ +use std::env; +use std::fs; +use std::fs::remove_file; use std::path::PathBuf; use std::process::Command; use serde::{Deserialize, Serialize}; -use std::env; -use std::fs; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct App { pub repo: String, pub branch: String, pub install_command: String, pub run_command: String, - pub enabled: Option, } -pub fn remove_app(apps: &mut Vec, index: usize) { - if let Some(app) = apps.get(index) { - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - let apps_dir = PathBuf::from(home).join("sentryx/apps"); - let repo_name = app - .repo +impl App { + /// Converts URL to "user/repo" + pub fn id(&self) -> Option { + let url = self.repo.trim_end_matches(".git"); + + if url.starts_with("git@") { + // Format: git@github.com:usr/repo + url.split_once(':').map(|(_, path)| path.to_string()) + } else if url.starts_with("https://") || url.starts_with("http://") { + // Format: https://github.com/usr/repo + url.split_once("github.com/") + .map(|(_, path)| path.to_string()) + } else { + None + } + } + + /// Converts URL to "user-repo" + pub fn id_system(&self) -> Option { + let url = self.repo.trim_end_matches(".git"); + + let repo_path = if url.starts_with("git@") { + url.split_once(':').map(|(_, path)| path) + } else if url.starts_with("https://") || url.starts_with("http://") { + url.split_once("github.com/").map(|(_, path)| path) + } else { + None + }?; + + Some(repo_path.replace('/', "-")) + } + + pub fn repo_folder_name(&self) -> String { + self.repo .trim_end_matches(".git") .rsplit('/') .next() - .expect("Failed to extract repo name"); - fs::remove_dir_all(&apps_dir.join(repo_name)).expect("Failed to remove app directory"); - println!("Removed app: {}", app.repo); - } else { - println!("App not found: {}", index); + .unwrap_or("unknown") + .to_string() + } +} + +fn home_dir() -> PathBuf { + let home = env::var("HOME").expect("Failed to get HOME environment variable"); + PathBuf::from(home) +} + +fn apps_dir() -> PathBuf { + let home = env::var("HOME").expect("Failed to get HOME environment variable"); + PathBuf::from(home).join("sentryx/apps") +} + +fn apps_file_path() -> PathBuf { + apps_dir().join("apps.json") +} + +pub fn init() -> Vec { + let path = apps_file_path(); + + if !path.exists() { + let _ = fs::create_dir_all(path.parent().unwrap()); + let _ = fs::write(&path, "[]"); } - apps.remove(index); - save_apps(apps); + let content = fs::read_to_string(&path).unwrap_or_else(|_| "[]".to_string()); + serde_json::from_str(&content).unwrap_or_else(|_| vec![]) +} + +pub fn save_apps(apps: &[App]) { + let path = apps_file_path(); + let json = serde_json::to_string_pretty(apps).expect("Failed to serialize apps"); + + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + fs::write(path, json).expect("Failed to write apps.json"); +} + +pub fn uninstall_app(apps: &mut Vec, id: &str) { + if let Some(index) = apps + .iter() + .position(|app| app.id().expect("Unable to obtain id") == id) + { + let app = &apps[index]; + let repo_dir = apps_dir().join(app.repo_folder_name()); + + if repo_dir.exists() { + if let Err(e) = fs::remove_dir_all(&repo_dir) { + eprintln!("Failed to remove directory {:?}: {}", repo_dir, e); + } else { + println!("Removed app: {}", id); + } + } + + let user_systemd_dir = home_dir().join(".config/systemd/user"); + + let service_path = user_systemd_dir.join(format!( + "{}.service", + app.id_system().expect("Unable to obtain id") + )); + + remove_file(service_path).unwrap_or_else(|e| { + eprintln!("Failed to remove service file: {}", e); + }); + + apps.remove(index); + save_apps(apps); + } else { + eprintln!("App not found: {}", id); + } } pub fn install_app(apps: &mut Vec, app: App) { - let a2 = app.clone(); - // Get home directory and create ~/sentryx/apps if needed - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - let apps_dir = PathBuf::from(home).join("sentryx/apps"); + let path = apps_dir(); + let repo_name = app.repo_folder_name(); - if !apps_dir.exists() { - fs::create_dir_all(&apps_dir).expect("Failed to create apps directory"); + // Ensure apps directory exists + if let Err(e) = fs::create_dir_all(&path) { + eprintln!("Failed to create apps directory: {}", e); + return; } - // Change to apps directory - env::set_current_dir(&apps_dir).expect("Failed to change to apps directory"); + if let Err(e) = env::set_current_dir(&path) { + eprintln!("Failed to switch to apps directory: {}", e); + return; + } - println!("Installing app from repo: {}", app.repo); - println!("Branch: {}", app.branch); - println!("Install command: {}", app.install_command); - println!("Run command: {}", app.run_command); + println!("Installing app: {}", app.id().expect("Unable to obtain id")); + println!("→ Cloning branch `{}` from `{}`", app.branch, app.repo); - // Clone the repo - println!("Cloning repo"); - Command::new("git") + let clone_status = Command::new("git") .arg("clone") .arg("-b") .arg(&app.branch) .arg(&app.repo) - .status() - .expect("Failed to clone repository"); + .status(); - // Extract repo name from URL (e.g., "https://github.com/user/myapp.git" -> "myapp") - let repo_name = app - .repo - .trim_end_matches(".git") - .rsplit('/') - .next() - .expect("Failed to extract repo name"); - - // Change to cloned repo directory - env::set_current_dir(&repo_name).expect("Failed to change to cloned repo directory"); - - // You are now inside the cloned repo - println!("Changed directory to {:?}", env::current_dir().unwrap()); - - apps.push(a2.clone()); - save_apps(&apps); - - Command::new("bash") - .arg("-c") - .arg(app.install_command) - .status() - .expect("Failed to execute Bash command"); - - run_app(a2); -} - -pub fn run_app(app: App) { - // Change to the app directory - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - let app_dir = PathBuf::from(home) - .join("sentryx/apps") - .join(app.repo.trim_end_matches(".git").split('/').last().unwrap()); - - env::set_current_dir(&app_dir).expect("Failed to change to app directory"); - - // Run the app - Command::new("bash") - .arg("-c") - .arg(app.run_command) - .status() - .expect("Failed to execute run command"); -} - -pub fn run_apps() -> Vec { - // Change to the app directory - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - let apps_dir = PathBuf::from(home).join("sentryx/apps"); - - if !apps_dir.exists() { - fs::create_dir_all(&apps_dir).expect("Failed to create apps directory"); - fs::write(apps_dir.join("apps.json"), "[]").expect("Failed to create apps.json"); + if !matches!(clone_status, Ok(s) if s.success()) { + eprintln!("Failed to clone repository"); + return; } - let apps_json = serde_json::from_str::>( - &fs::read_to_string(apps_dir.join("apps.json")).expect("Failed to read apps.json"), - ) - .expect("Failed to parse apps.json"); - - for app in apps_json.clone() { - if app.enabled.unwrap_or(true) { - println!("Running app: {}", app.repo); - std::thread::spawn(|| { - run_app(app); - }); - } else { - println!("Skipping disabled app: {}", app.repo); - } + let repo_path = path.join(&repo_name); + if let Err(e) = env::set_current_dir(&repo_path) { + eprintln!("Failed to change into cloned repo directory: {}", e); + return; } - apps_json + println!( + "→ Running install command in {:?}", + env::current_dir().unwrap() + ); + + let install_status = Command::new("bash") + .arg("-c") + .arg(&app.install_command) + .status(); + + if !matches!(install_status, Ok(s) if s.success()) { + eprintln!("Install command failed"); + } + + let service_contents = format!( + "[Unit]\nDescription={repo_name} (Installed with SentryX)\nAfter=network.target\n\n[Service]\nWorkingDirectory=/home/server/saturn-server\nExecStart={}\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\n\n[Install]\nWantedBy=multi-user.target", + app.run_command, + ); + + let user_systemd_dir = home_dir().join(".config/systemd/user"); + + fs::create_dir_all(&user_systemd_dir).expect("Failed to create user systemd dir"); + + let service_path = user_systemd_dir.join(format!( + "{}.service", + app.id_system().expect("Unable to obtain id") + )); + + println!("→ Creating service file: {service_path:?}"); + + if let Err(e) = fs::write(&service_path, service_contents) { + eprintln!( + "Failed to write service file, make sure you run this server with sudo permissions: {}", + e + ); + return; + } + + // Add to app list and save + apps.push(app); + save_apps(apps); } -pub fn save_apps(apps: &[App]) { - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - let apps_dir = PathBuf::from(home).join("sentryx/apps"); +pub fn toggle_app_state(id: &str, enable: bool) { + Command::new("systemctl") + .arg("--user") + .arg(if enable { "enable" } else { "disable" }) + .arg(id) + .status() + .expect("Failed to toggle app enable state"); +} - fs::write( - apps_dir.join("apps.json"), - serde_json::to_string(apps).expect("Failed to serialize apps"), - ) - .expect("Failed to write apps.json"); +pub fn start_app(id: &str) { + Command::new("systemctl") + .arg("--user") + .arg("start") + .arg(id) + .status() + .expect("Failed to start app"); +} + +pub fn stop_app(id: &str) { + Command::new("systemctl") + .arg("--user") + .arg("stop") + .arg(id) + .status() + .expect("Failed to stop app"); +} + +pub fn restart_app(id: &str) { + Command::new("systemctl") + .arg("restart") + .arg(id) + .status() + .expect("Failed to restart app"); } diff --git a/src/main.rs b/src/main.rs index 565ab31..dc724b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,23 @@ pub mod app_manager; pub mod server_config; +use crate::app_manager::{install_app, App}; +use crate::server_config::ServerStats; use std::sync::Arc; use std::thread::spawn; use std::{net::TcpListener, sync::Mutex}; use sysinfo::{Disks, Networks, System}; use tungstenite::accept; -use crate::app_manager::{install_app, App}; -use crate::server_config::ServerStats; -fn get_system_info(sys: &mut System, networks: &Networks, disks: &Disks) -> ServerStats { - let cpus = sys.cpus(); - let total_cpu_usage: f32 = cpus.iter().map(|cpu| cpu.cpu_usage()).sum(); - let average_cpu_usage = total_cpu_usage / cpus.len() as f32; - - let network_usage: u64 = networks - .iter() - .map(|(_, data)| data.received() + data.transmitted()) - .sum(); - - let total_disk_space: u64 = disks.iter().map(|d| d.total_space()).sum(); - let available_disk_space: u64 = disks.iter().map(|d| d.available_space()).sum(); - - let used_disk_space = total_disk_space.saturating_sub(available_disk_space); - let disk_usage_percentage = if total_disk_space == 0 { - 0 - } else { - ((used_disk_space as f32 / total_disk_space as f32) * 100.0) as u8 - }; - - ServerStats { - memory: (sys.used_memory() as f32 / sys.total_memory() as f32 * 100.0) as u8, - cpu: average_cpu_usage as u8, - disk: disk_usage_percentage, - network: network_usage, - } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub enum Method { + Install(App), + SetEnabled(usize, bool), + Edit(usize, App), } fn main() { - let apps = app_manager::run_apps(); + let apps = app_manager::init(); let server = TcpListener::bind("127.0.0.1:5273").unwrap(); for stream in server.incoming() { @@ -72,7 +51,7 @@ fn main() { } std::thread::spawn(move || loop { - let app: App = serde_json::from_str( + let mth: Method = serde_json::from_str( &reader_socket .lock() .unwrap() @@ -83,7 +62,26 @@ fn main() { ) .unwrap(); - install_app(&mut apps, app); + match mth { + Method::Install(app) => { + install_app(&mut apps, app); + } + Method::SetEnabled(i, b) => { + if let Some(app) = apps.get_mut(i) { + } else { + eprintln!("App index {} not found", i); + } + app_manager::save_apps(&apps); + } + Method::Edit(i, app) => { + if let Some(existing_app) = apps.get_mut(i) { + *existing_app = app; + } else { + eprintln!("App index {} not found", i); + } + app_manager::save_apps(&apps); + } + } }); let mut sys = System::new_all(); @@ -111,3 +109,31 @@ fn main() { }); } } + +fn get_system_info(sys: &mut System, networks: &Networks, disks: &Disks) -> ServerStats { + let cpus = sys.cpus(); + let total_cpu_usage: f32 = cpus.iter().map(|cpu| cpu.cpu_usage()).sum(); + let average_cpu_usage = total_cpu_usage / cpus.len() as f32; + + let network_usage: u64 = networks + .iter() + .map(|(_, data)| data.received() + data.transmitted()) + .sum(); + + let total_disk_space: u64 = disks.iter().map(|d| d.total_space()).sum(); + let available_disk_space: u64 = disks.iter().map(|d| d.available_space()).sum(); + + let used_disk_space = total_disk_space.saturating_sub(available_disk_space); + let disk_usage_percentage = if total_disk_space == 0 { + 0 + } else { + ((used_disk_space as f32 / total_disk_space as f32) * 100.0) as u8 + }; + + ServerStats { + memory: (sys.used_memory() as f32 / sys.total_memory() as f32 * 100.0) as u8, + cpu: average_cpu_usage as u8, + disk: disk_usage_percentage, + network: network_usage, + } +} diff --git a/test.js b/test.js index d909203..7bafcf6 100644 --- a/test.js +++ b/test.js @@ -5,10 +5,12 @@ ws.onopen = () => { ws.send("my-key"); ws.send( JSON.stringify({ - repo: "git@github.com:osui-rs/osui.git", - branch: "master", - install_command: "", - run_command: "cargo run", + Uninstall: { + repo: "git@github.com:osui-rs/osui.git", + branch: "master", + install_command: "", + run_command: "cargo run", + } }) ); };