Files
oxkerclone/src/app_data/mod.rs
T

593 lines
21 KiB
Rust

use bollard::models::ContainerSummary;
use core::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use tui::widgets::ListItem;
mod container_state;
use crate::{app_error::AppError, parse_args::CliArgs, ui::log_sanitizer, ENTRY_POINT};
pub use container_state::*;
/// Global app_state, stored in an Arc<Mutex>
#[derive(Debug, Clone)]
pub struct AppData {
error: Option<AppError>,
logs_parsed: bool,
sorted_by: Option<(Header, SortedOrder)>,
pub args: CliArgs,
pub containers: StatefulList<ContainerItem>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SortedOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum Header {
State,
Status,
Cpu,
Memory,
Id,
Name,
Image,
Rx,
Tx,
}
/// Convert Header enum into strings to display
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::State => "state",
Self::Status => "status",
Self::Cpu => "cpu",
Self::Memory => "memory/limit",
Self::Id => "id",
Self::Name => "name",
Self::Image => "image",
Self::Rx => "↓ rx",
Self::Tx => "↑ tx",
};
write!(f, "{disp:>x$}", x = f.width().unwrap_or(1))
}
}
impl AppData {
/// Generate a default app_state
pub fn default(args: CliArgs) -> Self {
Self {
args,
containers: StatefulList::new(vec![]),
error: None,
logs_parsed: false,
sorted_by: None,
}
}
pub const fn get_sorted(&self) -> Option<(Header, SortedOrder)> {
self.sorted_by
}
/// Remove the sorted header & order, and sort by default - created datetime
pub fn reset_sorted(&mut self) {
self.set_sorted(None);
}
/// Sort containers based on a given header, if headings match, and already ascending, remove sorting
pub fn set_sort_by_header(&mut self, selected_header: Header) {
let mut output = Some((selected_header, SortedOrder::Asc));
if let Some((current_header, order)) = self.get_sorted() {
if current_header == selected_header {
match order {
SortedOrder::Desc => output = None,
SortedOrder::Asc => output = Some((selected_header, SortedOrder::Desc)),
}
}
}
self.set_sorted(output);
}
/// Change the sorted order, also set the selected container state to match new order
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
self.sorted_by = x;
self.sort_containers();
self.containers
.state
.select(self.containers.items.iter().position(|i| {
self.get_selected_container_id()
.map_or(false, |id| i.id == id)
}));
}
/// Current time as unix timestamp
#[allow(clippy::expect_used)]
fn get_systemtime() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("In our known reality, this error should never occur")
.as_secs()
}
/// Get the current select docker command
/// So know which command to execute
pub fn get_docker_command(&self) -> Option<DockerControls> {
let mut output = None;
if let Some(index) = self.containers.state.selected() {
if let Some(control_index) = self.containers.items[index]
.docker_controls
.state
.selected()
{
output = Some(self.containers.items[index].docker_controls.items[control_index]);
}
}
output
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_next(&mut self) {
if let Some(index) = self.containers.state.selected() {
if let Some(i) = self.containers.items.get_mut(index) {
i.docker_controls.next();
}
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_previous(&mut self) {
if let Some(index) = self.containers.state.selected() {
if let Some(i) = self.containers.items.get_mut(index) {
i.docker_controls.previous();
}
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_start(&mut self) {
if let Some(index) = self.containers.state.selected() {
if let Some(i) = self.containers.items.get_mut(index) {
i.docker_controls.start();
}
}
}
/// Change selected choice of docker commands of selected container
pub fn docker_command_end(&mut self) {
if let Some(index) = self.containers.state.selected() {
if let Some(i) = self.containers.items.get_mut(index) {
i.docker_controls.end();
}
}
}
/// return single app_state error
pub const fn get_error(&self) -> Option<AppError> {
self.error
}
/// remove single app_state error
pub fn remove_error(&mut self) {
self.error = None;
}
/// insert single app_state error
pub fn set_error(&mut self, error: AppError) {
self.error = Some(error);
}
/// Find the id of the currently selected container.
/// If any containers on system, will always return a ContainerId
/// Only returns None when no containers found.
pub fn get_selected_container_id(&self) -> Option<ContainerId> {
let mut output = None;
if let Some(index) = self.containers.state.selected() {
if let Some(x) = self.containers.items.get(index) {
output = Some(x.id.clone());
}
}
output
}
/// Check if the selected container is a dockerised version of oxker
/// So that can disallow commands to be send
/// Is a poor way of implementing this
pub fn selected_container_is_oxker(&self) -> bool {
if let Some(index) = self.containers.state.selected() {
if let Some(x) = self.containers.items.get(index) {
return x.is_oxker;
}
}
false
}
/// Sort the containers vec, based on a heading, either ascending or descending,
/// If not sort set, then sort by created time
pub fn sort_containers(&mut self) {
if let Some((head, ord)) = self.sorted_by {
match head {
Header::State => match ord {
SortedOrder::Asc => self
.containers
.items
.sort_by(|a, b| b.state.order().cmp(&a.state.order())),
SortedOrder::Desc => self
.containers
.items
.sort_by(|a, b| a.state.order().cmp(&b.state.order())),
},
Header::Status => match ord {
SortedOrder::Asc => self
.containers
.items
.sort_by(|a, b| a.status.cmp(&b.status)),
SortedOrder::Desc => self
.containers
.items
.sort_by(|a, b| b.status.cmp(&a.status)),
},
Header::Cpu => match ord {
SortedOrder::Asc => self
.containers
.items
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
SortedOrder::Desc => self
.containers
.items
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
},
Header::Memory => match ord {
SortedOrder::Asc => self
.containers
.items
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
SortedOrder::Desc => self
.containers
.items
.sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
},
Header::Id => match ord {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
},
Header::Image => match ord {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
SortedOrder::Desc => {
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
}
},
Header::Name => match ord {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
},
Header::Rx => match ord {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
},
Header::Tx => match ord {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
},
}
} else {
self.containers
.items
.sort_by(|a, b| a.created.cmp(&b.created));
}
}
/// Find the index of the currently selected single log line
pub fn get_selected_log_index(&self) -> Option<usize> {
let mut output = None;
if let Some(id) = self.get_selected_container_id() {
if let Some(index) = self.containers.items.iter().position(|i| i.id == id) {
output = Some(index);
}
}
output
}
/// Get the title for log panel for selected container
/// will be either
/// 1) "logs x/x - container_name" where container_name is 32 chars max
/// 2) "logs - container_name" when no logs found, again 32 chars max
pub fn get_log_title(&self) -> String {
self.get_selected_log_index()
.map_or(String::new(), |index| {
let logs_len = self.containers.items[index].logs.get_state_title();
let mut name = self.containers.items[index].name.clone();
name.truncate(32);
if logs_len.is_empty() {
format!("- {name} ")
} else {
format!("{logs_len} - {name}")
}
})
}
/// select next selected log line
pub fn log_next(&mut self) {
if let Some(index) = self.get_selected_log_index() {
if let Some(i) = self.containers.items.get_mut(index) {
i.logs.next();
}
}
}
/// select previous selected log line
pub fn log_previous(&mut self) {
if let Some(index) = self.get_selected_log_index() {
if let Some(i) = self.containers.items.get_mut(index) {
i.logs.previous();
}
}
}
/// select last selected log line
pub fn log_end(&mut self) {
if let Some(index) = self.get_selected_log_index() {
if let Some(i) = self.containers.items.get_mut(index) {
i.logs.end();
}
}
}
/// select first selected log line
pub fn log_start(&mut self) {
if let Some(index) = self.get_selected_log_index() {
if let Some(i) = self.containers.items.get_mut(index) {
i.logs.start();
}
}
}
/// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec
pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> bool {
let count_is_running = all_ids.iter().filter(|i| i.0).count();
let number_with_cpu_status = self
.containers
.items
.iter()
.filter(|i| !i.cpu_stats.is_empty())
.count();
self.logs_parsed && count_is_running == number_with_cpu_status
}
/// Just get the total number of containers
pub fn get_container_len(&self) -> usize {
self.containers.items.len()
}
/// Find the widths for the strings in the containers panel.
/// So can display nicely and evenly
pub fn get_width(&self) -> Columns {
let mut output = Columns::new();
let count = |x: &String| x.chars().count();
// Should probably find a refactor here somewhere
for container in &self.containers.items {
let cpu_count = count(
&container
.cpu_stats
.back()
.unwrap_or(&CpuStats::default())
.to_string(),
);
let mem_count = count(&format!(
"{} / {}",
container.mem_stats.back().unwrap_or(&ByteStats::default()),
container.mem_limit
));
let rx_count = count(&container.rx.to_string());
let tx_count = count(&container.tx.to_string());
let image_count = count(&container.image);
let name_count = count(&container.name);
let state_count = count(&container.state.to_string());
let status_count = count(&container.status);
if cpu_count > output.cpu.1 {
output.cpu.1 = cpu_count;
};
if image_count > output.image.1 {
output.image.1 = image_count;
};
if mem_count > output.mem.1 {
output.mem.1 = mem_count;
};
if name_count > output.name.1 {
output.name.1 = name_count;
};
if state_count > output.state.1 {
output.state.1 = state_count;
};
if status_count > output.status.1 {
output.status.1 = status_count;
};
if rx_count > output.net_rx.1 {
output.net_rx.1 = rx_count;
};
if tx_count > output.net_tx.1 {
output.net_tx.1 = tx_count;
};
}
output
}
/// Get all containers ids
pub fn get_all_ids(&self) -> Vec<ContainerId> {
self.containers
.items
.iter()
.map(|i| i.id.clone())
.collect::<Vec<_>>()
}
/// return a mutable container by given id
fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
self.containers.items.iter_mut().find(|i| &i.id == id)
}
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
/// Will also, if a sort is set, sort the containers
pub fn update_stats(
&mut self,
id: &ContainerId,
cpu_stat: Option<f64>,
mem_stat: Option<u64>,
mem_limit: u64,
rx: u64,
tx: u64,
) {
if let Some(container) = self.get_container_by_id(id) {
if container.cpu_stats.len() >= 60 {
container.cpu_stats.pop_front();
}
if container.mem_stats.len() >= 60 {
container.mem_stats.pop_front();
}
if let Some(cpu) = cpu_stat {
container.cpu_stats.push_back(CpuStats::new(cpu));
}
if let Some(mem) = mem_stat {
container.mem_stats.push_back(ByteStats::new(mem));
}
container.rx.update(rx);
container.tx.update(tx);
container.mem_limit.update(mem_limit);
}
// need to benchmark this?
if self.get_sorted().is_some() {
self.sort_containers();
}
}
/// Update, or insert, containers
pub fn update_containers(&mut self, all_containers: &mut [ContainerSummary]) {
let all_ids = self.get_all_ids();
// Only sort it no containers currently set, as afterwards the order is fixed
if self.containers.items.is_empty() {
all_containers.sort_by(|a, b| a.created.cmp(&b.created));
}
if !all_containers.is_empty() && self.containers.state.selected().is_none() {
self.containers.start();
}
for (index, id) in all_ids.iter().enumerate() {
if !all_containers
.iter()
.filter_map(|i| i.id.as_ref())
.any(|x| x == id.get())
{
// If removed container is currently selected, then change selected to previous
// This will default to 0 in any edge cases
if self.containers.state.selected().is_some() {
self.containers.previous();
}
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
if self.containers.items.get(index).is_some() {
self.containers.items.remove(index);
}
}
}
// Trim a &String and return String
let trim_owned = |x: &String| x.trim().to_owned();
for i in all_containers {
if let Some(id) = i.id.as_ref() {
let name = i.names.as_mut().map_or(String::new(), |names| {
names.first_mut().map_or(String::new(), |f| {
if f.starts_with('/') {
f.remove(0);
}
(*f).to_string()
})
});
let is_oxker = i
.command
.as_ref()
.map_or(false, |i| i.starts_with(ENTRY_POINT));
let state = State::from(i.state.as_ref().map_or("dead".to_owned(), trim_owned));
let status = i.status.as_ref().map_or(String::new(), trim_owned);
let image = i
.image
.as_ref()
.map_or(String::new(), std::clone::Clone::clone);
let id = ContainerId::from(id);
let created = i.created.map_or(0, |i| u64::try_from(i).unwrap_or(0));
// If container info already in containers Vec, then just update details
if let Some(item) = self.get_container_by_id(&id) {
if item.name != name {
item.name = name;
};
if item.status != status {
item.status = status;
};
if item.state != state {
item.docker_controls.items = DockerControls::gen_vec(state);
// Update the list state, needs to be None if the gen_vec returns an empty vec
match state {
State::Removing | State::Restarting | State::Unknown => {
item.docker_controls.state.select(None);
}
_ => item.docker_controls.start(),
};
item.state = state;
};
if item.image != image {
item.image = image;
};
// else container not known, so make new ContainerItem and push into containers Vec
} else {
let container =
ContainerItem::new(created, id, image, is_oxker, name, state, status);
self.containers.items.push(container);
}
}
}
}
/// update logs of a given container, based on id
pub fn update_log_by_id(&mut self, output: &[String], id: &ContainerId) {
let tz = Self::get_systemtime();
let color = self.args.color;
let raw = self.args.raw;
if let Some(container) = self.get_container_by_id(id) {
container.last_updated = tz;
let current_len = container.logs.items.len();
for i in output {
let lines = if color {
log_sanitizer::colorize_logs(i)
} else if raw {
log_sanitizer::raw(i)
} else {
log_sanitizer::remove_ansi(i)
};
container.logs.items.push(ListItem::new(lines));
}
// Set the logs selected row for each container
// Either when no long currently selected, or currently selected (before updated) is already at end
if container.logs.state.selected().is_none()
|| container.logs.state.selected().map_or(1, |f| f + 1) == current_len
{
container.logs.end();
}
}
self.logs_parsed = true;
}
}