Files
bottom/src/bin/main.rs
T
Clement Tsang b60a62d5f0 other: keep running even if logger fails to initialize (#1132)
This just makes it so that even if the logger is enabled, if it fails to
initialize (e.g. read-only directory), bottom will still continue to
start up, and just print a warning that the logger encountered an issue.
2023-05-06 12:56:01 -04:00

337 lines
13 KiB
Rust

#![warn(rust_2018_idioms)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::missing_safety_doc)]
#[allow(unused_imports)] // TODO: Deny unused imports.
// Primarily used for debug purposes.
#[cfg(feature = "log")]
#[macro_use]
extern crate log;
use std::{
boxed::Box,
io::stdout,
panic,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, Condvar, Mutex,
},
thread,
time::Duration,
};
use anyhow::{Context, Result};
use crossterm::{
event::{EnableBracketedPaste, EnableMouseCapture},
execute,
terminal::{enable_raw_mode, EnterAlternateScreen},
};
use tui::{backend::CrosstermBackend, Terminal};
use bottom::{
canvas::{self, canvas_styling::CanvasColours},
constants::*,
data_conversion::*,
options::*,
*,
};
// Used for heap allocation debugging purposes.
// #[global_allocator]
// static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() -> Result<()> {
// let _profiler = dhat::Profiler::new_heap();
let matches = clap::get_matches();
#[cfg(all(feature = "fern"))]
{
if let Err(err) =
utils::logging::init_logger(log::LevelFilter::Debug, std::ffi::OsStr::new("debug.log"))
{
println!("Issue initializing logger: {err}");
}
}
// Read from config file.
let config_path = read_config(matches.get_one::<String>("config_location"))
.context("Unable to access the given config file location.")?;
let mut config: Config = create_or_get_config(&config_path)
.context("Unable to properly parse or create the config file.")?;
// Get widget layout separately
let (widget_layout, default_widget_id, default_widget_type_option) =
get_widget_layout(&matches, &config)
.context("Found an issue while trying to build the widget layout.")?;
// FIXME: Should move this into build app or config
let colours = {
let colour_scheme = get_color_scheme(&matches, &config)?;
CanvasColours::new(colour_scheme, &config)?
};
// Create an "app" struct, which will control most of the program and store settings/state
let mut app = build_app(
&matches,
&mut config,
&widget_layout,
default_widget_id,
&default_widget_type_option,
&colours,
)?;
// Create painter and set colours.
let mut painter = canvas::Painter::init(widget_layout, colours)?;
// Check if the current environment is in a terminal.
check_if_terminal();
// Create termination mutex and cvar
#[allow(clippy::mutex_atomic)]
let thread_termination_lock = Arc::new(Mutex::new(false));
let thread_termination_cvar = Arc::new(Condvar::new());
let (sender, receiver) = mpsc::channel();
// Set up event loop thread; we set this up early to speed up first-time-to-data.
let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();
let _collection_thread = create_collection_thread(
sender.clone(),
collection_thread_ctrl_receiver,
thread_termination_lock.clone(),
thread_termination_cvar.clone(),
&app.app_config_fields,
app.filters.clone(),
app.used_widgets,
);
// Set up input handling loop thread.
let _input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone());
// Set up cleaning loop thread.
let _cleaning_thread = {
let lock = thread_termination_lock.clone();
let cvar = thread_termination_cvar.clone();
let cleaning_sender = sender;
let offset_wait_time = app.app_config_fields.retention_ms + 60000;
thread::spawn(move || {
loop {
let result = cvar.wait_timeout(
lock.lock().unwrap(),
Duration::from_millis(offset_wait_time),
);
if let Ok(result) = result {
if *(result.0) {
break;
}
}
if cleaning_sender.send(BottomEvent::Clean).is_err() {
// debug!("Failed to send cleaning sender...");
break;
}
}
})
};
// Set up tui and crossterm
let mut stdout_val = stdout();
execute!(
stdout_val,
EnterAlternateScreen,
EnableMouseCapture,
EnableBracketedPaste
)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
terminal.clear()?;
terminal.hide_cursor()?;
#[cfg(target_os = "freebsd")]
let _stderr_fd = {
// A really ugly band-aid to suppress stderr warnings on FreeBSD due to sysinfo.
// For more information, see https://github.com/ClementTsang/bottom/issues/798.
use filedescriptor::{FileDescriptor, StdioDescriptor};
use std::fs::OpenOptions;
let path = OpenOptions::new().write(true).open("/dev/null")?;
FileDescriptor::redirect_stdio(&path, StdioDescriptor::Stderr)?
};
// Set panic hook
panic::set_hook(Box::new(panic_hook));
// Set termination hook
let is_terminated = Arc::new(AtomicBool::new(false));
let ist_clone = is_terminated.clone();
ctrlc::set_handler(move || {
ist_clone.store(true, Ordering::SeqCst);
})?;
let mut first_run = true;
// Draw once first to initialize the canvas, so it doesn't feel like it's frozen.
try_drawing(&mut terminal, &mut app, &mut painter)?;
while !is_terminated.load(Ordering::SeqCst) {
// TODO: Would be good to instead use a mix of is_terminated check + recv. Probably use a termination event instead.
if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) {
match recv {
BottomEvent::Resize => {
// FIXME: This is bugged with frozen?
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::KeyInput(event) => {
if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {
break;
}
update_data(&mut app);
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::MouseInput(event) => {
handle_mouse_event(event, &mut app);
update_data(&mut app);
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::PasteEvent(paste) => {
app.handle_paste(paste);
update_data(&mut app);
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
BottomEvent::Update(data) => {
app.data_collection.eat_data(data);
// This thing is required as otherwise, some widgets can't draw correctly w/o
// some data (or they need to be re-drawn).
if first_run {
first_run = false;
app.is_force_redraw = true;
}
if !app.frozen_state.is_frozen() {
// Convert all data into tui-compliant components
// Network
if app.used_widgets.use_net {
let network_data = convert_network_data_points(
&app.data_collection,
app.app_config_fields.use_basic_mode
|| app.app_config_fields.use_old_network_legend,
&app.app_config_fields.network_scale_type,
&app.app_config_fields.network_unit_type,
app.app_config_fields.network_use_binary_prefix,
);
app.converted_data.network_data_rx = network_data.rx;
app.converted_data.network_data_tx = network_data.tx;
app.converted_data.rx_display = network_data.rx_display;
app.converted_data.tx_display = network_data.tx_display;
if let Some(total_rx_display) = network_data.total_rx_display {
app.converted_data.total_rx_display = total_rx_display;
}
if let Some(total_tx_display) = network_data.total_tx_display {
app.converted_data.total_tx_display = total_tx_display;
}
}
// Disk
if app.used_widgets.use_disk {
app.converted_data.ingest_disk_data(&app.data_collection);
for disk in app.disk_state.widget_states.values_mut() {
disk.force_data_update();
}
}
// Temperatures
if app.used_widgets.use_temp {
app.converted_data.ingest_temp_data(
&app.data_collection,
app.app_config_fields.temperature_type,
);
for temp in app.temp_state.widget_states.values_mut() {
temp.force_data_update();
}
}
// Memory
if app.used_widgets.use_mem {
app.converted_data.mem_data =
convert_mem_data_points(&app.data_collection);
#[cfg(not(target_os = "windows"))]
{
app.converted_data.cache_data =
convert_cache_data_points(&app.data_collection);
}
app.converted_data.swap_data =
convert_swap_data_points(&app.data_collection);
#[cfg(feature = "zfs")]
{
app.converted_data.arc_data =
convert_arc_data_points(&app.data_collection);
}
#[cfg(feature = "gpu")]
{
app.converted_data.gpu_data =
convert_gpu_data(&app.data_collection);
}
app.converted_data.mem_labels =
convert_mem_label(&app.data_collection.memory_harvest);
app.converted_data.swap_labels =
convert_mem_label(&app.data_collection.swap_harvest);
#[cfg(not(target_os = "windows"))]
{
app.converted_data.cache_labels =
convert_mem_label(&app.data_collection.cache_harvest);
}
#[cfg(feature = "zfs")]
{
let arc_labels = convert_arc_labels(&app.data_collection);
app.converted_data.arc_labels = arc_labels;
}
}
// CPU
if app.used_widgets.use_cpu {
app.converted_data.ingest_cpu_data(&app.data_collection);
app.converted_data.load_avg_data = app.data_collection.load_avg_harvest;
}
// Processes
if app.used_widgets.use_proc {
for proc in app.proc_state.widget_states.values_mut() {
proc.force_data_update();
}
}
// Battery
#[cfg(feature = "battery")]
{
if app.used_widgets.use_battery {
app.converted_data.battery_data =
convert_battery_harvest(&app.data_collection);
}
}
update_data(&mut app);
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
}
BottomEvent::Clean => {
app.data_collection
.clean_data(app.app_config_fields.retention_ms);
}
}
}
}
// I think doing it in this order is safe...
*thread_termination_lock.lock().unwrap() = true;
thread_termination_cvar.notify_all();
cleanup_terminal(&mut terminal)?;
Ok(())
}