use log::info; use rocket::{ get, request::FlashMessage, response::{Flash, Redirect}, }; use rocket_dyn_templates::Template; use serde::Serialize; use std::{ io, process::{Command, Output}, }; use peach_lib::{ config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus, }; use peach_stats::{ stats, stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat}, }; use crate::routes::authentication::Authenticated; // HELPERS AND ROUTES FOR /status /// System statistics data. #[derive(Debug, Serialize)] pub struct StatusContext { pub back: Option, pub cpu_stat_percent: Option, pub disk_stats: Vec, pub flash_name: Option, pub flash_msg: Option, pub load_average: Option, pub mem_stats: Option, pub network_ping: String, pub oled_ping: String, pub dyndns_enabled: bool, pub dyndns_is_online: bool, pub config_is_valid: bool, pub sbot_is_online: bool, pub title: Option, pub uptime: Option, } impl StatusContext { pub fn build() -> StatusContext { // convert result to Option, discard any error let cpu_stat_percent = stats::cpu_stats_percent().ok(); let load_average = stats::load_average().ok(); let mem_stats = stats::mem_stats().ok(); // TODO: add `wpa_supplicant_status` to peach_network to replace this ping call // instead of: "is the network json-rpc server running?", we want to ask: // "is the wpa_supplicant systemd service functioning correctly?" let network_ping = match network_client::ping() { Ok(_) => "ONLINE".to_string(), Err(_) => "OFFLINE".to_string(), }; let oled_ping = match oled_client::ping() { Ok(_) => "ONLINE".to_string(), Err(_) => "OFFLINE".to_string(), }; let uptime = match stats::uptime() { Ok(secs) => { let uptime_mins = secs / 60; uptime_mins.to_string() } Err(_) => "Unavailable".to_string(), }; // parse the uptime string to a signed integer (for math) let uptime_parsed = uptime.parse::().ok(); // serialize disk usage data into Vec let disk_usage_stats = match stats::disk_usage() { Ok(disks) => disks, Err(_) => Vec::new(), }; let mut disk_stats = Vec::new(); // select only the partition we're interested in: /dev/mmcblk0p2 ("/") for disk in disk_usage_stats { if disk.mountpoint == "/" { disk_stats.push(disk); } } // dyndns_is_online & config_is_valid let dyndns_enabled: bool; let dyndns_is_online: bool; let config_is_valid: bool; let load_peach_config_result = load_peach_config(); match load_peach_config_result { Ok(peach_config) => { dyndns_enabled = peach_config.dyn_enabled; config_is_valid = true; if dyndns_enabled { let is_dyndns_online_result = dyndns_client::is_dns_updater_online(); match is_dyndns_online_result { Ok(is_online) => { dyndns_is_online = is_online; } Err(_err) => { dyndns_is_online = false; } } } else { dyndns_is_online = false; } } Err(_err) => { dyndns_enabled = false; dyndns_is_online = false; config_is_valid = false; } } // test if go-sbot is running let sbot_status = SbotStatus::read(); let sbot_is_online: bool = match sbot_status { // return true if state is active Ok(status) => matches!(status.state == Some("active".to_string()), true), _ => false, }; StatusContext { back: None, cpu_stat_percent, disk_stats, flash_name: None, flash_msg: None, load_average, mem_stats, network_ping, oled_ping, dyndns_enabled, dyndns_is_online, config_is_valid, sbot_is_online, title: None, uptime: uptime_parsed, } } } #[get("/")] pub fn device_status(flash: Option, _auth: Authenticated) -> Template { // assign context through context_builder call let mut context = StatusContext::build(); context.back = Some("/".to_string()); context.title = Some("Device Status".to_string()); // check to see if there is a flash message to display if let Some(flash) = flash { // add flash message contents to the context object context.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; // template_dir is set in Rocket.toml Template::render("status/device", &context) } // HELPERS AND ROUTES FOR /power/reboot /// Executes a system command to reboot the device immediately. pub fn reboot() -> io::Result { info!("Rebooting the device"); // ideally, we'd like to reboot after 5 seconds to allow time for JSON // response but this is not possible with the `shutdown` command alone. // TODO: send "rebooting..." message to `peach-oled` for display Command::new("sudo") .arg("shutdown") .arg("-r") .arg("now") .output() } #[get("/power/reboot")] pub fn reboot_cmd(_auth: Authenticated) -> Flash { match reboot() { Ok(_) => Flash::success(Redirect::to("/power"), "Rebooting the device"), Err(_) => Flash::error(Redirect::to("/power"), "Failed to reboot the device"), } } // HELPERS AND ROUTES FOR /power/shutdown /// Executes a system command to shutdown the device immediately. pub fn shutdown() -> io::Result { info!("Shutting down the device"); // ideally, we'd like to reboot after 5 seconds to allow time for JSON // response but this is not possible with the `shutdown` command alone. // TODO: send "shutting down..." message to `peach-oled` for display Command::new("sudo").arg("shutdown").arg("now").output() } #[get("/power/shutdown")] pub fn shutdown_cmd(_auth: Authenticated) -> Flash { match shutdown() { Ok(_) => Flash::success(Redirect::to("/power"), "Shutting down the device"), Err(_) => Flash::error(Redirect::to("/power"), "Failed to shutdown the device"), } } // HELPERS AND ROUTES FOR /power #[derive(Debug, Serialize)] pub struct PowerContext { pub back: Option, pub flash_name: Option, pub flash_msg: Option, pub title: Option, } impl PowerContext { pub fn build() -> PowerContext { PowerContext { back: None, flash_name: None, flash_msg: None, title: None, } } } #[get("/power")] pub fn power_menu(flash: Option, _auth: Authenticated) -> Template { let mut context = PowerContext::build(); context.back = Some("/".to_string()); context.title = Some("Power Menu".to_string()); // check to see if there is a flash message to display if let Some(flash) = flash { // add flash message contents to the context object context.flash_name = Some(flash.kind().to_string()); context.flash_msg = Some(flash.message().to_string()); }; Template::render("power", &context) }