add services status

This commit is contained in:
2026-04-11 21:11:38 +02:00
parent 21fe914f18
commit d1f9238b2a
9 changed files with 730 additions and 20 deletions

View File

@@ -1,30 +1,43 @@
use ratatui::layout::Rect;
use ratatui::prelude::*;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Gauge};
use ratatui::style::Style;
use ratatui::widgets::{Block, Borders, Gauge, Paragraph};
use sysinfo::{Components, Disk, Disks, Networks, System};
use sysinfo::{Disks, System};
use crate::config::{Config, load};
use crate::docker;
use std::path::Path;
pub struct App {
sys: System,
config: Config,
services: Vec<bool>,
}
impl Default for App {
fn default() -> Self {
App {
sys: System::new_all(),
config: load("server.toml").unwrap(),
services: Default::default(),
}
}
}
impl App {
pub fn update_info(&mut self) {
pub async fn update_info(&mut self) {
self.sys.refresh_all();
self.services = docker::running_services(&self.config.services.clone())
.await
.unwrap();
}
pub fn draw(&self, frame: &mut Frame) {
let block = Block::default()
.title_top(Line::from("Server").centered().bold())
.title_top(Line::from("Server status").centered().bold())
.title_bottom(Line::from("press 'q' to exit").right_aligned().italic());
frame.render_widget(block, frame.area());
@@ -37,6 +50,8 @@ impl App {
let block = Block::default().title("Services").borders(Borders::ALL);
self.draw_services(frame, layout[0]);
frame.render_widget(block, layout[0]);
let disks_gauges = self.disks();
@@ -55,7 +70,9 @@ impl App {
self.draw_gauges(frame, layout_system[2], cpus_gauges, "CPU");
}
fn memory(&self) -> Vec<Gauge> {
fn memory(&self) -> Vec<Gauge<'_>> {
let mut gauges: Vec<Gauge<'_>> = Default::default();
let mem_total: f64 = ((self.sys.total_memory() >> 20) as f64) / 1024.0;
let mem_used: f64 = ((self.sys.used_memory() >> 20) as f64) / 1024.0;
@@ -65,28 +82,39 @@ impl App {
.label(format!("{mem_used:.2}/{mem_total:.2}Go"))
.ratio(mem_used / mem_total);
gauges.push(mem);
let swap_total: f64 = ((self.sys.total_swap() >> 20) as f64) / 1024.0;
let swap_used: f64 = ((self.sys.used_swap() >> 20) as f64) / 1024.0;
if swap_total <= 0.0 {
return gauges;
}
let swap = Gauge::default()
.block(Block::default().title("SWAP usage:"))
.gauge_style(Style::new().white().on_black().italic())
.label(format!("{swap_used:.2}/{swap_total:.2}Go"))
.ratio(swap_used / swap_total);
vec![mem, swap]
gauges.push(swap);
gauges
}
fn disks(&self) -> Vec<Gauge> {
fn disks(&self) -> Vec<Gauge<'_>> {
let mut gauges: Vec<Gauge> = Default::default();
for disk in &Disks::new_with_refreshed_list() {
if disk.mount_point().starts_with("/boot/") || disk.mount_point().starts_with("/tmp/") {
let total = ((disk.total_space() >> 20) as f64) / 1024.0;
let used = total - ((disk.available_space() >> 20) as f64) / 1024.0;
if self.config.disks.iter().all(|config_disk| {
disk.mount_point() != AsRef::<Path>::as_ref(config_disk.mount_point.as_str())
}) || total <= 0.0
{
continue;
}
let total = ((disk.total_space() >> 20) as f64) / 1024.0;
let used = total - ((disk.available_space() >> 20) as f64) / 1024.0;
let gauge = Gauge::default()
.block(Block::default().title(format!(
"{:?} ({:?})",
@@ -103,10 +131,10 @@ impl App {
gauges
}
fn cpus(&self) -> Vec<Gauge> {
fn cpus(&self) -> Vec<Gauge<'_>> {
let mut gauges: Vec<Gauge> = Default::default();
for (idx, cpu) in self.sys.cpus().into_iter().enumerate() {
for cpu in self.sys.cpus() {
let usage = cpu.cpu_usage() as f64;
let gauge = Gauge::default()
.gauge_style(Style::new().white().on_black().italic())
@@ -119,6 +147,28 @@ impl App {
gauges
}
fn draw_services(&self, frame: &mut Frame, area: Rect) {
let layout = Layout::vertical(vec![Constraint::Length(2); self.config.services.len()])
.margin(1)
.split(area);
for (idx, (service, status)) in self
.config
.services
.iter()
.zip(self.services.iter())
.enumerate()
{
let display = Paragraph::new(service.description.clone()).block(
Block::default()
.title_top(Line::from(service.name.clone()).bold())
.title_top(Line::from(format!("(running: {status})")).right_aligned()),
);
frame.render_widget(display, layout[idx]);
}
}
fn draw_gauges(&self, frame: &mut Frame, area: Rect, gauges: Vec<Gauge>, name: &str) {
let block = Block::default().title(name).borders(Borders::ALL);

View File

@@ -1,5 +1,8 @@
//extern crate termios;
use ssh_status::config;
use ssh_status::docker;
use ssh_status::runner::{Action, Runner};
use std::io::{self, Write};
use std::time::Duration;
@@ -7,7 +10,8 @@ use crossterm::cursor;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
fn main() {
#[tokio::main]
async fn main() {
let mut stdout = io::stdout();
crossterm::terminal::enable_raw_mode().unwrap();
crossterm::execute!(
@@ -21,10 +25,10 @@ fn main() {
let mut app = Runner::new(stdout).unwrap();
let (w, h) = crossterm::terminal::size().unwrap();
app.resize(w as u32, h as u32).unwrap();
app.update();
app.update().await;
loop {
if crossterm::event::poll(Duration::from_millis(100)).unwrap() {
if crossterm::event::poll(Duration::from_millis(1000)).unwrap() {
match crossterm::event::read().unwrap() {
Event::Key(key) => match key.code {
KeyCode::Char(c) => {
@@ -41,7 +45,7 @@ fn main() {
}
};
app.update();
app.update().await;
}
let mut stdout = io::stdout();

32
src/config.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::path::Path;
use std::{fs, io};
use std::collections::HashMap;
use serde::Deserialize;
use toml;
#[derive(Deserialize, Debug, Clone)]
pub struct Config {
pub name: String,
pub disks: Vec<Disk>,
pub services: Vec<Service>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Service {
pub name: String,
pub description: String,
pub containers: Vec<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Disk {
pub name: String,
pub mount_point: String,
}
pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Config> {
let config_str = fs::read_to_string(path)?;
Ok(toml::from_str(&config_str).unwrap())
}

47
src/docker.rs Normal file
View File

@@ -0,0 +1,47 @@
use bollard::models::ContainerSummary;
use bollard::{Docker, errors::Error};
use crate::config::Service;
use std::collections::HashMap;
use std::default::Default;
pub async fn running_containers() -> Result<Vec<String>, Error> {
let mut names: Vec<String> = Default::default();
let docker = Docker::connect_with_socket_defaults()?;
let mut list_container_filters = HashMap::new();
list_container_filters.insert(String::from("status"), vec![String::from("running")]);
let containers = &docker
.list_containers(Some(
bollard::query_parameters::ListContainersOptionsBuilder::default()
.all(true)
.filters(&list_container_filters)
.build(),
))
.await?;
for c in containers {
if let Some(c_names) = &c.names {
for name in c_names {
names.push(name.clone());
}
}
}
Ok(names)
}
pub async fn running_services(services: &Vec<Service>) -> Result<Vec<bool>, Error> {
let running = running_containers().await?;
let mut status: Vec<bool> = Default::default();
for service in services.iter() {
let service_status: bool = service.containers.iter().all(|name| running.contains(name));
status.push(service_status);
}
Ok(status)
}

View File

@@ -1,3 +1,5 @@
pub mod app;
pub mod config;
pub mod docker;
pub mod runner;
pub mod server;

View File

@@ -39,8 +39,8 @@ impl<W: std::io::Write> Runner<W> {
}
}
pub fn update(&mut self) {
self.app.update_info();
pub async fn update(&mut self) {
self.app.update_info().await;
self.terminal.draw(|frame| self.app.draw(frame)).unwrap();
}

View File

@@ -27,7 +27,7 @@ impl AppServer {
let clients = self.clients.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
for (_, app) in clients.lock().await.iter_mut() {
app.update();