use log::{debug, info, warn}; use rocket::{ get, post, 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; use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat}; use peach_lib::{dyndns_client, network_client, oled_client, sbot_client, stats_client}; use crate::utils::build_json_response; use crate::routes::authentication::Authenticated; use rocket::serde::json::Value; // HELPERS AND ROUTES FOR /device /// System statistics data. #[derive(Debug, Serialize)] pub struct DeviceContext { 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 stats_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 DeviceContext { pub fn build() -> DeviceContext { // convert result to Option, discard any error let cpu_stat_percent = stats_client::cpu_stats_percent().ok(); let load_average = stats_client::load_average().ok(); let mem_stats = stats_client::mem_stats().ok(); 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 stats_ping = match stats_client::ping() { Ok(_) => "ONLINE".to_string(), Err(_) => "OFFLINE".to_string(), }; let uptime = match stats_client::uptime() { Ok(mins) => mins, Err(_) => "Unavailable".to_string(), }; // serialize disk usage data into Vec let disk_usage_stats = match stats_client::disk_usage() { Ok(disks) => { let partitions: Vec = serde_json::from_str(disks.as_str()) .expect("Failed to deserialize disk_usage response"); partitions } 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); } } // parse the uptime string to a signed integer (for math) let uptime_parsed = uptime.parse::().ok(); // 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_is_online: bool; let sbot_is_online_result = sbot_client::is_sbot_online(); match sbot_is_online_result { Ok(val) => { sbot_is_online = val; } Err(_err) => { sbot_is_online = false; } } DeviceContext { back: None, cpu_stat_percent, disk_stats, flash_name: None, flash_msg: None, load_average, mem_stats, network_ping, oled_ping, stats_ping, dyndns_enabled, dyndns_is_online, config_is_valid, sbot_is_online, title: None, uptime: uptime_parsed, } } } #[get("/device")] pub fn device_stats(flash: Option, auth: Authenticated) -> Template { // assign context through context_builder call let mut context = DeviceContext::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("device", &context) } // HELPERS AND ROUTES FOR /device/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("/device/reboot")] pub fn reboot_cmd(auth: Authenticated) -> Flash { match reboot() { Ok(_) => Flash::success(Redirect::to("/shutdown"), "Rebooting the device"), Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to reboot the device"), } } /// JSON request handler for device reboot. #[post("/api/v1/device/reboot")] pub fn reboot_device(auth: Authenticated) -> Value { match reboot() { Ok(_) => { debug!("Going down for reboot..."); let status = "success".to_string(); let msg = "Going down for reboot.".to_string(); build_json_response(status, None, Some(msg)) } Err(_) => { warn!("Reboot failed"); let status = "error".to_string(); let msg = "Failed to reboot the device.".to_string(); build_json_response(status, None, Some(msg)) } } } // HELPERS AND ROUTES FOR /device/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("/device/shutdown")] pub fn shutdown_cmd(auth: Authenticated) -> Flash { match shutdown() { Ok(_) => Flash::success(Redirect::to("/shutdown"), "Shutting down the device"), Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to shutdown the device"), } } // shutdown the device #[post("/api/v1/device/shutdown")] pub fn shutdown_device(auth: Authenticated) -> Value { match shutdown() { Ok(_) => { debug!("Going down for shutdown..."); let status = "success".to_string(); let msg = "Going down for shutdown.".to_string(); build_json_response(status, None, Some(msg)) } Err(_) => { warn!("Shutdown failed"); let status = "error".to_string(); let msg = "Failed to shutdown the device.".to_string(); build_json_response(status, None, Some(msg)) } } } // HELPERS AND ROUTES FOR /shutdown #[derive(Debug, Serialize)] pub struct ShutdownContext { pub back: Option, pub flash_name: Option, pub flash_msg: Option, pub title: Option, } impl ShutdownContext { pub fn build() -> ShutdownContext { ShutdownContext { back: None, flash_name: None, flash_msg: None, title: None, } } } #[get("/shutdown")] pub fn shutdown_menu(flash: Option, auth: Authenticated) -> Template { let mut context = ShutdownContext::build(); context.back = Some("/".to_string()); context.title = Some("Shutdown Device".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("shutdown", &context) }