testing
This commit is contained in:
+197
-101
@@ -1,146 +1,242 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::remove_file;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub repo: String,
|
pub repo: String,
|
||||||
pub branch: String,
|
pub branch: String,
|
||||||
pub install_command: String,
|
pub install_command: String,
|
||||||
pub run_command: String,
|
pub run_command: String,
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_app(apps: &mut Vec<App>, index: usize) {
|
impl App {
|
||||||
if let Some(app) = apps.get(index) {
|
/// Converts URL to "user/repo"
|
||||||
let home = env::var("HOME").expect("Failed to get HOME environment variable");
|
pub fn id(&self) -> Option<String> {
|
||||||
let apps_dir = PathBuf::from(home).join("sentryx/apps");
|
let url = self.repo.trim_end_matches(".git");
|
||||||
let repo_name = app
|
|
||||||
.repo
|
if url.starts_with("git@") {
|
||||||
|
// Format: [email protected]: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<String> {
|
||||||
|
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")
|
.trim_end_matches(".git")
|
||||||
.rsplit('/')
|
.rsplit('/')
|
||||||
.next()
|
.next()
|
||||||
.expect("Failed to extract repo name");
|
.unwrap_or("unknown")
|
||||||
fs::remove_dir_all(&apps_dir.join(repo_name)).expect("Failed to remove app directory");
|
.to_string()
|
||||||
println!("Removed app: {}", app.repo);
|
|
||||||
} else {
|
|
||||||
println!("App not found: {}", index);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<App> {
|
||||||
|
let path = apps_file_path();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||||
|
let _ = fs::write(&path, "[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
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<App>, 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);
|
apps.remove(index);
|
||||||
save_apps(apps);
|
save_apps(apps);
|
||||||
|
} else {
|
||||||
|
eprintln!("App not found: {}", id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_app(apps: &mut Vec<App>, app: App) {
|
pub fn install_app(apps: &mut Vec<App>, app: App) {
|
||||||
let a2 = app.clone();
|
let path = apps_dir();
|
||||||
// Get home directory and create ~/sentryx/apps if needed
|
let repo_name = app.repo_folder_name();
|
||||||
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() {
|
// Ensure apps directory exists
|
||||||
fs::create_dir_all(&apps_dir).expect("Failed to create apps directory");
|
if let Err(e) = fs::create_dir_all(&path) {
|
||||||
|
eprintln!("Failed to create apps directory: {}", e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change to apps directory
|
if let Err(e) = env::set_current_dir(&path) {
|
||||||
env::set_current_dir(&apps_dir).expect("Failed to change to apps directory");
|
eprintln!("Failed to switch to apps directory: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
println!("Installing app from repo: {}", app.repo);
|
println!("Installing app: {}", app.id().expect("Unable to obtain id"));
|
||||||
println!("Branch: {}", app.branch);
|
println!("→ Cloning branch `{}` from `{}`", app.branch, app.repo);
|
||||||
println!("Install command: {}", app.install_command);
|
|
||||||
println!("Run command: {}", app.run_command);
|
|
||||||
|
|
||||||
// Clone the repo
|
let clone_status = Command::new("git")
|
||||||
println!("Cloning repo");
|
|
||||||
Command::new("git")
|
|
||||||
.arg("clone")
|
.arg("clone")
|
||||||
.arg("-b")
|
.arg("-b")
|
||||||
.arg(&app.branch)
|
.arg(&app.branch)
|
||||||
.arg(&app.repo)
|
.arg(&app.repo)
|
||||||
.status()
|
.status();
|
||||||
.expect("Failed to clone repository");
|
|
||||||
|
|
||||||
// Extract repo name from URL (e.g., "https://github.com/user/myapp.git" -> "myapp")
|
if !matches!(clone_status, Ok(s) if s.success()) {
|
||||||
let repo_name = app
|
eprintln!("Failed to clone repository");
|
||||||
.repo
|
return;
|
||||||
.trim_end_matches(".git")
|
}
|
||||||
.rsplit('/')
|
|
||||||
.next()
|
|
||||||
.expect("Failed to extract repo name");
|
|
||||||
|
|
||||||
// Change to cloned repo directory
|
let repo_path = path.join(&repo_name);
|
||||||
env::set_current_dir(&repo_name).expect("Failed to change to cloned repo directory");
|
if let Err(e) = env::set_current_dir(&repo_path) {
|
||||||
|
eprintln!("Failed to change into cloned repo directory: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// You are now inside the cloned repo
|
println!(
|
||||||
println!("Changed directory to {:?}", env::current_dir().unwrap());
|
"→ Running install command in {:?}",
|
||||||
|
env::current_dir().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
apps.push(a2.clone());
|
let install_status = Command::new("bash")
|
||||||
save_apps(&apps);
|
|
||||||
|
|
||||||
Command::new("bash")
|
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(app.install_command)
|
.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 toggle_app_state(id: &str, enable: bool) {
|
||||||
|
Command::new("systemctl")
|
||||||
|
.arg("--user")
|
||||||
|
.arg(if enable { "enable" } else { "disable" })
|
||||||
|
.arg(id)
|
||||||
.status()
|
.status()
|
||||||
.expect("Failed to execute Bash command");
|
.expect("Failed to toggle app enable state");
|
||||||
|
|
||||||
run_app(a2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_app(app: App) {
|
pub fn start_app(id: &str) {
|
||||||
// Change to the app directory
|
Command::new("systemctl")
|
||||||
let home = env::var("HOME").expect("Failed to get HOME environment variable");
|
.arg("--user")
|
||||||
let app_dir = PathBuf::from(home)
|
.arg("start")
|
||||||
.join("sentryx/apps")
|
.arg(id)
|
||||||
.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()
|
.status()
|
||||||
.expect("Failed to execute run command");
|
.expect("Failed to start app");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_apps() -> Vec<App> {
|
pub fn stop_app(id: &str) {
|
||||||
// Change to the app directory
|
Command::new("systemctl")
|
||||||
let home = env::var("HOME").expect("Failed to get HOME environment variable");
|
.arg("--user")
|
||||||
let apps_dir = PathBuf::from(home).join("sentryx/apps");
|
.arg("stop")
|
||||||
|
.arg(id)
|
||||||
if !apps_dir.exists() {
|
.status()
|
||||||
fs::create_dir_all(&apps_dir).expect("Failed to create apps directory");
|
.expect("Failed to stop app");
|
||||||
fs::write(apps_dir.join("apps.json"), "[]").expect("Failed to create apps.json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let apps_json = serde_json::from_str::<Vec<App>>(
|
pub fn restart_app(id: &str) {
|
||||||
&fs::read_to_string(apps_dir.join("apps.json")).expect("Failed to read apps.json"),
|
Command::new("systemctl")
|
||||||
)
|
.arg("restart")
|
||||||
.expect("Failed to parse apps.json");
|
.arg(id)
|
||||||
|
.status()
|
||||||
for app in apps_json.clone() {
|
.expect("Failed to restart app");
|
||||||
if app.enabled.unwrap_or(true) {
|
|
||||||
println!("Running app: {}", app.repo);
|
|
||||||
std::thread::spawn(|| {
|
|
||||||
run_app(app);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("Skipping disabled app: {}", app.repo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apps_json
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
fs::write(
|
|
||||||
apps_dir.join("apps.json"),
|
|
||||||
serde_json::to_string(apps).expect("Failed to serialize apps"),
|
|
||||||
)
|
|
||||||
.expect("Failed to write apps.json");
|
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-30
@@ -1,44 +1,23 @@
|
|||||||
pub mod app_manager;
|
pub mod app_manager;
|
||||||
pub mod server_config;
|
pub mod server_config;
|
||||||
|
|
||||||
|
use crate::app_manager::{install_app, App};
|
||||||
|
use crate::server_config::ServerStats;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
use std::{net::TcpListener, sync::Mutex};
|
use std::{net::TcpListener, sync::Mutex};
|
||||||
use sysinfo::{Disks, Networks, System};
|
use sysinfo::{Disks, Networks, System};
|
||||||
use tungstenite::accept;
|
use tungstenite::accept;
|
||||||
|
|
||||||
use crate::app_manager::{install_app, App};
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
use crate::server_config::ServerStats;
|
pub enum Method {
|
||||||
fn get_system_info(sys: &mut System, networks: &Networks, disks: &Disks) -> ServerStats {
|
Install(App),
|
||||||
let cpus = sys.cpus();
|
SetEnabled(usize, bool),
|
||||||
let total_cpu_usage: f32 = cpus.iter().map(|cpu| cpu.cpu_usage()).sum();
|
Edit(usize, App),
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let apps = app_manager::run_apps();
|
let apps = app_manager::init();
|
||||||
|
|
||||||
let server = TcpListener::bind("127.0.0.1:5273").unwrap();
|
let server = TcpListener::bind("127.0.0.1:5273").unwrap();
|
||||||
for stream in server.incoming() {
|
for stream in server.incoming() {
|
||||||
@@ -72,7 +51,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::thread::spawn(move || loop {
|
std::thread::spawn(move || loop {
|
||||||
let app: App = serde_json::from_str(
|
let mth: Method = serde_json::from_str(
|
||||||
&reader_socket
|
&reader_socket
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -83,7 +62,26 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
match mth {
|
||||||
|
Method::Install(app) => {
|
||||||
install_app(&mut apps, 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();
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ ws.onopen = () => {
|
|||||||
ws.send("my-key");
|
ws.send("my-key");
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
Uninstall: {
|
||||||
repo: "[email protected]:osui-rs/osui.git",
|
repo: "[email protected]:osui-rs/osui.git",
|
||||||
branch: "master",
|
branch: "master",
|
||||||
install_command: "",
|
install_command: "",
|
||||||
run_command: "cargo run",
|
run_command: "cargo run",
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user