Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Server Manager - Next-Gen Media Server Orchestrator 🚀

![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.7-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue)
![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.8-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue)

**Server Manager** is a powerful and intelligent tool written in Rust to deploy, manage, and optimize a complete personal media and cloud server stack. It detects your hardware and automatically configures 28 Docker services for optimal performance.

Expand Down
2 changes: 1 addition & 1 deletion server_manager/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server_manager/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "server_manager"
version = "1.0.7"
version = "1.0.8"
edition = "2021"

[dependencies]
Expand Down
141 changes: 99 additions & 42 deletions server_manager/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use log::info;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::SystemTime;
use tokio::sync::RwLock;
Expand All @@ -12,6 +12,7 @@ use tokio::sync::RwLock;
struct CachedConfig {
config: Config,
last_mtime: Option<SystemTime>,
loaded_path: Option<PathBuf>,
}

static CONFIG_CACHE: OnceLock<RwLock<CachedConfig>> = OnceLock::new();
Expand All @@ -25,8 +26,18 @@ pub struct Config {
impl Config {
pub fn load() -> Result<Self> {
let path = Path::new("config.yaml");
if path.exists() {
let content = fs::read_to_string(path).context("Failed to read config.yaml")?;
let fallback_path = Path::new("/opt/server_manager/config.yaml");

let load_path = if path.exists() {
Some(path)
} else if fallback_path.exists() {
Some(fallback_path)
} else {
None
};

if let Some(p) = load_path {
let content = fs::read_to_string(p).context("Failed to read config.yaml")?;
// If empty file, return default
if content.trim().is_empty() {
return Ok(Config::default());
Expand All @@ -42,70 +53,116 @@ impl Config {
RwLock::new(CachedConfig {
config: Config::default(),
last_mtime: None,
loaded_path: None,
})
});

// Determine current priority path
let path = Path::new("config.yaml");
let fallback_path = Path::new("/opt/server_manager/config.yaml");

// Check for existence asynchronously
let current_path = if tokio::fs::try_exists(path).await.unwrap_or(false) {
Some(path.to_path_buf())
} else if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) {
Some(fallback_path.to_path_buf())
} else {
None
};

// Fast path: Optimistic read
{
let guard = cache.read().await;
if let Some(cached_mtime) = guard.last_mtime {
// Check if file still matches
if let Ok(metadata) = tokio::fs::metadata("config.yaml").await {
if let Ok(modified) = metadata.modified() {
if modified == cached_mtime {
return Ok(guard.config.clone());
if let Some(p) = &current_path {
// If we are looking at the same file as cached
if guard.loaded_path.as_ref() == Some(p) {
if let Some(cached_mtime) = guard.last_mtime {
// Check if file still matches
if let Ok(metadata) = tokio::fs::metadata(p).await {
if let Ok(modified) = metadata.modified() {
if modified == cached_mtime {
return Ok(guard.config.clone());
}
}
}
}
}
} else if guard.loaded_path.is_none() {
// No file exists and cache has no file -> return default
return Ok(guard.config.clone());
}
}

// Slow path: Update cache
let mut guard = cache.write().await;

// Check metadata again (double-checked locking pattern)
let metadata_res = tokio::fs::metadata("config.yaml").await;

match metadata_res {
Ok(metadata) => {
let modified = metadata.modified().unwrap_or(SystemTime::now());

if let Some(cached_mtime) = guard.last_mtime {
if modified == cached_mtime {
return Ok(guard.config.clone());
// Re-evaluate path under lock (though unlikely to race in this context, good practice)
let current_path_2 = if tokio::fs::try_exists(path).await.unwrap_or(false) {
Some(path.to_path_buf())
} else if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) {
Some(fallback_path.to_path_buf())
} else {
None
};

if let Some(p) = current_path_2 {
let metadata_res = tokio::fs::metadata(&p).await;
match metadata_res {
Ok(metadata) => {
let modified = metadata.modified().unwrap_or(SystemTime::now());

// Check if cache is already up to date for this path
if guard.loaded_path.as_ref() == Some(&p) {
if let Some(cached_mtime) = guard.last_mtime {
if modified == cached_mtime {
return Ok(guard.config.clone());
}
}
}
}

// Load file
match tokio::fs::read_to_string("config.yaml").await {
Ok(content) => {
let config = if content.trim().is_empty() {
Config::default()
} else {
serde_yaml_ng::from_str(&content)
.context("Failed to parse config.yaml")?
};

guard.config = config.clone();
guard.last_mtime = Some(modified);
Ok(config)
// Load file
match tokio::fs::read_to_string(&p).await {
Ok(content) => {
let config = if content.trim().is_empty() {
Config::default()
} else {
serde_yaml_ng::from_str(&content)
.context("Failed to parse config.yaml")?
};

guard.config = config.clone();
guard.last_mtime = Some(modified);
guard.loaded_path = Some(p);
Ok(config)
}
Err(e) => Err(anyhow::Error::new(e).context("Failed to read config.yaml")),
}
Err(e) => Err(anyhow::Error::new(e).context("Failed to read config.yaml")),
}
Err(e) => Err(anyhow::Error::new(e).context("Failed to read config metadata")),
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// File not found -> Default
guard.config = Config::default();
guard.last_mtime = None;
Ok(guard.config.clone())
}
Err(e) => Err(anyhow::Error::new(e).context("Failed to read config metadata")),
} else {
// File not found -> Default
guard.config = Config::default();
guard.last_mtime = None;
guard.loaded_path = None;
Ok(guard.config.clone())
}
}

pub fn save(&self) -> Result<()> {
// Prefer saving to /opt/server_manager if it exists/is writable (checked by parent dir existence), else CWD
let target = if Path::new("/opt/server_manager").exists() {
Path::new("/opt/server_manager/config.yaml")
} else {
Path::new("config.yaml")
};

let content = serde_yaml_ng::to_string(self)?;
fs::write("config.yaml", content).context("Failed to write config.yaml")?;
fs::write(target, content).context("Failed to write config.yaml")?;

// Invalidate cache implicitly?
// Ideally we should update the cache here or invalidate it.
// Since the cache checks mtime, it will pick up the change on next read.
Ok(())
}

Expand Down
83 changes: 46 additions & 37 deletions server_manager/src/interface/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,43 +331,52 @@ async fn dashboard(State(state): State<SharedState>, session: Session) -> impl I
let config = state.get_config().await;

// System Stats
let mut sys = state.system.lock().unwrap();
let now = SystemTime::now();
let mut last_refresh = state.last_system_refresh.lock().unwrap();

// Throttle refresh to max once every 500ms
if now
.duration_since(*last_refresh)
.unwrap_or_default()
.as_millis()
> 500
{
sys.refresh_cpu();
sys.refresh_memory();
sys.refresh_disks();
*last_refresh = now;
}
let ram_used = sys.used_memory() / 1024 / 1024; // MB
let ram_total = sys.total_memory() / 1024 / 1024; // MB
let swap_used = sys.used_swap() / 1024 / 1024; // MB
let swap_total = sys.total_swap() / 1024 / 1024; // MB
let cpu_usage = sys.global_cpu_info().cpu_usage();

// Simple Disk Usage (Root or fallback)
let mut disk_total = 0;
let mut disk_used = 0;

let target_disk = sys
.disks()
.iter()
.find(|d| d.mount_point() == std::path::Path::new("/"))
.or_else(|| sys.disks().first());

if let Some(disk) = target_disk {
disk_total = disk.total_space() / 1024 / 1024 / 1024; // GB
disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; // GB
}
drop(sys); // Release lock explicitely
let state_clone = state.clone();
let (ram_used, ram_total, swap_used, swap_total, cpu_usage, disk_total, disk_used) =
tokio::task::spawn_blocking(move || {
let mut sys = state_clone.system.lock().unwrap();
let now = SystemTime::now();
let mut last_refresh = state_clone.last_system_refresh.lock().unwrap();

// Throttle refresh to max once every 500ms
if now
.duration_since(*last_refresh)
.unwrap_or_default()
.as_millis()
> 500
{
sys.refresh_cpu();
sys.refresh_memory();
sys.refresh_disks();
*last_refresh = now;
}
let ram_used = sys.used_memory() / 1024 / 1024; // MB
let ram_total = sys.total_memory() / 1024 / 1024; // MB
let swap_used = sys.used_swap() / 1024 / 1024; // MB
let swap_total = sys.total_swap() / 1024 / 1024; // MB
let cpu_usage = sys.global_cpu_info().cpu_usage();

// Simple Disk Usage (Root or fallback)
let mut disk_total = 0;
let mut disk_used = 0;

let target_disk = sys
.disks()
.iter()
.find(|d| d.mount_point() == std::path::Path::new("/"))
.or_else(|| sys.disks().first());

if let Some(disk) = target_disk {
disk_total = disk.total_space() / 1024 / 1024 / 1024; // GB
disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; // GB
}

(
ram_used, ram_total, swap_used, swap_total, cpu_usage, disk_total, disk_used,
)
})
.await
.unwrap_or_default();

let mut html = String::with_capacity(8192);
write_html_head(&mut html, "Dashboard - Server Manager");
Expand Down
2 changes: 1 addition & 1 deletion server_manager/src/services/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ impl Service for NextcloudService {
return Ok(());
}

let escape_php = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
let escape_php = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"").replace('$', "\\$");
let db_pass = escape_php(&secrets.nextcloud_db_password.clone().unwrap_or_default());
let admin_pass = escape_php(&secrets.nextcloud_admin_password.clone().unwrap_or_default());

Expand Down