Began working on populating fields.

This commit is contained in:
ClementTsang 2019-09-09 00:09:58 -04:00
parent 471209f511
commit ff89f1187f
6 changed files with 193 additions and 99 deletions

View File

@ -1,18 +1,34 @@
# rustop # rustop
A gotop clone, written in Rust. Mostly done in an effort to learn Rust, while also potentially making a tool that people might want to use. A [gotop](https://github.com/cjbassi/gotop) clone, written in Rust.
## Features ## Installation
* Support for Linux. ### Linux
TODO: Write
### Windows
TODO: Test
### MacOS
Currently, I'm unable to test on MacOS, so I'm not sure how well this will work, if at all. I'll try to source MacOS hardware to test this application.
## Thanks ## Thanks
* As mentioned, this project is most definitely inspired by [gotop](https://github.com/cjbassi/gotop). * As mentioned, this project is most definitely inspired by [gotop](https://github.com/cjbassi/gotop).
* This application was written with the following libraries: * This application was written with the following libraries:
* [crossterm](https://github.com/TimonPost/crossterm)
* [heim](https://github.com/heim-rs/heim) * [heim](https://github.com/heim-rs/heim)
* [sysinfo](https://github.com/GuillaumeGomez/sysinfo) * [sysinfo](https://github.com/GuillaumeGomez/sysinfo)
* [termion](https://github.com/redox-os/termion)
* [tokio](https://github.com/tokio-rs/tokio) * [tokio](https://github.com/tokio-rs/tokio)
* [tui-rs](https://github.com/fdehau/tui-rs) * [tui-rs](https://github.com/fdehau/tui-rs)
## Why
I was looking to try writing more things in Rust, and I love the gotop tool. And thus, this project was born.
Also, yes, I realize that gotop has a Rust rewrite branch... I found out about that when I was nearly ready to release the first version.

View File

@ -1,14 +1,11 @@
#![feature(async_closure)]
use crossterm::{input, AlternateScreen, InputEvent, KeyEvent}; use crossterm::{input, AlternateScreen, InputEvent, KeyEvent};
use std::{ use std::{io, sync::mpsc, thread, time::Duration};
io::{self, stdin, stdout, Write},
sync::mpsc,
thread,
time::Duration,
};
use tui::{ use tui::{
backend::CrosstermBackend, backend::CrosstermBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Widget}, style::{Color, Style},
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Row, Table, Widget},
Terminal, Terminal,
}; };
@ -24,6 +21,9 @@ async fn main() -> Result<(), io::Error> {
let screen = AlternateScreen::to_alternate(true)?; let screen = AlternateScreen::to_alternate(true)?;
let backend = CrosstermBackend::with_alternate_screen(screen)?; let backend = CrosstermBackend::with_alternate_screen(screen)?;
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let update_rate_in_milliseconds : u64 = 500;
terminal.hide_cursor()?; terminal.hide_cursor()?;
// Setup input handling // Setup input handling
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@ -47,20 +47,78 @@ async fn main() -> Result<(), io::Error> {
let tx = tx.clone(); let tx = tx.clone();
loop { loop {
tx.send(Event::Tick).unwrap(); tx.send(Event::Tick).unwrap();
thread::sleep(Duration::from_millis(250)); thread::sleep(Duration::from_millis(update_rate_in_milliseconds));
} }
}); });
} }
let mut app : widgets::App = widgets::App::new("rustop"); let mut app : widgets::App = widgets::App::new("rustop");
terminal.clear()?; terminal.clear()?;
loop { loop {
if let Ok(recv) = rx.recv() {
match recv {
Event::Input(event) => match event {
KeyEvent::Char(c) => app.on_key(c),
KeyEvent::Left => {}
KeyEvent::Right => {}
KeyEvent::Up => {}
KeyEvent::Down => {}
KeyEvent::Ctrl('c') => break,
_ => {}
},
Event::Tick => {
app.update_data().await; // TODO: This await is causing slow responsiveness... perhaps make drawing another thread?
}
}
if app.should_quit {
break;
}
}
// Convert data into tui components
let temperature_rows = app.list_of_temperature.iter().map(|sensor| {
Row::StyledData(
vec![sensor.component_name.to_string(), sensor.temperature.to_string() + "C"].into_iter(), // TODO: Change this based on temperature type
Style::default().fg(Color::LightGreen),
)
});
let disk_rows = app.list_of_disks.iter().map(|disk| {
Row::StyledData(
vec![
disk.name.to_string(),
disk.mount_point.to_string(),
format!("{:.2}%", disk.used_space as f64 / disk.total_space as f64 * 100_f64),
(disk.free_space / 1024).to_string() + "GB",
(disk.total_space / 1024).to_string() + "GB",
]
.into_iter(), // TODO: Change this based on temperature type
Style::default().fg(Color::LightGreen),
)
});
let mem_total_mb = app.memory.mem_total_in_mb as f64;
let process_rows = app.list_of_processes.iter().map(|process| {
Row::StyledData(
vec![
process.pid.to_string(),
process.command.to_string(),
format!("{:.2}%", process.cpu_usage_percent),
format!("{:.2}%", process.mem_usage_in_mb as f64 / mem_total_mb * 100_f64),
]
.into_iter(),
Style::default().fg(Color::LightGreen),
)
});
// Draw!
terminal.draw(|mut f| { terminal.draw(|mut f| {
let vertical_chunks = Layout::default() let vertical_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(1) .margin(1)
.constraints([Constraint::Percentage(33), Constraint::Percentage(34), Constraint::Percentage(33)].as_ref()) .constraints([Constraint::Percentage(30), Constraint::Percentage(40), Constraint::Percentage(30)].as_ref())
.split(f.size()); .split(f.size());
let top_chunks = Layout::default() let top_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
@ -83,37 +141,57 @@ async fn main() -> Result<(), io::Error> {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(vertical_chunks[2]); .split(vertical_chunks[2]);
Block::default().title("CPU Usage").borders(Borders::ALL).render(&mut f, top_chunks[0]); // Set up blocks and their components
// CPU usage graph
Chart::default()
.block(Block::default().title("CPU Usage").borders(Borders::ALL))
.x_axis(Axis::default().style(Style::default().fg(Color::White)).bounds([0.0, 10.0]).labels(&["0.0", "10.0"]))
.y_axis(Axis::default().style(Style::default().fg(Color::White)).bounds([0.0, 10.0]).labels(&["0.0", "10.0"]))
.datasets(&[
Dataset::default()
.name("data1")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
Dataset::default()
.name("data2")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
])
.render(&mut f, top_chunks[0]);
//Memory usage graph
Block::default().title("Memory Usage").borders(Borders::ALL).render(&mut f, top_chunks[1]); Block::default().title("Memory Usage").borders(Borders::ALL).render(&mut f, top_chunks[1]);
Block::default().title("Temperatures").borders(Borders::ALL).render(&mut f, middle_divided_chunk[0]); // Temperature table
Block::default().title("Disk Usage").borders(Borders::ALL).render(&mut f, middle_divided_chunk[1]); Table::new(["Sensor", "Temperature"].iter(), temperature_rows)
.block(Block::default().title("Temperatures").borders(Borders::ALL))
.header_style(Style::default().fg(Color::LightBlue))
.widths(&[25, 25])
.render(&mut f, middle_divided_chunk[0]);
// Disk usage table
Table::new(["Disk", "Mount", "Used", "Total", "Free"].iter(), disk_rows)
.block(Block::default().title("Disk Usage").borders(Borders::ALL))
.header_style(Style::default().fg(Color::LightBlue))
.widths(&[25, 25, 10, 10, 10])
.render(&mut f, middle_divided_chunk[1]);
// IO graph
Block::default().title("IO Usage").borders(Borders::ALL).render(&mut f, middle_chunks[1]); Block::default().title("IO Usage").borders(Borders::ALL).render(&mut f, middle_chunks[1]);
// Network graph
Block::default().title("Network").borders(Borders::ALL).render(&mut f, bottom_chunks[0]); Block::default().title("Network").borders(Borders::ALL).render(&mut f, bottom_chunks[0]);
Block::default().title("Processes").borders(Borders::ALL).render(&mut f, bottom_chunks[1]);
})?;
// TODO: Ctrl-C? // Processes table
if let Ok(recv) = rx.recv() { Table::new(["PID", "Command", "CPU%", "Mem%"].iter(), process_rows)
match recv { .block(Block::default().title("Processes").borders(Borders::ALL))
Event::Input(event) => match event { .header_style(Style::default().fg(Color::LightBlue))
KeyEvent::Char(c) => app.on_key(c), .widths(&[5, 15, 10, 10])
KeyEvent::Left => {} .render(&mut f, bottom_chunks[1]);
KeyEvent::Right => {} })?;
KeyEvent::Up => {}
KeyEvent::Down => {}
KeyEvent::Ctrl('c') => break,
_ => {}
},
Event::Tick => {
app.update_data();
}
}
if app.should_quit {
break;
}
}
} }
Ok(()) Ok(())

View File

@ -2,16 +2,16 @@ use heim_common::units::information;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct MemData { pub struct MemData {
pub mem_total : u64, pub mem_total_in_mb : u64,
pub mem_used : u64, pub mem_used_in_mb : u64,
} }
pub async fn get_mem_data_list() -> Result<MemData, heim::Error> { pub async fn get_mem_data_list() -> Result<MemData, heim::Error> {
let memory = heim::memory::memory().await?; let memory = heim::memory::memory().await?;
Ok(MemData { Ok(MemData {
mem_total : memory.total().get::<information::megabyte>(), mem_total_in_mb : memory.total().get::<information::megabyte>(),
mem_used : memory.total().get::<information::megabyte>() - memory.available().get::<information::megabyte>(), mem_used_in_mb : memory.total().get::<information::megabyte>() - memory.available().get::<information::megabyte>(),
}) })
} }
@ -19,7 +19,7 @@ pub async fn get_swap_data_list() -> Result<MemData, heim::Error> {
let memory = heim::memory::swap().await?; let memory = heim::memory::swap().await?;
Ok(MemData { Ok(MemData {
mem_total : memory.total().get::<information::megabyte>(), mem_total_in_mb : memory.total().get::<information::megabyte>(),
mem_used : memory.used().get::<information::megabyte>(), mem_used_in_mb : memory.used().get::<information::megabyte>(),
}) })
} }

View File

@ -7,7 +7,6 @@ pub mod temperature;
use sysinfo::{System, SystemExt}; use sysinfo::{System, SystemExt};
#[derive(Default)]
pub struct App<'a> { pub struct App<'a> {
pub should_quit : bool, pub should_quit : bool,
pub list_of_cpu_packages : Vec<cpu::CPUData>, pub list_of_cpu_packages : Vec<cpu::CPUData>,
@ -20,6 +19,9 @@ pub struct App<'a> {
pub list_of_processes : Vec<processes::ProcessData>, pub list_of_processes : Vec<processes::ProcessData>,
pub list_of_disks : Vec<disks::DiskData>, pub list_of_disks : Vec<disks::DiskData>,
pub title : &'a str, pub title : &'a str,
process_sorting_type : processes::ProcessSorting,
process_sorting_reverse : bool,
sys : System,
} }
fn set_if_valid<T : std::clone::Clone>(result : &Result<T, heim::Error>, value_to_set : &mut T) { fn set_if_valid<T : std::clone::Clone>(result : &Result<T, heim::Error>, value_to_set : &mut T) {
@ -30,72 +32,70 @@ fn set_if_valid<T : std::clone::Clone>(result : &Result<T, heim::Error>, value_t
impl<'a> App<'a> { impl<'a> App<'a> {
pub fn new(title : &str) -> App { pub fn new(title : &str) -> App {
let mut app = App::default(); App {
app.title = title; title,
app process_sorting_type : processes::ProcessSorting::NAME, // TODO: Change this based on input args...
sys : System::new(), // TODO: Evaluate whether this will cause efficiency issues...
list_of_cpu_packages : Vec::new(),
list_of_disks : Vec::new(),
list_of_physical_io : Vec::new(),
list_of_io : Vec::new(),
list_of_processes : Vec::new(),
list_of_temperature : Vec::new(),
network : network::NetworkData::default(),
memory : mem::MemData::default(),
swap : mem::MemData::default(),
should_quit : false,
process_sorting_reverse : false,
}
} }
pub fn on_key(&mut self, c : char) { pub fn on_key(&mut self, c : char) {
match c { match c {
'q' => self.should_quit = true, 'q' => self.should_quit = true,
'c' => {
self.process_sorting_type = processes::ProcessSorting::CPU;
//processes::sort_processes(&self.process_sorting_type, &mut self.list_of_processes, self.process_sorting_reverse);
// TODO: This CANNOT run while it is updating...
}
'm' => {
self.process_sorting_type = processes::ProcessSorting::MEM;
//processes::sort_processes(&self.process_sorting_type, &mut self.list_of_processes, self.process_sorting_reverse);
}
'p' => {
self.process_sorting_type = processes::ProcessSorting::PID;
//processes::sort_processes(&self.process_sorting_type, &mut self.list_of_processes, self.process_sorting_reverse);
}
'n' => {
self.process_sorting_type = processes::ProcessSorting::NAME;
//processes::sort_processes(&self.process_sorting_type, &mut self.list_of_processes, self.process_sorting_reverse);
}
'r' => {
self.process_sorting_reverse = !self.process_sorting_reverse;
//processes::sort_processes(&self.process_sorting_type, &mut self.list_of_processes, self.process_sorting_reverse);
}
_ => {} _ => {}
} }
} }
pub async fn update_data(&mut self) { pub async fn update_data(&mut self) {
// Initialize self.sys.refresh_system();
let mut sys = System::new(); self.sys.refresh_network();
sys.refresh_system();
sys.refresh_network();
// What we want to do: For timed data, if there is an error, just do not add. For other data, just don't update! // What we want to do: For timed data, if there is an error, just do not add. For other data, just don't update!
set_if_valid(&network::get_network_data(&sys), &mut self.network); set_if_valid(&network::get_network_data(&self.sys), &mut self.network);
set_if_valid(&cpu::get_cpu_data_list(&sys), &mut self.list_of_cpu_packages); set_if_valid(&cpu::get_cpu_data_list(&self.sys), &mut self.list_of_cpu_packages);
// TODO: Joining all futures would be better... // TODO: Joining all futures would be better...
set_if_valid(&processes::get_sorted_processes_list(processes::ProcessSorting::NAME, false).await, &mut self.list_of_processes); set_if_valid(
&processes::get_sorted_processes_list(&self.process_sorting_type, self.process_sorting_reverse).await,
&mut self.list_of_processes,
);
set_if_valid(&disks::get_disk_usage_list().await, &mut self.list_of_disks); set_if_valid(&disks::get_disk_usage_list().await, &mut self.list_of_disks);
set_if_valid(&disks::get_io_usage_list(false).await, &mut self.list_of_io); set_if_valid(&disks::get_io_usage_list(false).await, &mut self.list_of_io);
set_if_valid(&disks::get_io_usage_list(true).await, &mut self.list_of_physical_io); set_if_valid(&disks::get_io_usage_list(true).await, &mut self.list_of_physical_io);
set_if_valid(&mem::get_mem_data_list().await, &mut self.memory); set_if_valid(&mem::get_mem_data_list().await, &mut self.memory);
set_if_valid(&mem::get_swap_data_list().await, &mut self.swap); set_if_valid(&mem::get_swap_data_list().await, &mut self.swap);
set_if_valid(&temperature::get_temperature_data().await, &mut self.list_of_temperature); set_if_valid(&temperature::get_temperature_data().await, &mut self.list_of_temperature);
/*
// DEBUG - output results
for process in &list_of_processes {
println!(
"Process: {} with PID {}, CPU: {}%, MEM: {} MB",
process.command, process.pid, process.cpu_usage_percent, process.mem_usage_in_mb,
);
}
for disk in &list_of_disks {
println!("{} is mounted on {}: {} used.", disk.name, disk.mount_point, disk.used_space as f64 / disk.total_space as f64);
// TODO: Check if this is valid
}
for io in &list_of_io {
println!("IO counter for {}: {} writes, {} reads.", &io.mount_point, io.write_bytes, io.read_bytes);
}
for io in &list_of_physical_io {
println!("Physical IO counter for {}: {} writes, {} reads.", &io.mount_point, io.write_bytes, io.read_bytes);
}
for cpu in &list_of_cpu_packages {
println!("CPU {} has {}% usage!", &cpu.cpu_name, cpu.cpu_usage);
}
println!("Memory usage: {} out of {} is used", memory.mem_used, memory.mem_total);
println!("Memory usage: {} out of {} is used", swap.mem_used, swap.mem_total);
for sensor in &list_of_temperature {
println!("Sensor for {} is at {} degrees Celsius", sensor.component_name, sensor.temperature);
}
println!("Network: {} rx, {} tx", network.rx, network.tx);
*/
} }
} }

View File

@ -15,7 +15,7 @@ pub enum ProcessSorting {
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct ProcessData { pub struct ProcessData {
pub pid : u32, pub pid : u32,
pub cpu_usage_percent : f32, pub cpu_usage_percent : f64,
pub mem_usage_in_mb : u64, pub mem_usage_in_mb : u64,
pub command : String, pub command : String,
} }
@ -44,13 +44,13 @@ fn get_ordering<T : std::cmp::PartialOrd>(a_val : T, b_val : T, reverse_order :
async fn cpu_usage(process : heim::process::Process) -> heim::process::ProcessResult<(heim::process::Process, heim_common::units::Ratio)> { async fn cpu_usage(process : heim::process::Process) -> heim::process::ProcessResult<(heim::process::Process, heim_common::units::Ratio)> {
let usage_1 = process.cpu_usage().await?; let usage_1 = process.cpu_usage().await?;
futures_timer::Delay::new(std::time::Duration::from_millis(100)).await?; futures_timer::Delay::new(std::time::Duration::from_millis(100)).await?; // TODO: This is causing MASSIVE PROBLEMS WITH THE EVENT LOOP!
let usage_2 = process.cpu_usage().await?; let usage_2 = process.cpu_usage().await?;
Ok((process, usage_2 - usage_1)) Ok((process, usage_2 - usage_1))
} }
pub async fn get_sorted_processes_list(sorting_method : ProcessSorting, reverse_order : bool) -> Result<Vec<ProcessData>, heim::Error> { pub async fn get_sorted_processes_list(sorting_method : &ProcessSorting, reverse_order : bool) -> Result<Vec<ProcessData>, heim::Error> {
let mut process_stream = heim::process::processes().map_ok(cpu_usage).try_buffer_unordered(std::usize::MAX); let mut process_stream = heim::process::processes().map_ok(cpu_usage).try_buffer_unordered(std::usize::MAX);
let mut process_vector : Vec<ProcessData> = Vec::new(); let mut process_vector : Vec<ProcessData> = Vec::new();
@ -72,18 +72,18 @@ pub async fn get_sorted_processes_list(sorting_method : ProcessSorting, reverse_
process_vector.push(ProcessData { process_vector.push(ProcessData {
command : process.name().await.unwrap_or_else(|_| "".to_string()), command : process.name().await.unwrap_or_else(|_| "".to_string()),
pid : process.pid() as u32, pid : process.pid() as u32,
cpu_usage_percent : cpu_usage.get::<units::ratio::percent>(), cpu_usage_percent : f64::from(cpu_usage.get::<units::ratio::percent>()),
mem_usage_in_mb : mem_measurement.rss().get::<units::information::megabyte>(), mem_usage_in_mb : mem_measurement.rss().get::<units::information::megabyte>(),
}); });
} }
} }
} }
sort_processes(sorting_method, &mut process_vector, reverse_order); sort_processes(&sorting_method, &mut process_vector, reverse_order);
Ok(process_vector) Ok(process_vector)
} }
pub fn sort_processes(sorting_method : ProcessSorting, process_vector : &mut Vec<ProcessData>, reverse_order : bool) { pub fn sort_processes(sorting_method : &ProcessSorting, process_vector : &mut Vec<ProcessData>, reverse_order : bool) {
match sorting_method { match sorting_method {
ProcessSorting::CPU => process_vector.sort_by(|a, b| get_ordering(a.cpu_usage_percent, b.cpu_usage_percent, reverse_order)), ProcessSorting::CPU => process_vector.sort_by(|a, b| get_ordering(a.cpu_usage_percent, b.cpu_usage_percent, reverse_order)),
ProcessSorting::MEM => process_vector.sort_by(|a, b| get_ordering(a.mem_usage_in_mb, b.mem_usage_in_mb, reverse_order)), ProcessSorting::MEM => process_vector.sort_by(|a, b| get_ordering(a.mem_usage_in_mb, b.mem_usage_in_mb, reverse_order)),

View File

@ -14,7 +14,7 @@ pub async fn get_temperature_data() -> Result<Vec<TempData>, heim::Error> {
if let Ok(sensor) = sensor { if let Ok(sensor) = sensor {
temperature_vec.push(TempData { temperature_vec.push(TempData {
component_name : Box::from(sensor.unit()), component_name : Box::from(sensor.unit()),
temperature : sensor.current().get::<thermodynamic_temperature::degree_celsius>(), temperature : sensor.current().get::<thermodynamic_temperature::degree_celsius>(), // TODO: Allow for toggling this!
}); });
} }
} }