below_view/
lib.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![deny(clippy::all)]
16
17/// View Module defines how to render below inside terminal.
18///
19/// ## High level architecture
20/// ```text
21///  ------------------------------------------------------------
22/// |                      Status Bar                            |
23///  ------------------------------------------------------------
24///  ------------------------------------------------------------
25/// |                      System View                           |
26///  ------------------------------------------------------------
27///  ------------------------------------------------------------
28/// |                      Stats View                            |
29///  ------------------------------------------------------------
30/// ```
31/// * Status Bar: Displays datetime, elapsed time, hostname, and below version.
32/// * System View: Displays overall system stats including cpu, mem, io, iface, transport, and network.
33/// * Stats View: Display the detailed stats. Please check the stats view section for more details.
34///
35/// ### Stats View
36/// ```text
37///  ------------------------------------------------------------
38/// |                         Tabs                               |
39/// | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
40/// |                 Column name in Bold                        |
41/// |                 Selectable Stats View                      |
42///  ------------------------------------------------------------
43/// |                 Command Palette                            |
44///  ------------------------------------------------------------
45/// ```
46/// * Tabs: Defines the topics of stats view. Each stats view by default will show only the general stats:
47///   a combination of all important stats from each topic. User can use `tab` or `shift-tab` to switch
48///   between different tabs to see the detailed view of that topic. For example, for cgroup view, the `general` tab
49///   will only show pressure status of cpu_some, memory_full, io_full. But the `pressure` tab will show all
50///   pressure related stats including cpu_some, memory_some, memory_full, io_some, io_full.
51///
52/// * Column names: The column names line also called title line in below_derive. It defines the table column of
53///   the following selectable view. A user can press `,` or `.` to switch between different columns and press `s`
54///   or `S` to sort in ascending or descending order.
55use std::cell::RefCell;
56use std::collections::HashMap;
57use std::rc::Rc;
58use std::time::Duration;
59use std::time::SystemTime;
60
61use anyhow::Result;
62use common::logutil::get_last_log_to_display;
63use common::open_source_shim;
64use common::util::get_belowrc_cmd_section_key;
65use common::util::get_belowrc_filename;
66use common::util::get_belowrc_view_section_key;
67use crossterm::event::DisableMouseCapture;
68use crossterm::execute;
69use cursive::event::Event;
70use cursive::theme::BaseColor;
71use cursive::theme::Color;
72use cursive::theme::PaletteColor;
73use cursive::view::Nameable;
74use cursive::views::BoxedView;
75use cursive::views::LinearLayout;
76use cursive::views::NamedView;
77use cursive::views::OnEventView;
78use cursive::views::Panel;
79use cursive::views::ResizedView;
80use cursive::views::ScreensView;
81use cursive::Cursive;
82use cursive::CursiveRunnable;
83use cursive::ScreenId;
84use model::CgroupModel;
85#[cfg(fbcode_build)]
86use model::GpuModel;
87use model::Model;
88use model::NetworkModel;
89use model::ProcessModel;
90use model::SystemModel;
91use store::Advance;
92use toml::value::Value;
93use viewrc::ViewRc;
94extern crate render as base_render;
95
96open_source_shim!();
97
98mod cgroup_tabs;
99pub mod cgroup_view;
100pub mod command_palette;
101mod default_styles;
102mod filter_popup;
103mod help_menu;
104mod process_tabs;
105mod process_view;
106mod render;
107pub mod stats_view;
108mod status_bar;
109mod summary_view;
110mod system_tabs;
111mod system_view;
112mod tab_view;
113
114pub struct View {
115    inner: CursiveRunnable,
116}
117
118macro_rules! advance {
119    ($c:ident, $adv:ident, $dir:expr) => {
120        match $adv.advance($dir) {
121            Some(data) => {
122                $c.user_data::<ViewState>()
123                    .expect("No user data set")
124                    .update(data);
125            }
126            None => view_warn!(
127                $c,
128                "Data is not available{}",
129                if $dir == Direction::Forward {
130                    " yet."
131                } else {
132                    "."
133                }
134            ),
135        }
136    };
137}
138
139// Raise warning message in current view.
140macro_rules! view_warn {
141    ($c:ident, $($args:tt)*) => {{
142        let state = $c
143            .user_data::<crate::ViewState>()
144            .expect("No user data set")
145            .main_view_state
146            .clone();
147        let msg = format!($($args)*);
148        match state {
149            crate::MainViewState::Cgroup => crate::cgroup_view::ViewType::cp_warn($c, &msg),
150            crate::MainViewState::Process(_) =>
151                crate::process_view::ViewType::cp_warn($c, &msg),
152            crate::MainViewState::System => crate::system_view::ViewType::cp_warn($c, &msg),
153            #[cfg(fbcode_build)]
154            crate::MainViewState::Gpu => crate::gpu_view::ViewType::cp_warn($c, &msg),
155        }
156    }};
157}
158
159// controllers depends on Advance
160pub mod controllers;
161pub mod viewrc;
162// Jump popup depends on view_warn
163mod jump_popup;
164
165#[derive(Clone, Debug, PartialEq)]
166pub enum ProcessZoomState {
167    NoZoom,
168    Cgroup,
169    Pids,
170}
171
172#[derive(Clone, Debug, PartialEq)]
173pub enum MainViewState {
174    Cgroup,
175    Process(ProcessZoomState),
176    System,
177    #[cfg(fbcode_build)]
178    Gpu,
179}
180
181impl MainViewState {
182    pub fn is_process_zoom_state(&self) -> bool {
183        matches!(&self, &MainViewState::Process(zoom) if zoom != &ProcessZoomState::NoZoom)
184    }
185}
186
187#[derive(Clone)]
188pub enum ViewMode {
189    Live(Rc<RefCell<Advance>>),
190    Pause(Rc<RefCell<Advance>>),
191    Replay(Rc<RefCell<Advance>>),
192}
193
194// Invoked either when the data view was explicitly advanced, or
195// periodically (during live mode)
196fn refresh(c: &mut Cursive) {
197    status_bar::refresh(c);
198    summary_view::refresh(c);
199    let current_state = c
200        .user_data::<ViewState>()
201        .expect("No data stored in Cursive object!")
202        .main_view_state
203        .clone();
204    match current_state {
205        MainViewState::Cgroup => cgroup_view::CgroupView::refresh(c),
206        MainViewState::Process(_) => process_view::ProcessView::refresh(c),
207        MainViewState::System => system_view::SystemView::refresh(c),
208        #[cfg(fbcode_build)]
209        MainViewState::Gpu => gpu_view::GpuView::refresh(c),
210    }
211}
212
213pub fn set_active_screen(c: &mut Cursive, name: &str) {
214    let screen_id = *c
215        .user_data::<ViewState>()
216        .expect("No data stored in Cursive object!")
217        .main_view_screens
218        .get(name)
219        .unwrap_or_else(|| panic!("Failed to find screen id for {}", name));
220    c.call_on_name(
221        "main_view_screens",
222        |screens: &mut NamedView<ScreensView>| {
223            screens.get_mut().set_active_screen(screen_id);
224            screens
225                .get_mut()
226                .screen_mut()
227                .unwrap()
228                .take_focus(cursive::direction::Direction::none())
229                .ok();
230        },
231    )
232    .expect("failed to find main_view_screens");
233}
234
235pub struct ViewState {
236    pub time_elapsed: Duration,
237    /// Keep track of the lowest seen `time_elapsed` so that view can highlight abnormal
238    /// elapsed times. Below will never go faster than the requested interval rate but
239    /// can certainly go higher (b/c of a loaded system or other delays).
240    pub lowest_time_elapsed: Duration,
241    pub timestamp: SystemTime,
242    // TODO: Replace other fields with model
243    pub model: Rc<RefCell<Model>>,
244    pub system: Rc<RefCell<SystemModel>>,
245    pub cgroup: Rc<RefCell<CgroupModel>>,
246    pub process: Rc<RefCell<ProcessModel>>,
247    pub network: Rc<RefCell<NetworkModel>>,
248    #[cfg(fbcode_build)]
249    pub gpu: Rc<RefCell<Option<GpuModel>>>,
250    pub main_view_state: MainViewState,
251    pub main_view_screens: HashMap<String, ScreenId>,
252    pub mode: ViewMode,
253    pub viewrc: ViewRc,
254    pub viewrc_error: Option<String>,
255    pub event_controllers: Rc<RefCell<HashMap<Event, controllers::Controllers>>>,
256    pub cmd_controllers: Rc<RefCell<HashMap<&'static str, controllers::Controllers>>>,
257}
258
259impl ViewState {
260    pub fn update(&mut self, model: Model) {
261        self.time_elapsed = model.time_elapsed;
262        if model.time_elapsed.as_secs() != 0 && model.time_elapsed < self.lowest_time_elapsed {
263            self.lowest_time_elapsed = model.time_elapsed;
264        }
265        self.timestamp = model.timestamp;
266        self.model.replace(model.clone());
267        self.system.replace(model.system);
268        self.cgroup.replace(model.cgroup);
269        self.process.replace(model.process);
270        self.network.replace(model.network);
271        #[cfg(fbcode_build)]
272        self.gpu.replace(model.gpu);
273    }
274
275    pub fn new_with_advance(
276        main_view_state: MainViewState,
277        model: Model,
278        mode: ViewMode,
279        viewrc: ViewRc,
280        viewrc_error: Option<String>,
281    ) -> Self {
282        Self {
283            time_elapsed: model.time_elapsed,
284            lowest_time_elapsed: model.time_elapsed,
285            timestamp: model.timestamp,
286            model: Rc::new(RefCell::new(model.clone())),
287            system: Rc::new(RefCell::new(model.system)),
288            cgroup: Rc::new(RefCell::new(model.cgroup)),
289            process: Rc::new(RefCell::new(model.process)),
290            network: Rc::new(RefCell::new(model.network)),
291            #[cfg(fbcode_build)]
292            gpu: Rc::new(RefCell::new(model.gpu)),
293            main_view_state,
294            main_view_screens: HashMap::new(),
295            mode,
296            viewrc,
297            viewrc_error,
298            event_controllers: Rc::new(RefCell::new(HashMap::new())),
299            cmd_controllers: Rc::new(RefCell::new(controllers::make_cmd_controller_map())),
300        }
301    }
302
303    pub fn view_mode_str(&self) -> &'static str {
304        match self.mode {
305            ViewMode::Live(_) => "live",
306            ViewMode::Pause(_) => "live-paused",
307            ViewMode::Replay(_) => "replay",
308        }
309    }
310
311    pub fn is_paused(&self) -> bool {
312        matches!(self.mode, ViewMode::Pause(_))
313    }
314}
315
316impl View {
317    pub fn new_with_advance(model: model::Model, mode: ViewMode) -> View {
318        let mut inner = cursive::CursiveRunnable::new(|| {
319            let backend = cursive::backends::crossterm::Backend::init().map(|backend| {
320                Box::new(cursive_buffered_backend::BufferedBackend::new(backend))
321                    as Box<(dyn cursive::backend::Backend)>
322            });
323            execute!(std::io::stdout(), DisableMouseCapture).expect("Failed to disable mouse.");
324            backend
325        });
326        let (viewrc, viewrc_error) = viewrc::ViewRc::new();
327        inner.set_user_data(ViewState::new_with_advance(
328            MainViewState::Cgroup,
329            model,
330            mode,
331            viewrc,
332            viewrc_error,
333        ));
334        View { inner }
335    }
336
337    pub fn cb_sink(&mut self) -> &::cursive::CbSink {
338        self.inner.set_fps(4);
339        self.inner.cb_sink()
340    }
341
342    // Function to generate event_controller_map, we cannot make
343    // event_controller_map during ViewState construction since it
344    // depends on CommandPalette to construct for raising errors
345    pub fn generate_event_controller_map(c: &mut Cursive, filename: String) {
346        // Verify belowrc file format
347        let cmdrc_opt = match std::fs::read_to_string(filename) {
348            Ok(belowrc_str) => match belowrc_str.parse::<Value>() {
349                Ok(belowrc) => belowrc
350                    .get(get_belowrc_cmd_section_key())
351                    .map(|cmdrc| cmdrc.to_owned()),
352                Err(e) => {
353                    view_warn!(c, "Failed to parse belowrc: {}", e);
354                    None
355                }
356            },
357            _ => None,
358        };
359
360        let event_controller_map = controllers::make_event_controller_map(c, &cmdrc_opt);
361
362        c.user_data::<ViewState>()
363            .expect("No data stored in Cursive object!")
364            .event_controllers
365            .replace(event_controller_map);
366    }
367
368    pub fn run(&mut self) -> Result<()> {
369        let mut theme = self.inner.current_theme().clone();
370        theme.palette[PaletteColor::Background] = Color::TerminalDefault;
371        theme.palette[PaletteColor::View] = Color::TerminalDefault;
372        theme.palette[PaletteColor::Primary] = Color::TerminalDefault;
373        theme.palette[PaletteColor::Highlight] = Color::Dark(BaseColor::Cyan);
374        theme.palette[PaletteColor::HighlightText] = Color::Dark(BaseColor::Black);
375        theme.shadow = false;
376
377        self.inner.set_theme(theme);
378
379        self.inner
380            .add_global_callback(Event::CtrlChar('z'), |c| unsafe {
381                use crossterm::cursor::Hide;
382                use crossterm::cursor::Show;
383                use crossterm::terminal::EnterAlternateScreen;
384                use crossterm::terminal::LeaveAlternateScreen;
385
386                // The following logic is necessary on crossterm as it does not
387                // disable/re-enable tty on SIGTSTP, while ncurses does.
388
389                // Reset tty to original mode
390                execute!(std::io::stdout(), LeaveAlternateScreen, Show)
391                    .expect("Failed to reset tty");
392                crossterm::terminal::disable_raw_mode().expect("Failed to disable tty");
393
394                // Send signal to put process to background
395                if libc::raise(libc::SIGTSTP) != 0 {
396                    panic!("failed to SIGTSTP self");
397                }
398
399                // Re-enable tty
400                crossterm::terminal::enable_raw_mode().expect("Failed to enable tty");
401                execute!(std::io::stdout(), EnterAlternateScreen, Hide)
402                    .expect("Failed to setup tty");
403                // Use WindowResize event to force redraw everything.
404                c.on_event(Event::WindowResize);
405            });
406        self.inner.add_global_callback(Event::Refresh, |c| {
407            refresh(c);
408        });
409        self.inner.add_global_callback(Event::CtrlChar('r'), |c| {
410            c.clear();
411            refresh(c);
412        });
413
414        // Used to handle warning assignment to the correct view
415        let init_warnings = get_last_log_to_display();
416
417        let status_bar = status_bar::new(&mut self.inner);
418        let summary_view = summary_view::new(&mut self.inner);
419        let cgroup_view = cgroup_view::CgroupView::new(&mut self.inner);
420        let process_view = process_view::ProcessView::new(&mut self.inner);
421        let system_view = system_view::SystemView::new(&mut self.inner);
422        #[cfg(fbcode_build)]
423        let gpu_view = gpu_view::GpuView::new(&mut self.inner);
424
425        let mut screens_view = ScreensView::new();
426        let main_view_screens = &mut self
427            .inner
428            .user_data::<ViewState>()
429            .expect("No data stored in Cursive object!")
430            .main_view_screens;
431        main_view_screens.insert(
432            "cgroup_view_panel".to_owned(),
433            screens_view.add_screen(BoxedView::boxed(ResizedView::with_full_screen(cgroup_view))),
434        );
435        main_view_screens.insert(
436            "process_view_panel".to_owned(),
437            screens_view.add_screen(BoxedView::boxed(ResizedView::with_full_screen(
438                process_view,
439            ))),
440        );
441        main_view_screens.insert(
442            "system_view_panel".to_owned(),
443            screens_view.add_screen(BoxedView::boxed(ResizedView::with_full_screen(system_view))),
444        );
445        #[cfg(fbcode_build)]
446        main_view_screens.insert(
447            "gpu_view_panel".to_owned(),
448            screens_view.add_screen(BoxedView::boxed(ResizedView::with_full_screen(gpu_view))),
449        );
450
451        self.inner
452            .add_fullscreen_layer(ResizedView::with_full_screen(
453                LinearLayout::vertical()
454                    .child(Panel::new(status_bar))
455                    .child(Panel::new(summary_view))
456                    .child(
457                        OnEventView::new(screens_view.with_name("main_view_screens"))
458                            .with_name("dynamic_view"),
459                    ),
460            ));
461
462        self.inner
463            .focus_name("dynamic_view")
464            .expect("Could not set focus at initialization!");
465
466        // Set default view from viewrc
467        if let Some(view) = self
468            .inner
469            .user_data::<ViewState>()
470            .expect("No data stored in Cursive object!")
471            .viewrc
472            .default_view
473            .clone()
474        {
475            let main_view_state = &mut self
476                .inner
477                .user_data::<ViewState>()
478                .expect("No data stored in Cursive object!")
479                .main_view_state;
480            match view {
481                viewrc::DefaultFrontView::Cgroup => {
482                    *main_view_state = MainViewState::Cgroup;
483                    set_active_screen(&mut self.inner, "cgroup_view_panel")
484                }
485                viewrc::DefaultFrontView::Process => {
486                    *main_view_state = MainViewState::Process(ProcessZoomState::NoZoom);
487                    set_active_screen(&mut self.inner, "process_view_panel")
488                }
489                viewrc::DefaultFrontView::System => {
490                    *main_view_state = MainViewState::System;
491                    set_active_screen(&mut self.inner, "system_view_panel")
492                }
493            }
494        }
495
496        // Raise warning message if failed to map the customized command.
497        Self::generate_event_controller_map(&mut self.inner, get_belowrc_filename());
498        if let Some(msg) = &self
499            .inner
500            .user_data::<ViewState>()
501            .expect("No data stored in Cursive object!")
502            .viewrc_error
503        {
504            let msg = msg.clone();
505            let c = &mut self.inner;
506            view_warn!(c, "{}", msg);
507        }
508        if let Some(msg) = init_warnings {
509            let c = &mut self.inner;
510            view_warn!(c, "{}", msg);
511        }
512        self.inner.run();
513
514        Ok(())
515    }
516}
517
518#[cfg(test)]
519pub mod fake_view {
520    use std::cell::RefCell;
521    use std::path::PathBuf;
522    use std::rc::Rc;
523
524    use common::logutil::get_logger;
525    use cursive::views::DummyView;
526    use cursive::views::ViewRef;
527    use model::Collector;
528    use store::advance::new_advance_local;
529
530    use self::viewrc::ViewRc;
531    use super::*;
532    use crate::cgroup_view::CgroupView;
533    use crate::command_palette::CommandPalette;
534    use crate::stats_view::StatsView;
535    use crate::MainViewState;
536    use crate::ViewMode;
537    use crate::ViewState;
538
539    pub struct FakeView {
540        pub inner: CursiveRunnable,
541    }
542
543    #[allow(clippy::new_without_default)]
544    impl FakeView {
545        pub fn new() -> Self {
546            let time = SystemTime::now();
547            let logger = get_logger();
548            let advance = new_advance_local(logger.clone(), PathBuf::new(), time);
549            let mut collector = Collector::new(logger.clone(), Default::default());
550            let model = collector
551                .collect_and_update_model()
552                .expect("Fail to get model");
553
554            let mut inner = CursiveRunnable::dummy();
555            let mut user_data = ViewState::new_with_advance(
556                MainViewState::Cgroup,
557                model,
558                ViewMode::Live(Rc::new(RefCell::new(advance))),
559                ViewRc::default(),
560                None,
561            );
562            // Dummy screen to make switching panel no-op except state changes.
563            inner.add_layer(
564                ScreensView::single_screen(BoxedView::boxed(DummyView))
565                    .with_name("main_view_screens"),
566            );
567            user_data.main_view_screens = [
568                ("cgroup_view_panel".to_owned(), 0),
569                ("process_view_panel".to_owned(), 0),
570                ("system_view_panel".to_owned(), 0),
571            ]
572            .into();
573            inner.set_user_data(user_data);
574
575            Self { inner }
576        }
577
578        pub fn add_cgroup_view(&mut self) {
579            let cgroup_view = CgroupView::new(&mut self.inner);
580            self.inner.add_layer(cgroup_view);
581        }
582
583        pub fn get_cmd_palette(&mut self, name: &str) -> ViewRef<CommandPalette> {
584            self.inner
585                .find_name::<StatsView<CgroupView>>(name)
586                .expect("Failed to dereference command palette")
587                .get_cmd_palette()
588        }
589    }
590}