extern crate clap; use anyhow::{anyhow, Result}; use clap::{App, Arg}; use std::fmt; use std::io::{self, Write}; use std::path::Path; use std::sync::mpsc; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use std::thread; use std::time::Duration; use termion::event::Key; use termion::input::TermRead; use termion::raw::IntoRawMode; use tui::{ backend::TermionBackend, buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, terminal::Frame, text::Span, widgets::{Block, Borders, List, ListItem, ListState, Row, StatefulWidget, Table, Widget}, Terminal, }; use thinp::io_engine::*; use thinp::pdata::btree; use thinp::pdata::unpack::*; use thinp::thin::block_time::*; use thinp::thin::device_detail::*; use thinp::thin::superblock::*; //------------------------------------ pub enum Event { Input(I), Tick, } pub struct Events { rx: mpsc::Receiver>, input_handle: thread::JoinHandle<()>, ignore_exit_key: Arc, } #[derive(Debug, Clone, Copy)] pub struct Config { pub exit_key: Key, pub tick_rate: Duration, } impl Default for Config { fn default() -> Config { Config { exit_key: Key::Char('q'), tick_rate: Duration::from_millis(250), } } } impl Events { pub fn new() -> Events { Events::with_config(Config::default()) } pub fn with_config(config: Config) -> Events { let (tx, rx) = mpsc::channel(); let ignore_exit_key = Arc::new(AtomicBool::new(false)); let input_handle = { let ignore_exit_key = ignore_exit_key.clone(); thread::spawn(move || { let stdin = io::stdin(); for key in stdin.keys().flatten() { if let Err(err) = tx.send(Event::Input(key)) { eprintln!("{}", err); return; } if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { return; } } }) }; Events { rx, input_handle, ignore_exit_key, } } pub fn next(&self) -> Result, mpsc::RecvError> { self.rx.recv() } pub fn disable_exit_key(&mut self) { self.ignore_exit_key.store(true, Ordering::Relaxed); } pub fn enable_exit_key(&mut self) { self.ignore_exit_key.store(false, Ordering::Relaxed); } } impl Default for Events { fn default() -> Self { Self::new() } } //------------------------------------ fn ls_next(ls: &mut ListState, max: usize) { let i = match ls.selected() { Some(i) => { if i >= max - 1 { max - 1 } else { i + 1 } } None => 0, }; ls.select(Some(i)); } fn ls_previous(ls: &mut ListState) { let i = match ls.selected() { Some(i) => { if i == 0 { 0 } else { i - 1 } } None => 0, }; ls.select(Some(i)); } //------------------------------------ struct SBWidget<'a> { sb: &'a Superblock, } impl<'a> StatefulWidget for SBWidget<'a> { type State = ListState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut ListState) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(10), Constraint::Percentage(80)].as_ref()) .split(area); let sb = self.sb; let flags = vec!["flags".to_string(), format!("{}", sb.flags)]; let block = vec!["block".to_string(), format!("{}", sb.block)]; let uuid = vec!["uuid".to_string(), "-".to_string()]; let version = vec!["version".to_string(), format!("{}", sb.version)]; let time = vec!["time".to_string(), format!("{}", sb.time)]; let transaction_id = vec![ "transaction_id".to_string(), format!("{}", sb.transaction_id), ]; let metadata_snap = vec![ "metadata_snap".to_string(), if sb.metadata_snap == 0 { "-".to_string() } else { format!("{}", sb.metadata_snap) }, ]; let mapping_root = vec!["mapping root".to_string(), format!("{}", sb.mapping_root)]; let details_root = vec!["details root".to_string(), format!("{}", sb.details_root)]; let data_block_size = vec![ "data block size".to_string(), format!("{}k", sb.data_block_size * 2), ]; let table = Table::new(vec![ Row::new(flags), Row::new(block), Row::new(uuid), Row::new(version), Row::new(time), Row::new(transaction_id), Row::new(metadata_snap), Row::new(mapping_root), Row::new(details_root), Row::new(data_block_size), ]) .header(Row::new(vec!["Field", "Value"]).style(Style::default().fg(Color::Yellow))) .block( Block::default() .borders(Borders::ALL) .title("Superblock".to_string()), ) .widths(&[Constraint::Length(20), Constraint::Length(60)]) .style(Style::default().fg(Color::White)) .column_spacing(1); Widget::render(table, chunks[0], buf); let items = vec![ ListItem::new(Span::raw("Device tree".to_string())), ListItem::new(Span::raw("Mapping tree".to_string())), ]; let items = List::new(items) .block(Block::default().borders(Borders::ALL).title("Entries")) .highlight_style( Style::default() .bg(Color::LightGreen) .add_modifier(Modifier::BOLD), ); StatefulWidget::render(items, chunks[1], buf, state); } } //------------------------------------ struct HeaderWidget<'a> { title: String, hdr: &'a btree::NodeHeader, } impl<'a> Widget for HeaderWidget<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let hdr = &self.hdr; let block = vec!["block".to_string(), format!("{}", hdr.block)]; let kind = vec![ "type".to_string(), match hdr.is_leaf { true => "LEAF".to_string(), false => "INTERNAL".to_string(), }, ]; let nr_entries = vec!["nr_entries".to_string(), format!("{}", hdr.nr_entries)]; let max_entries = vec!["max_entries".to_string(), format!("{}", hdr.max_entries)]; let value_size = vec!["value size".to_string(), format!("{}", hdr.value_size)]; let table = Table::new(vec![ Row::new(block), Row::new(kind), Row::new(nr_entries), Row::new(max_entries), Row::new(value_size), ]) .header(Row::new(vec!["Field", "Value"]).style(Style::default().fg(Color::Yellow))) .block(Block::default().borders(Borders::ALL).title(self.title)) .widths(&[Constraint::Length(20), Constraint::Length(60)]) .style(Style::default().fg(Color::White)) .column_spacing(1); Widget::render(table, area, buf); } } fn read_node(engine: &dyn IoEngine, loc: u64) -> Result> { let b = engine.read(loc)?; let path = Vec::new(); btree::unpack_node(&path, b.get_data(), true, false) .map_err(|_| anyhow!("couldn't unpack btree node")) } //------------------------------------ // For types that have the concept of adjacency, but not of a distance // between values. For instance with a BlockTime there is no delta that // will get between two values with different times. trait Adjacent { fn adjacent(&self, rhs: &Self) -> bool; } impl Adjacent for u64 { fn adjacent(&self, rhs: &Self) -> bool { (*self + 1) == *rhs } } impl Adjacent for BlockTime { fn adjacent(&self, rhs: &Self) -> bool { if self.time != rhs.time { return false; } self.block + 1 == rhs.block } } impl Adjacent for DeviceDetail { fn adjacent(&self, _rhs: &Self) -> bool { false } } impl Adjacent for (X, Y) { fn adjacent(&self, rhs: &Self) -> bool { self.0.adjacent(&rhs.0) && self.1.adjacent(&rhs.1) } } fn adjacent_runs(mut ns: Vec) -> Vec<(V, usize)> { let mut result = Vec::new(); if ns.is_empty() { return result; } // Reverse so we can pop without cloning the value. ns.reverse(); let mut base = ns.pop().unwrap(); let mut current = base; let mut len = 1; while let Some(v) = ns.pop() { if current.adjacent(&v) { current = v; len += 1; } else { result.push((base, len)); base = v; current = v; len = 1; } } result.push((base, len)); result } fn mk_runs(keys: &[u64], values: &[V]) -> Vec<((u64, V), usize)> { let mut pairs = Vec::new(); for (k, v) in keys.iter().zip(values.iter()) { pairs.push((*k, *v)); } adjacent_runs(pairs) } //------------------------------------ struct NodeWidget<'a, V: Unpack + Adjacent + Clone> { title: String, node: &'a btree::Node, } fn mk_item<'a, V: fmt::Display>(k: u64, v: &V, len: usize) -> ListItem<'a> { if len > 1 { ListItem::new(Span::raw(format!("{} x {} -> {}", k, len as u64, v))) } else { ListItem::new(Span::raw(format!("{} -> {}", k, v))) } } fn mk_items<'a, V>(keys: &[u64], values: &[V], selected: usize) -> (Vec>, usize) where V: Adjacent + Copy + fmt::Display, { let mut items = Vec::new(); let bkeys = &keys[0..selected]; let key = keys[selected]; let akeys = &keys[(selected + 1)..]; let bvalues = &values[0..selected]; let value = values[selected]; let avalues = &values[(selected + 1)..]; let bruns = mk_runs(bkeys, bvalues); let aruns = mk_runs(akeys, avalues); let i = bruns.len(); for ((k, v), len) in bruns { items.push(mk_item(k, &v, len)); } items.push(ListItem::new(Span::raw(format!("{} -> {}", key, value)))); for ((k, v), len) in aruns { items.push(mk_item(k, &v, len)); } (items, i) } impl<'a, V: Unpack + fmt::Display + Adjacent + Copy> StatefulWidget for NodeWidget<'a, V> { type State = ListState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut ListState) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(10), Constraint::Percentage(80)].as_ref()) .split(area); let hdr = HeaderWidget { title: self.title, hdr: self.node.get_header(), }; hdr.render(chunks[0], buf); let items: Vec; let i: usize; let selected = state.selected().unwrap(); let mut state = ListState::default(); match self.node { btree::Node::Internal { keys, values, .. } => { let (items_, i_) = mk_items(keys, values, selected); items = items_; i = i_; } btree::Node::Leaf { keys, values, .. } => { let (items_, i_) = mk_items(keys, values, selected); items = items_; i = i_; } } state.select(Some(i)); let items = List::new(items) .block(Block::default().borders(Borders::ALL).title("Entries")) .highlight_style( Style::default() .bg(Color::LightGreen) .add_modifier(Modifier::BOLD), ); StatefulWidget::render(items, chunks[1], buf, &mut state); } } //------------------------------------ enum Action { PushDeviceDetail(u64), PushTopLevel(u64), PushBottomLevel(u32, u64), PopPanel, } use Action::*; type Frame_<'a, 'b> = Frame<'a, TermionBackend>>>; trait Panel { fn render(&mut self, area: Rect, f: &mut Frame_); fn input(&mut self, k: Key) -> Option; fn path_action(&mut self, child: u64) -> Option; } //------------------------------------ struct SBPanel { sb: Superblock, state: ListState, } impl SBPanel { fn new(sb: Superblock) -> SBPanel { let mut state = ListState::default(); state.select(Some(0)); SBPanel { sb, state } } } impl Panel for SBPanel { fn render(&mut self, area: Rect, f: &mut Frame_) { let w = SBWidget { sb: &self.sb }; f.render_stateful_widget(w, area, &mut self.state); } fn input(&mut self, k: Key) -> Option { match k { Key::Char('j') | Key::Down => { ls_next(&mut self.state, 2); None } Key::Char('k') | Key::Up => { ls_previous(&mut self.state); None } Key::Char('l') | Key::Right => { if self.state.selected().unwrap() == 0 { Some(PushDeviceDetail(self.sb.details_root)) } else { Some(PushTopLevel(self.sb.mapping_root)) } } Key::Char('h') | Key::Left => Some(PopPanel), _ => None, } } fn path_action(&mut self, child: u64) -> Option { if child == self.sb.mapping_root { Some(PushTopLevel(child)) } else if child == self.sb.details_root { Some(PushDeviceDetail(child)) } else { None } } } //------------------------------------ struct DeviceDetailPanel { node: btree::Node, nr_entries: usize, state: ListState, } impl DeviceDetailPanel { fn new(node: btree::Node) -> DeviceDetailPanel { let nr_entries = node.get_header().nr_entries as usize; let mut state = ListState::default(); state.select(Some(0)); DeviceDetailPanel { node, nr_entries, state, } } } impl Panel for DeviceDetailPanel { fn render(&mut self, area: Rect, f: &mut Frame_) { let w = NodeWidget { title: "Device Details".to_string(), node: &self.node, }; f.render_stateful_widget(w, area, &mut self.state); } fn input(&mut self, k: Key) -> Option { match k { Key::Char('j') | Key::Down => { ls_next(&mut self.state, self.nr_entries); None } Key::Char('k') | Key::Up => { ls_previous(&mut self.state); None } Key::Char('l') | Key::Right => match &self.node { btree::Node::Internal { values, .. } => { Some(PushDeviceDetail(values[self.state.selected().unwrap()])) } btree::Node::Leaf { .. } => None, }, Key::Char('h') | Key::Left => Some(PopPanel), _ => None, } } fn path_action(&mut self, child: u64) -> Option { match &self.node { btree::Node::Internal { values, .. } => { for (i, v) in values.iter().enumerate() { if *v == child { self.state.select(Some(i)); return Some(PushDeviceDetail(child)); } } None } btree::Node::Leaf { .. } => None, } } } //------------------------------------ struct TopLevelPanel { node: btree::Node, nr_entries: usize, state: ListState, } impl TopLevelPanel { fn new(node: btree::Node) -> TopLevelPanel { let nr_entries = node.get_header().nr_entries as usize; let mut state = ListState::default(); state.select(Some(0)); TopLevelPanel { node, nr_entries, state, } } } impl Panel for TopLevelPanel { fn render(&mut self, area: Rect, f: &mut Frame_) { let w = NodeWidget { title: "Top Level".to_string(), node: &self.node, }; f.render_stateful_widget(w, area, &mut self.state); } fn input(&mut self, k: Key) -> Option { match k { Key::Char('j') | Key::Down => { ls_next(&mut self.state, self.nr_entries); None } Key::Char('k') | Key::Up => { ls_previous(&mut self.state); None } Key::Char('l') | Key::Right => match &self.node { btree::Node::Internal { values, .. } => { Some(PushTopLevel(values[self.state.selected().unwrap()])) } btree::Node::Leaf { values, keys, .. } => { let index = self.state.selected().unwrap(); Some(PushBottomLevel(keys[index] as u32, values[index])) } }, Key::Char('h') | Key::Left => Some(PopPanel), _ => None, } } fn path_action(&mut self, child: u64) -> Option { match &self.node { btree::Node::Internal { values, .. } => { for (i, v) in values.iter().enumerate() { if *v == child { self.state.select(Some(i)); return Some(PushTopLevel(child)); } } None } btree::Node::Leaf { keys, values, .. } => { for i in 0..values.len() { if values[i] == child { self.state.select(Some(i)); return Some(PushBottomLevel(keys[i] as u32, child)); } } None } } } } struct BottomLevelPanel { thin_id: u32, node: btree::Node, nr_entries: usize, state: ListState, } impl BottomLevelPanel { fn new(thin_id: u32, node: btree::Node) -> BottomLevelPanel { let nr_entries = node.get_header().nr_entries as usize; let mut state = ListState::default(); state.select(Some(0)); BottomLevelPanel { thin_id, node, nr_entries, state, } } } impl Panel for BottomLevelPanel { fn render(&mut self, area: Rect, f: &mut Frame_) { let w = NodeWidget { title: format!("Thin dev #{}", self.thin_id), node: &self.node, }; f.render_stateful_widget(w, area, &mut self.state); } fn input(&mut self, k: Key) -> Option { match k { Key::Char('j') | Key::Down => { ls_next(&mut self.state, self.nr_entries); None } Key::Char('k') | Key::Up => { ls_previous(&mut self.state); None } Key::Char('l') | Key::Right => match &self.node { btree::Node::Internal { values, .. } => Some(PushBottomLevel( self.thin_id, values[self.state.selected().unwrap()], )), _ => None, }, Key::Char('h') | Key::Left => Some(PopPanel), _ => None, } } fn path_action(&mut self, child: u64) -> Option { match &self.node { btree::Node::Internal { values, .. } => { for (i, v) in values.iter().enumerate() { if *v == child { self.state.select(Some(i)); return Some(PushBottomLevel(self.thin_id, child)); } } None } btree::Node::Leaf { .. } => None, } } } //------------------------------------ fn perform_action( panels: &mut Vec>, engine: &dyn IoEngine, action: Action, ) -> Result<()> { match action { PushDeviceDetail(b) => { let node = read_node::(engine, b)?; panels.push(Box::new(DeviceDetailPanel::new(node))); } PushTopLevel(b) => { let node = read_node::(engine, b)?; panels.push(Box::new(TopLevelPanel::new(node))); } PushBottomLevel(thin_id, b) => { let node = read_node::(engine, b)?; panels.push(Box::new(BottomLevelPanel::new(thin_id, node))); } PopPanel => { if panels.len() > 1 { panels.pop(); } } }; Ok(()) } fn explore(path: &Path, node_path: Option>) -> Result<()> { let engine = SyncIoEngine::new(path, 1, false)?; let mut panels: Vec> = Vec::new(); if let Some(path) = node_path { eprintln!("using path: {:?}", path); assert_eq!(path[0], 0); let sb = read_superblock(&engine, path[0])?; panels.push(Box::new(SBPanel::new(sb))); for b in &path[1..] { let action = panels.last_mut().unwrap().path_action(*b); if let Some(action) = action { perform_action(&mut panels, &engine, action)?; } else { return Err(anyhow!("bad node path: couldn't find child node {}", b)); } } } else { let sb = read_superblock(&engine, 0)?; panels.push(Box::new(SBPanel::new(sb))); } let events = Events::new(); let stdout = io::stdout(); let mut stdout = stdout.lock().into_raw_mode()?; write!(stdout, "{}", termion::clear::All)?; let backend = TermionBackend::new(stdout); let mut terminal = Terminal::new(backend)?; 'main: loop { let render_panels = |f: &mut Frame_| { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(f.size()); let mut base = panels.len(); if base >= 2 { base -= 2; } else { base = 0; } for i in base..panels.len() { panels[i].render(chunks[i - base], f); } }; terminal.draw(render_panels)?; let last = panels.len() - 1; let active_panel = &mut panels[last]; if let Event::Input(key) = events.next()? { match key { Key::Char('q') => break 'main, _ => { if let Some(action) = active_panel.input(key) { perform_action(&mut panels, &engine, action)?; } } } } } events.input_handle.join().unwrap(); Ok(()) } //------------------------------------ fn main() -> Result<()> { let parser = App::new("thin_explore") .version(thinp::version::tools_version()) .about("A text user interface for examining thin metadata.") .arg( Arg::with_name("NODE_PATH") .help("Pass in a node path as output by thin_check") .short("p") .long("node-path") .value_name("NODE_PATH"), ) .arg( Arg::with_name("INPUT") .help("Specify the input device to check") .required(true) .index(1), ); let matches = parser.get_matches(); let node_path = matches .value_of("NODE_PATH") .map(|text| btree::decode_node_path(text).unwrap()); let input_file = Path::new(matches.value_of("INPUT").unwrap()); explore(input_file, node_path) } //------------------------------------