Monitor with UI based on tui-rs (#480)
* first working version * full gui * remove warnings * remove errors in release * allow missing_docs in tui * tui_monitor flag * working graphs * disable tui on windows * clippy * clippy * tui module only under std * use tui from git * fmt * tui from crates
This commit is contained in:
parent
ab7d16347f
commit
cc0880e784
@ -30,6 +30,7 @@ use libafl::{
|
|||||||
feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
|
feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
|
||||||
fuzzer::{Fuzzer, StdFuzzer},
|
fuzzer::{Fuzzer, StdFuzzer},
|
||||||
inputs::{BytesInput, HasTargetBytes},
|
inputs::{BytesInput, HasTargetBytes},
|
||||||
|
monitors::tui::TuiMonitor,
|
||||||
monitors::MultiMonitor,
|
monitors::MultiMonitor,
|
||||||
mutators::scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator},
|
mutators::scheduled::{havoc_mutations, tokens_mutations, StdScheduledMutator},
|
||||||
mutators::token_mutations::Tokens,
|
mutators::token_mutations::Tokens,
|
||||||
@ -141,7 +142,8 @@ pub fn libafl_main() {
|
|||||||
|
|
||||||
let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory");
|
let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory");
|
||||||
|
|
||||||
let monitor = MultiMonitor::new(|s| println!("{}", s));
|
//let monitor = MultiMonitor::new(|s| println!("{}", s));
|
||||||
|
let monitor = TuiMonitor::new("Test fuzzer on libpng".into(), true);
|
||||||
|
|
||||||
let mut run_client = |state: Option<StdState<_, _, _, _, _>>, mut restarting_mgr, _core_id| {
|
let mut run_client = |state: Option<StdState<_, _, _, _, _>>, mut restarting_mgr, _core_id| {
|
||||||
// Create an observation channel using the coverage map
|
// Create an observation channel using the coverage map
|
||||||
|
@ -12,12 +12,13 @@ edition = "2021"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["std", "derive", "llmp_compression", "rand_trait", "fork"]
|
default = ["std", "derive", "llmp_compression", "rand_trait", "fork"]
|
||||||
std = ["serde_json", "serde_json/std", "hostname", "core_affinity", "nix", "serde/std", "bincode", "wait-timeout", "regex", "build_id", "uuid"] # print, env, launcher ... support
|
std = ["serde_json", "serde_json/std", "hostname", "core_affinity", "nix", "serde/std", "bincode", "wait-timeout", "regex", "build_id", "uuid", "tui_monitor"] # print, env, launcher ... support
|
||||||
derive = ["libafl_derive"] # provide derive(SerdeAny) macro.
|
derive = ["libafl_derive"] # provide derive(SerdeAny) macro.
|
||||||
fork = [] # uses the fork() syscall to spawn children, instead of launching a new command, if supported by the OS (has no effect on Windows, no_std).
|
fork = [] # uses the fork() syscall to spawn children, instead of launching a new command, if supported by the OS (has no effect on Windows, no_std).
|
||||||
rand_trait = ["rand_core"] # If set, libafl's rand implementations will implement `rand::Rng`
|
rand_trait = ["rand_core"] # If set, libafl's rand implementations will implement `rand::Rng`
|
||||||
introspection = [] # Include performance statistics of the fuzzing pipeline
|
introspection = [] # Include performance statistics of the fuzzing pipeline
|
||||||
concolic_mutation = ["z3"] # include a simple concolic mutator based on z3
|
concolic_mutation = ["z3"] # include a simple concolic mutator based on z3
|
||||||
|
tui_monitor = ["tui", "crossterm"] # enable TuiMonitor with crossterm
|
||||||
# features hiding dependencies licensed under GPL
|
# features hiding dependencies licensed under GPL
|
||||||
gpl = []
|
gpl = []
|
||||||
# features hiding dependencies licensed under AGPL
|
# features hiding dependencies licensed under AGPL
|
||||||
@ -70,6 +71,8 @@ regex = { version = "1", optional = true }
|
|||||||
build_id = { version = "0.2.1", git = "https://github.com/domenukk/build_id", rev = "6a61943", optional = true }
|
build_id = { version = "0.2.1", git = "https://github.com/domenukk/build_id", rev = "6a61943", optional = true }
|
||||||
uuid = { version = "0.8.2", optional = true, features = ["serde", "v4"] }
|
uuid = { version = "0.8.2", optional = true, features = ["serde", "v4"] }
|
||||||
libm = "0.2.1"
|
libm = "0.2.1"
|
||||||
|
tui = { version = "0.16", default-features = false, features = ['crossterm'], optional = true }
|
||||||
|
crossterm = { version = "0.20", optional = true }
|
||||||
|
|
||||||
wait-timeout = { version = "0.2", optional = true } # used by CommandExecutor to wait for child process
|
wait-timeout = { version = "0.2", optional = true } # used by CommandExecutor to wait for child process
|
||||||
|
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
pub mod multi;
|
pub mod multi;
|
||||||
pub use multi::MultiMonitor;
|
pub use multi::MultiMonitor;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "tui_monitor", feature = "std"))]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub mod tui;
|
||||||
|
|
||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
#[cfg(feature = "introspection")]
|
#[cfg(feature = "introspection")]
|
||||||
@ -17,6 +21,7 @@ use crate::bolts::{current_time, format_duration_hms};
|
|||||||
const CLIENT_STATS_TIME_WINDOW_SECS: u64 = 5; // 5 seconds
|
const CLIENT_STATS_TIME_WINDOW_SECS: u64 = 5; // 5 seconds
|
||||||
|
|
||||||
/// User-defined stat types
|
/// User-defined stat types
|
||||||
|
/// TODO define aggregation function (avg, median, max, ...)
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum UserStats {
|
pub enum UserStats {
|
||||||
/// A numerical value
|
/// A numerical value
|
||||||
|
415
libafl/src/monitors/tui/mod.rs
Normal file
415
libafl/src/monitors/tui/mod.rs
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
//! Monitor based on tui-rs
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use tui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{self, BufRead},
|
||||||
|
string::String,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
use super::{ClientPerfMonitor, PerfFeature};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bolts::{current_time, format_duration_hms},
|
||||||
|
monitors::{ClientStats, Monitor, UserStats},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod ui;
|
||||||
|
use ui::TuiUI;
|
||||||
|
|
||||||
|
const DEFAULT_TIME_WINDOW: u64 = 60 * 10; // 10 min
|
||||||
|
const DEFAULT_LOGS_NUMBER: usize = 128;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TimedStat {
|
||||||
|
pub time: Duration,
|
||||||
|
pub item: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimedStats {
|
||||||
|
pub series: VecDeque<TimedStat>,
|
||||||
|
pub window: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimedStats {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(window: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
series: VecDeque::new(),
|
||||||
|
window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, time: Duration, item: u64) {
|
||||||
|
if self.series.is_empty() || self.series.back().unwrap().item != item {
|
||||||
|
if self.series.front().is_some()
|
||||||
|
&& time - self.series.front().unwrap().time > self.window
|
||||||
|
{
|
||||||
|
self.series.pop_front();
|
||||||
|
}
|
||||||
|
self.series.push_back(TimedStat { time, item });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_now(&mut self, item: u64) {
|
||||||
|
if self.series.is_empty() || self.series[self.series.len() - 1].item != item {
|
||||||
|
let time = current_time();
|
||||||
|
if self.series.front().is_some()
|
||||||
|
&& time - self.series.front().unwrap().time > self.window
|
||||||
|
{
|
||||||
|
self.series.pop_front();
|
||||||
|
}
|
||||||
|
self.series.push_back(TimedStat { time, item });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_window(&mut self, window: Duration) {
|
||||||
|
self.window = window;
|
||||||
|
while !self.series.is_empty()
|
||||||
|
&& self.series.back().unwrap().time - self.series.front().unwrap().time > window
|
||||||
|
{
|
||||||
|
self.series.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct PerfTuiContext {
|
||||||
|
pub scheduler: f64,
|
||||||
|
pub manager: f64,
|
||||||
|
pub unmeasured: f64,
|
||||||
|
pub stages: Vec<Vec<(String, f64)>>,
|
||||||
|
pub feedbacks: Vec<(String, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
impl PerfTuiContext {
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
pub fn grab_data(&mut self, m: &ClientPerfMonitor) {
|
||||||
|
// Calculate the elapsed time from the monitor
|
||||||
|
let elapsed: f64 = m.elapsed_cycles() as f64;
|
||||||
|
|
||||||
|
// Calculate the percentages for each benchmark
|
||||||
|
self.scheduler = m.scheduler_cycles() as f64 / elapsed;
|
||||||
|
self.manager = m.manager_cycles() as f64 / elapsed;
|
||||||
|
|
||||||
|
// Calculate the remaining percentage that has not been benchmarked
|
||||||
|
let mut other_percent = 1.0;
|
||||||
|
other_percent -= self.scheduler;
|
||||||
|
other_percent -= self.manager;
|
||||||
|
|
||||||
|
self.stages.clear();
|
||||||
|
|
||||||
|
// Calculate each stage
|
||||||
|
// Make sure we only iterate over used stages
|
||||||
|
for (_stage_index, features) in m.used_stages() {
|
||||||
|
let mut features_percentages = vec![];
|
||||||
|
|
||||||
|
for (feature_index, feature) in features.iter().enumerate() {
|
||||||
|
// Calculate this current stage's percentage
|
||||||
|
let feature_percent = *feature as f64 / elapsed;
|
||||||
|
|
||||||
|
// Ignore this feature if it isn't used
|
||||||
|
if feature_percent == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the other percent by removing this current percent
|
||||||
|
other_percent -= feature_percent;
|
||||||
|
|
||||||
|
// Get the actual feature from the feature index for printing its name
|
||||||
|
let feature: PerfFeature = feature_index.into();
|
||||||
|
features_percentages.push((format!("{:?}", feature), feature_percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stages.push(features_percentages);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.feedbacks.clear();
|
||||||
|
|
||||||
|
for (feedback_name, feedback_time) in m.feedbacks() {
|
||||||
|
// Calculate this current stage's percentage
|
||||||
|
let feedback_percent = *feedback_time as f64 / elapsed;
|
||||||
|
|
||||||
|
// Ignore this feedback if it isn't used
|
||||||
|
if feedback_percent == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the other percent by removing this current percent
|
||||||
|
other_percent -= feedback_percent;
|
||||||
|
|
||||||
|
self.feedbacks
|
||||||
|
.push((feedback_name.clone(), feedback_percent));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.unmeasured = other_percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct ClientTuiContext {
|
||||||
|
pub corpus: u64,
|
||||||
|
pub objectives: u64,
|
||||||
|
pub executions: u64,
|
||||||
|
pub exec_sec: u64,
|
||||||
|
|
||||||
|
pub user_stats: HashMap<String, UserStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientTuiContext {
|
||||||
|
pub fn grab_data(&mut self, client: &ClientStats, exec_sec: u64) {
|
||||||
|
self.corpus = client.corpus_size;
|
||||||
|
self.objectives = client.objective_size;
|
||||||
|
self.executions = client.executions;
|
||||||
|
self.exec_sec = exec_sec;
|
||||||
|
|
||||||
|
for (key, val) in &client.user_monitor {
|
||||||
|
self.user_stats.insert(key.clone(), val.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TuiContext {
|
||||||
|
pub graphs: Vec<String>,
|
||||||
|
|
||||||
|
// TODO update the window using the UI key press events (+/-)
|
||||||
|
pub corpus_size_timed: TimedStats,
|
||||||
|
pub objective_size_timed: TimedStats,
|
||||||
|
pub execs_per_sec_timed: TimedStats,
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
pub introspection: HashMap<usize, PerfTuiContext>,
|
||||||
|
|
||||||
|
pub clients: HashMap<usize, ClientTuiContext>,
|
||||||
|
|
||||||
|
pub client_logs: VecDeque<String>,
|
||||||
|
|
||||||
|
pub clients_num: usize,
|
||||||
|
pub total_execs: u64,
|
||||||
|
pub start_time: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiContext {
|
||||||
|
/// Create a new TUI context
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(start_time: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
graphs: vec!["corpus".into(), "objectives".into(), "exec/sec".into()],
|
||||||
|
corpus_size_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)),
|
||||||
|
objective_size_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)),
|
||||||
|
execs_per_sec_timed: TimedStats::new(Duration::from_secs(DEFAULT_TIME_WINDOW)),
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
introspection: HashMap::default(),
|
||||||
|
clients: HashMap::default(),
|
||||||
|
|
||||||
|
client_logs: VecDeque::with_capacity(DEFAULT_LOGS_NUMBER),
|
||||||
|
|
||||||
|
clients_num: 0,
|
||||||
|
total_execs: 0,
|
||||||
|
start_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracking monitor during fuzzing and display with tui-rs.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TuiMonitor {
|
||||||
|
pub(crate) context: Arc<RwLock<TuiContext>>,
|
||||||
|
|
||||||
|
start_time: Duration,
|
||||||
|
client_stats: Vec<ClientStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Monitor for TuiMonitor {
|
||||||
|
/// the client monitor, mutable
|
||||||
|
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
|
||||||
|
&mut self.client_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the client monitor
|
||||||
|
fn client_stats(&self) -> &[ClientStats] {
|
||||||
|
&self.client_stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time this fuzzing run stated
|
||||||
|
fn start_time(&mut self) -> Duration {
|
||||||
|
self.start_time
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&mut self, event_msg: String, sender_id: u32) {
|
||||||
|
let cur_time = current_time();
|
||||||
|
|
||||||
|
{
|
||||||
|
let execsec = self.execs_per_sec();
|
||||||
|
let totalexec = self.total_execs();
|
||||||
|
let run_time = cur_time - self.start_time;
|
||||||
|
|
||||||
|
let mut ctx = self.context.write().unwrap();
|
||||||
|
ctx.corpus_size_timed.add(run_time, self.corpus_size());
|
||||||
|
ctx.objective_size_timed
|
||||||
|
.add(run_time, self.objective_size());
|
||||||
|
ctx.execs_per_sec_timed.add(run_time, execsec);
|
||||||
|
ctx.total_execs = totalexec;
|
||||||
|
ctx.clients_num = self.client_stats.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = self.client_stats_mut_for(sender_id);
|
||||||
|
let exec_sec = client.execs_per_sec(cur_time);
|
||||||
|
|
||||||
|
let sender = format!("#{}", sender_id);
|
||||||
|
let pad = if event_msg.len() + sender.len() < 13 {
|
||||||
|
" ".repeat(13 - event_msg.len() - sender.len())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let head = format!("{}{} {}", event_msg, pad, sender);
|
||||||
|
let mut fmt = format!(
|
||||||
|
"[{}] corpus: {}, objectives: {}, executions: {}, exec/sec: {}",
|
||||||
|
head, client.corpus_size, client.objective_size, client.executions, exec_sec
|
||||||
|
);
|
||||||
|
for (key, val) in &client.user_monitor {
|
||||||
|
fmt += &format!(", {}: {}", key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = &self.client_stats()[sender_id as usize];
|
||||||
|
let mut ctx = self.context.write().unwrap();
|
||||||
|
ctx.clients
|
||||||
|
.entry(sender_id as usize)
|
||||||
|
.or_default()
|
||||||
|
.grab_data(client, exec_sec);
|
||||||
|
while ctx.client_logs.len() >= DEFAULT_LOGS_NUMBER {
|
||||||
|
ctx.client_logs.pop_front();
|
||||||
|
}
|
||||||
|
ctx.client_logs.push_back(fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
{
|
||||||
|
// Print the client performance monitor. Skip the Client 0 which is the broker
|
||||||
|
for (i, client) in self.client_stats.iter().skip(1).enumerate() {
|
||||||
|
self.context
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.introspection
|
||||||
|
.entry(i + 1)
|
||||||
|
.or_default()
|
||||||
|
.grab_data(&client.introspection_monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiMonitor {
|
||||||
|
/// Creates the monitor
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(title: String, enhanced_graphics: bool) -> Self {
|
||||||
|
Self::with_time(title, enhanced_graphics, current_time())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the monitor with a given `start_time`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_time(title: String, enhanced_graphics: bool, start_time: Duration) -> Self {
|
||||||
|
let context = Arc::new(RwLock::new(TuiContext::new(start_time)));
|
||||||
|
run_tui_thread(
|
||||||
|
context.clone(),
|
||||||
|
Duration::from_millis(250),
|
||||||
|
title,
|
||||||
|
enhanced_graphics,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
context,
|
||||||
|
start_time,
|
||||||
|
client_stats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_tui_thread(
|
||||||
|
context: Arc<RwLock<TuiContext>>,
|
||||||
|
tick_rate: Duration,
|
||||||
|
title: String,
|
||||||
|
enhanced_graphics: bool,
|
||||||
|
) {
|
||||||
|
thread::spawn(move || -> io::Result<()> {
|
||||||
|
// setup terminal
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
let mut ui = TuiUI::new(title, enhanced_graphics);
|
||||||
|
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
let mut cnt = 0;
|
||||||
|
loop {
|
||||||
|
// to avoid initial ui glitches
|
||||||
|
if cnt < 8 {
|
||||||
|
drop(terminal.clear());
|
||||||
|
cnt += 1;
|
||||||
|
}
|
||||||
|
terminal.draw(|f| ui.draw(f, &context))?;
|
||||||
|
|
||||||
|
let timeout = tick_rate
|
||||||
|
.checked_sub(last_tick.elapsed())
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(0));
|
||||||
|
if crossterm::event::poll(timeout)? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char(c) => ui.on_key(c),
|
||||||
|
KeyCode::Left => ui.on_left(),
|
||||||
|
//KeyCode::Up => ui.on_up(),
|
||||||
|
KeyCode::Right => ui.on_right(),
|
||||||
|
//KeyCode::Down => ui.on_down(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
//context.on_tick();
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
if ui.should_quit {
|
||||||
|
// restore terminal
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
println!("\nPress Control-C to stop the fuzzers, otherwise press Enter to resume the visualization\n");
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
io::stdin().lock().read_line(&mut line)?;
|
||||||
|
|
||||||
|
// setup terminal
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
cnt = 0;
|
||||||
|
ui.should_quit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
507
libafl/src/monitors/tui/ui.rs
Normal file
507
libafl/src/monitors/tui/ui.rs
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
use super::{current_time, format_duration_hms, Duration, String, TimedStats, TuiContext};
|
||||||
|
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
symbols,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{
|
||||||
|
Axis, Block, Borders, Cell, Chart, Dataset, List, ListItem, Paragraph, Row, Table, Tabs,
|
||||||
|
},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cmp::{max, min},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TuiUI {
|
||||||
|
title: String,
|
||||||
|
enhanced_graphics: bool,
|
||||||
|
show_logs: bool,
|
||||||
|
clients_idx: usize,
|
||||||
|
clients: usize,
|
||||||
|
charts_tab_idx: usize,
|
||||||
|
graph_data: Vec<(f64, f64)>,
|
||||||
|
|
||||||
|
pub should_quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiUI {
|
||||||
|
pub fn new(title: String, enhanced_graphics: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
enhanced_graphics,
|
||||||
|
show_logs: true,
|
||||||
|
clients_idx: 1,
|
||||||
|
..TuiUI::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_key(&mut self, c: char) {
|
||||||
|
match c {
|
||||||
|
'q' => {
|
||||||
|
self.should_quit = true;
|
||||||
|
}
|
||||||
|
'g' => {
|
||||||
|
self.charts_tab_idx = (self.charts_tab_idx + 1) % 3;
|
||||||
|
}
|
||||||
|
't' => {
|
||||||
|
self.show_logs = !self.show_logs;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn on_up(&mut self) {}
|
||||||
|
|
||||||
|
//pub fn on_down(&mut self) {}
|
||||||
|
|
||||||
|
pub fn on_right(&mut self) {
|
||||||
|
// never 0
|
||||||
|
self.clients_idx = 1 + self.clients_idx % (self.clients - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_left(&mut self) {
|
||||||
|
// never 0
|
||||||
|
if self.clients_idx == 1 {
|
||||||
|
self.clients_idx = self.clients - 1;
|
||||||
|
} else {
|
||||||
|
self.clients_idx = 1 + (self.clients_idx - 2) % (self.clients - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
self.clients = app.read().unwrap().clients_num;
|
||||||
|
|
||||||
|
let body = Layout::default()
|
||||||
|
.constraints(if self.show_logs {
|
||||||
|
[Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()
|
||||||
|
} else {
|
||||||
|
[Constraint::Percentage(100)].as_ref()
|
||||||
|
})
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
let top_layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(body[0]);
|
||||||
|
|
||||||
|
let left_layout = Layout::default()
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||||
|
.split(top_layout[0]);
|
||||||
|
|
||||||
|
let text = vec![Spans::from(Span::styled(
|
||||||
|
&self.title,
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightMagenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))];
|
||||||
|
let block = Block::default().borders(Borders::ALL);
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center); //.wrap(Wrap { trim: true });
|
||||||
|
f.render_widget(paragraph, left_layout[0]);
|
||||||
|
|
||||||
|
self.draw_text(f, app, left_layout[1]);
|
||||||
|
|
||||||
|
let right_layout = Layout::default()
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||||
|
.split(top_layout[1]);
|
||||||
|
let titles = vec![
|
||||||
|
Spans::from(Span::styled(
|
||||||
|
"speed",
|
||||||
|
Style::default().fg(Color::LightGreen),
|
||||||
|
)),
|
||||||
|
Spans::from(Span::styled(
|
||||||
|
"corpus",
|
||||||
|
Style::default().fg(Color::LightGreen),
|
||||||
|
)),
|
||||||
|
Spans::from(Span::styled(
|
||||||
|
"objectives",
|
||||||
|
Style::default().fg(Color::LightGreen),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
let tabs = Tabs::new(titles)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
"charts (`g` switch)",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.highlight_style(Style::default().fg(Color::LightYellow))
|
||||||
|
.select(self.charts_tab_idx);
|
||||||
|
f.render_widget(tabs, right_layout[0]);
|
||||||
|
|
||||||
|
match self.charts_tab_idx {
|
||||||
|
0 => {
|
||||||
|
let ctx = app.read().unwrap();
|
||||||
|
self.draw_time_chart(
|
||||||
|
"speed chart",
|
||||||
|
"exec/sec",
|
||||||
|
f,
|
||||||
|
right_layout[1],
|
||||||
|
&ctx.execs_per_sec_timed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let ctx = app.read().unwrap();
|
||||||
|
self.draw_time_chart(
|
||||||
|
"corpus chart",
|
||||||
|
"corpus size",
|
||||||
|
f,
|
||||||
|
right_layout[1],
|
||||||
|
&ctx.corpus_size_timed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let ctx = app.read().unwrap();
|
||||||
|
self.draw_time_chart(
|
||||||
|
"corpus chart",
|
||||||
|
"objectives",
|
||||||
|
f,
|
||||||
|
right_layout[1],
|
||||||
|
&ctx.objective_size_timed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_logs {
|
||||||
|
self.draw_logs(f, app, body[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
|
||||||
|
fn draw_time_chart<B>(
|
||||||
|
&mut self,
|
||||||
|
title: &str,
|
||||||
|
y_name: &str,
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
area: Rect,
|
||||||
|
stats: &TimedStats,
|
||||||
|
) where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
if stats.series.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start = stats.series.front().unwrap().time;
|
||||||
|
let end = stats.series.back().unwrap().time;
|
||||||
|
let min_lbl_x = format_duration_hms(&start);
|
||||||
|
let med_lbl_x = format_duration_hms(&((end - start) / 2));
|
||||||
|
let max_lbl_x = format_duration_hms(&end);
|
||||||
|
|
||||||
|
let x_labels = vec![
|
||||||
|
Span::styled(min_lbl_x, Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw(med_lbl_x),
|
||||||
|
Span::styled(max_lbl_x, Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let max_x = u64::from(area.width);
|
||||||
|
let window = end - start;
|
||||||
|
let time_unit = if max_x > window.as_secs() {
|
||||||
|
0 // millis / 10
|
||||||
|
} else if max_x > window.as_secs() * 60 {
|
||||||
|
1 // secs
|
||||||
|
} else {
|
||||||
|
2 // min
|
||||||
|
};
|
||||||
|
let convert_time = |d: &Duration| -> u64 {
|
||||||
|
if time_unit == 0 {
|
||||||
|
(d.as_millis() / 10) as u64
|
||||||
|
} else if time_unit == 1 {
|
||||||
|
d.as_secs()
|
||||||
|
} else {
|
||||||
|
d.as_secs() * 60
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let window_unit = convert_time(&window);
|
||||||
|
if window_unit == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_x = |d: &Duration| (convert_time(d) - convert_time(&start)) * max_x / window_unit;
|
||||||
|
|
||||||
|
self.graph_data.clear();
|
||||||
|
|
||||||
|
let mut max_y = u64::MIN;
|
||||||
|
let mut min_y = u64::MAX;
|
||||||
|
let mut prev = (0, 0);
|
||||||
|
for ts in &stats.series {
|
||||||
|
let x = to_x(&ts.time);
|
||||||
|
if x > prev.0 + 1 && x < max_x {
|
||||||
|
for v in (prev.0 + 1)..x {
|
||||||
|
self.graph_data.push((v as f64, prev.1 as f64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = (x, ts.item);
|
||||||
|
self.graph_data.push((x as f64, ts.item as f64));
|
||||||
|
max_y = max(ts.item, max_y);
|
||||||
|
min_y = min(ts.item, min_y);
|
||||||
|
}
|
||||||
|
if max_x > prev.0 + 1 {
|
||||||
|
for v in (prev.0 + 1)..max_x {
|
||||||
|
self.graph_data.push((v as f64, prev.1 as f64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//println!("max_x: {}, len: {}", max_x, self.graph_data.len());
|
||||||
|
|
||||||
|
let datasets = vec![Dataset::default()
|
||||||
|
//.name("data")
|
||||||
|
.marker(if self.enhanced_graphics {
|
||||||
|
symbols::Marker::Braille
|
||||||
|
} else {
|
||||||
|
symbols::Marker::Dot
|
||||||
|
})
|
||||||
|
.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightYellow)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.data(&self.graph_data)];
|
||||||
|
let chart = Chart::new(datasets)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
title,
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("time")
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.bounds([0.0, max_x as f64])
|
||||||
|
.labels(x_labels),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title(y_name)
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.bounds([min_y as f64, max_y as f64])
|
||||||
|
.labels(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", min_y),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(format!("{}", (max_y - min_y) / 2)),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", max_y),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
f.render_widget(chart, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn draw_text<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let items = vec![
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(Span::raw("run time")),
|
||||||
|
Cell::from(Span::raw(format_duration_hms(
|
||||||
|
&(current_time() - app.read().unwrap().start_time),
|
||||||
|
))),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(Span::raw("clients")),
|
||||||
|
Cell::from(Span::raw(format!("{}", self.clients))),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(Span::raw("executions")),
|
||||||
|
Cell::from(Span::raw(format!("{}", app.read().unwrap().total_execs))),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(Span::raw("exec/sec")),
|
||||||
|
Cell::from(Span::raw(format!(
|
||||||
|
"{}",
|
||||||
|
app.read()
|
||||||
|
.unwrap()
|
||||||
|
.execs_per_sec_timed
|
||||||
|
.series
|
||||||
|
.back()
|
||||||
|
.map_or(0, |x| x.item)
|
||||||
|
))),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(2 + items.len() as u16),
|
||||||
|
Constraint::Min(8),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let table = Table::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
"generic",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||||
|
f.render_widget(table, chunks[0]);
|
||||||
|
|
||||||
|
let client_block = Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
format!("client #{} (l/r arrows to switch)", self.clients_idx),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
let client_area = client_block.inner(chunks[1]);
|
||||||
|
f.render_widget(client_block, chunks[1]);
|
||||||
|
|
||||||
|
let mut client_items = vec![];
|
||||||
|
{
|
||||||
|
let ctx = app.read().unwrap();
|
||||||
|
if let Some(client) = ctx.clients.get(&self.clients_idx) {
|
||||||
|
client_items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("executions")),
|
||||||
|
Cell::from(Span::raw(format!("{}", client.executions))),
|
||||||
|
]));
|
||||||
|
client_items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("exec/sec")),
|
||||||
|
Cell::from(Span::raw(format!("{}", client.exec_sec))),
|
||||||
|
]));
|
||||||
|
client_items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("corpus")),
|
||||||
|
Cell::from(Span::raw(format!("{}", client.corpus))),
|
||||||
|
]));
|
||||||
|
client_items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("objectives")),
|
||||||
|
Cell::from(Span::raw(format!("{}", client.objectives))),
|
||||||
|
]));
|
||||||
|
for (key, val) in &client.user_stats {
|
||||||
|
client_items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw(key.clone())),
|
||||||
|
Cell::from(Span::raw(format!("{}", val.clone()))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
let client_chunks = Layout::default()
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(client_items.len() as u16),
|
||||||
|
Constraint::Min(4),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(client_area);
|
||||||
|
#[cfg(not(feature = "introspection"))]
|
||||||
|
let client_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(100)].as_ref())
|
||||||
|
.split(client_area);
|
||||||
|
|
||||||
|
let table = Table::new(client_items)
|
||||||
|
.block(Block::default())
|
||||||
|
.widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||||
|
f.render_widget(table, client_chunks[0]);
|
||||||
|
|
||||||
|
#[cfg(feature = "introspection")]
|
||||||
|
{
|
||||||
|
let mut items = vec![];
|
||||||
|
{
|
||||||
|
let ctx = app.read().unwrap();
|
||||||
|
if let Some(client) = ctx.introspection.get(&self.clients_idx) {
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("scheduler")),
|
||||||
|
Cell::from(Span::raw(format!("{:.2}%", client.scheduler * 100.0))),
|
||||||
|
]));
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("manager")),
|
||||||
|
Cell::from(Span::raw(format!("{:.2}%", client.manager * 100.0))),
|
||||||
|
]));
|
||||||
|
for i in 0..client.stages.len() {
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw(format!("stage {}", i))),
|
||||||
|
Cell::from(Span::raw("")),
|
||||||
|
]));
|
||||||
|
|
||||||
|
for (key, val) in &client.stages[i] {
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw(key.clone())),
|
||||||
|
Cell::from(Span::raw(format!("{:.2}%", val * 100.0))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (key, val) in &client.feedbacks {
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw(key.clone())),
|
||||||
|
Cell::from(Span::raw(format!("{:.2}%", val * 100.0))),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
items.push(Row::new(vec![
|
||||||
|
Cell::from(Span::raw("not measured")),
|
||||||
|
Cell::from(Span::raw(format!("{:.2}%", client.unmeasured * 100.0))),
|
||||||
|
]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let table = Table::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(Span::styled(
|
||||||
|
"introspection",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.widths(&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||||
|
f.render_widget(table, client_chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unused_self)]
|
||||||
|
fn draw_logs<B>(&mut self, f: &mut Frame<B>, app: &Arc<RwLock<TuiContext>>, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let app = app.read().unwrap();
|
||||||
|
let logs: Vec<ListItem> = app
|
||||||
|
.client_logs
|
||||||
|
.iter()
|
||||||
|
.map(|msg| ListItem::new(Span::raw(msg)))
|
||||||
|
.collect();
|
||||||
|
let logs = List::new(logs).block(
|
||||||
|
Block::default().borders(Borders::ALL).title(Span::styled(
|
||||||
|
"clients logs (`t` to show/hide)",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightCyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
f.render_widget(logs, area);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user