diff --git a/boot_diags/src/app.rs b/boot_diags/src/app.rs index d767c89..2272e52 100644 --- a/boot_diags/src/app.rs +++ b/boot_diags/src/app.rs @@ -13,9 +13,8 @@ // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . // -use crate::diags; use core::{ - action::{Action, DiagResult}, + action::Action, components::{ Component, footer::Footer, @@ -57,13 +56,14 @@ use ratatui::{ use tokio::sync::mpsc; use tracing::{debug, info}; +use crate::diags::{DiagGroup, Type as DiagType, get_diag_type, parse_chkdsk}; + pub struct App { // TUI action_rx: mpsc::UnboundedReceiver, action_tx: mpsc::UnboundedSender, components: Vec>, config: Config, - diag_groups: diags::Groups, frame_rate: f64, last_tick_key_events: Vec, should_quit: bool, @@ -72,11 +72,11 @@ pub struct App { // App clone: CloneSettings, cur_mode: Mode, + diag_groups: Arc>>, list: StatefulList, boot_modes: Vec, selections: Vec>, system32: String, - results: Arc>>, tasks: Tasks, } @@ -119,7 +119,6 @@ impl App { )), ], config: Config::new()?, - diag_groups: diags::Groups::new(), frame_rate, last_tick_key_events: Vec::new(), should_quit: false, @@ -128,11 +127,11 @@ impl App { // App clone: CloneSettings::new(disk_list_arc), cur_mode: Mode::Home, + diag_groups: Arc::new(Mutex::new(Vec::new())), list, boot_modes: vec![SafeMode::Enable, SafeMode::Disable], system32: String::new(), selections: vec![None, None], - results: results_arc, tasks, }) } @@ -367,8 +366,8 @@ impl App { self.action_tx.send(Action::NextScreen)?; } Mode::BootDiags | Mode::BootSetup => { - let new_mode = self.next_mode(); - self.action_tx.send(Action::SetMode(new_mode))?; + //let new_mode = self.next_mode(); + //self.action_tx.send(Action::SetMode(new_mode))?; } _ => {} }, @@ -419,7 +418,9 @@ impl App { self.action_tx.send(Action::DiagLineStart { text: title.clone(), })?; - self.diag_groups.start(title.to_owned()); + if let Ok(mut diag_groups) = self.diag_groups.lock() { + diag_groups.push(DiagGroup::new(get_diag_type(&title))); + } } } Action::TasksComplete => { @@ -448,90 +449,60 @@ impl App { fn handle_task(&mut self, task: &Task) -> Result<()> { info!("Handling Task: {task:?}"); - match self.cur_mode { - Mode::BootScan => { + if self.cur_mode == Mode::BootScan { + if let Ok(mut diag_groups) = self.diag_groups.lock() { + if let Some(current_group) = diag_groups.last_mut() { + match current_group.diag_type { + DiagType::CheckDisk => { + if let Some(task_result) = &task.result { + // + parse_chkdsk(current_group, task_result.clone()); + } + } + _ => (), + } + } + } + return Ok(()); + } + match task.task_type { + TaskType::CommandNoWait(_, _) | TaskType::CommandWait(_, _) | TaskType::Diskpart(_) => { + // Check result if let Some(result) = &task.result { - let title = self.diag_groups.current_group(); - let passed: bool; - let info: String; match result { TaskResult::Error(msg) => { - passed = false; - info = msg.to_owned(); + self.action_tx + .send(Action::Error(format!("{task:?} Failed: {msg}")))?; } TaskResult::Output(stdout, stderr, success) => { - passed = *success; - if title == "Filesystem" { - info = parse_chkdsk(stdout); - } else { - let div = if !(stdout.is_empty() || stderr.is_empty()) { - "\n\n-----------\n\n" + if !success { + let msg = if !stdout.is_empty() { + stdout.clone() + } else if !stderr.is_empty() { + stderr.clone() } else { - "" + String::from("Unknown Error") }; - info = format!("{stdout}{div}{stderr}"); + self.action_tx + .send(Action::Error(format!("{task:?} Failed: {msg}")))?; } } } - self.diag_groups.update(title, passed, info); - if passed { - self.action_tx.send(Action::DiagLineUpdate { - result: DiagResult::Pass, - text: String::from("Pass?"), - })?; - } else { - self.action_tx.send(Action::DiagLineUpdate { - result: DiagResult::Fail, - text: String::from("Fail?"), - })?; - }; } } - _ => { - match task.task_type { - TaskType::CommandNoWait(_, _) - | TaskType::CommandWait(_, _) - | TaskType::Diskpart(_) => { - // Check result - if let Some(result) = &task.result { - match result { - TaskResult::Error(msg) => { - self.action_tx - .send(Action::Error(format!("{task:?} Failed: {msg}")))?; - } - TaskResult::Output(stdout, stderr, success) => { - if !success { - let msg = if !stdout.is_empty() { - stdout.clone() - } else if !stderr.is_empty() { - stderr.clone() - } else { - String::from("Unknown Error") - }; - self.action_tx.send(Action::Error(format!( - "{task:?} Failed: {msg}" - )))?; - } - } - } - } - } - TaskType::GroupEnd { ref label } => { - self.action_tx.send(Action::DiagLineEnd { - text: label.clone(), - })?; - } - _ => {} - } + TaskType::GroupEnd { ref label } => { + self.action_tx.send(Action::DiagLineEnd { + text: label.clone(), + })?; } + _ => {} } Ok(()) } fn queue_boot_scan_tasks(&mut self) -> Result<()> { - self.diag_groups.reset(); - if let Ok(mut results) = self.results.lock() { - results.clear(); + if let Ok(mut diag_groups) = self.diag_groups.lock() { + diag_groups.clear(); } let disk_list = self.clone.disk_list.lock().unwrap(); if let Some(disk_index) = self.clone.disk_index_dest { @@ -557,7 +528,7 @@ impl App { // BCD if !letter_boot.is_empty() { self.tasks.add_group( - "Boot Files", + DiagType::BootConfigData.to_string().as_str(), vec![TaskType::CommandWait( PathBuf::from(format!("{}\\bcdedit.exe", &self.system32)), vec![ @@ -578,7 +549,7 @@ impl App { // Bitlocker self.tasks.add_group( - "Bitlocker", + DiagType::Bitlocker.to_string().as_str(), vec![ TaskType::CommandWait( PathBuf::from(format!("{}\\manage-bde.exe", &self.system32)), @@ -596,6 +567,24 @@ impl App { ); // Filesystem Health + self.tasks.add_group( + DiagType::CheckDisk.to_string().as_str(), + vec![TaskType::CommandWait( + PathBuf::from(format!("{}\\chkdsk.exe", &self.system32)), + vec![format!("{letter_os}:")], + )], + ); + + // DISM Health + self.tasks.add_group( + DiagType::ComponentStore.to_string().as_str(), + vec![TaskType::CommandWait( + PathBuf::from(format!("{}\\dism.exe", &self.system32)), + vec![format!("{letter_os}:")], + )], + ); + + // Critical Files/Folders let paths: Vec = [ // Files/Folders "Users", @@ -609,28 +598,13 @@ impl App { .map(|s| PathBuf::from(format!("{letter_os}:\\{s}"))) .collect(); self.tasks.add_group( - "Filesystem", - vec![ - TaskType::CommandWait( - PathBuf::from(format!("{}\\chkdsk.exe", &self.system32)), - vec![format!("{letter_os}:")], - ), - TaskType::TestPaths(paths), - ], - ); - - // DISM Health - self.tasks.add_group( - "System Files", - vec![TaskType::CommandWait( - PathBuf::from(format!("{}\\dism.exe", &self.system32)), - vec![format!("{letter_os}:")], - )], + DiagType::SystemFiles.to_string().as_str(), + vec![TaskType::TestPaths(paths)], ); // Registry self.tasks.add_group( - "Registry", + DiagType::Registry.to_string().as_str(), vec![ TaskType::CommandWait( PathBuf::from(format!("{}\\reg.exe", &self.system32)), @@ -762,13 +736,16 @@ fn get_chunks(r: Rect) -> Vec { fn build_footer_string(cur_mode: Mode) -> String { match cur_mode { + Mode::BootDiags => { + String::from("(Enter) to select / (m) for menu / (s) to start over / (q) to quit") + } Mode::BootScan | Mode::BootSetup | Mode::Home | Mode::ScanDisks => { String::from("(q) to quit") } Mode::InstallDrivers | Mode::InjectDrivers | Mode::SetBootMode => { String::from("(Enter) to select / (q) to quit") } - Mode::BootDiags | Mode::DiagMenu | Mode::SelectParts => { + Mode::DiagMenu | Mode::SelectParts => { String::from("(Enter) to select / (s) to start over / (q) to quit") } Mode::Done => String::from("(Enter) to continue / (q) to quit"), @@ -852,14 +829,21 @@ fn build_left_items(app: &App) -> Action { select_type = SelectionType::Loop; let (new_title, _) = get_mode_strings(app.cur_mode); title = new_title; - app.diag_groups.get().iter().for_each(|group| { - info!("BootDiags Group: {:?}", group); - items.push(if group.passed { - group.title.clone() - } else { - format!("{} - Issues detected!", group.title) - }); - }); + if let Ok(diag_groups) = app.diag_groups.lock() { + let labels: Vec = diag_groups + .iter() + .map(|group| { + let label = group.diag_type.to_string(); + let status = if group.passed { + "" // Leave blank if OK + } else { + " -- Issue(s) detected" + }; + format!("{label}{status}") + }) + .collect(); + items.extend(labels.into_iter()); + } } Mode::BootSetup => { select_type = SelectionType::Loop; @@ -970,18 +954,13 @@ fn build_right_items(app: &App) -> Action { }); } Mode::BootDiags => { - app.diag_groups.get().iter().for_each(|group| { - let mut lines = Vec::new(); - group.info.iter().for_each(|text| { - text.lines().for_each(|line| { - lines.push(DVLine { - line_parts: vec![String::from(line)], - line_colors: vec![Color::Reset], - }); - }); - }); - items.push(lines); - }); + if let Ok(diag_groups) = app.diag_groups.lock() { + let mut summary: Vec = Vec::new(); + diag_groups + .iter() + .for_each(|group| summary.extend(group.get_logs_summary().into_iter())); + items.push(summary); + } } Mode::InjectDrivers | Mode::InstallDrivers => { items.push(vec![DVLine { @@ -1070,22 +1049,3 @@ fn get_mode_strings(mode: Mode) -> (String, String) { _ => panic!("This shouldn't happen"), } } - -fn parse_chkdsk(output: &str) -> String { - // Split lines - let lines: Vec<_> = output.split("\r\n").collect(); - - // Omit progress lines and unhelpful messages - lines - .into_iter() - .filter(|line| { - !(line.contains("\r") - || line.contains("Class not registered") - || line.contains("/F parameter") - || line.contains("Running CHKDSK") - || line.contains("Total duration:") - || line.contains("Failed to transfer logged messages")) - }) - .collect::>() - .join("\n") -} diff --git a/boot_diags/src/diags.rs b/boot_diags/src/diags.rs index 32d6fb7..aba4e92 100644 --- a/boot_diags/src/diags.rs +++ b/boot_diags/src/diags.rs @@ -13,78 +13,169 @@ // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . -use std::collections::HashMap; +use core::{line::DVLine, tasks::TaskResult}; +use std::{fmt, thread::sleep, time::Duration}; -use tracing::warn; +use ratatui::style::Color; +use tracing::{info, warn}; -#[derive(Debug)] -pub struct Groups { - items: HashMap, - order: Vec, +#[derive(Clone, Copy, Debug)] +pub enum Type { + Bitlocker, + BootConfigData, + CheckDisk, + ComponentStore, + Registry, + SystemFiles, + Unknown, } -impl Groups { - pub fn new() -> Self { - Groups { - items: HashMap::new(), - order: Vec::new(), - } - } - - pub fn current_group(&self) -> String { - match self.order.last() { - Some(label) => label.clone(), - None => String::from("No current group"), - } - } - - pub fn get(&self) -> Vec<&Line> { - let mut lines = Vec::new(); - self.order.iter().for_each(|key| { - if let Some(line) = self.items.get(key) { - lines.push(line); - } - }); - lines - } - - pub fn reset(&mut self) { - self.items.clear(); - self.order.clear(); - } - - pub fn start(&mut self, title: String) { - self.order.push(title.clone()); - self.items.insert( - title.clone(), - Line { - title, - passed: true, - info: Vec::new(), - }, - ); - } - - pub fn update(&mut self, title: String, passed: bool, info: String) { - if let Some(line) = self.items.get_mut(&title) { - line.update(passed, info); - } else { - warn!("WARNING/DELETEME - This shouldn't happen?!"); - self.start(title); +impl fmt::Display for Type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Type::Bitlocker => write!(f, "Bitlocker"), + Type::BootConfigData => write!(f, "Boot Files"), + Type::CheckDisk => write!(f, "CHKDSK"), + Type::ComponentStore => write!(f, "DISM ScanHealth"), + Type::Registry => write!(f, "Registry"), + Type::SystemFiles => write!(f, "System Files"), + Type::Unknown => write!(f, "Unknown Type"), } } } #[derive(Clone, Debug)] -pub struct Line { - pub title: String, - pub passed: bool, - pub info: Vec, +pub struct Log { + pub label: String, + pub summary: Vec, + pub raw: String, } -impl Line { - pub fn update(&mut self, passed: bool, info: String) { - self.passed &= passed; // We fail if any tests in this group fail - self.info.push(info); +#[derive(Clone, Debug)] +pub struct DiagGroup { + pub complete: bool, + pub diag_type: Type, + pub passed: bool, + pub logs: Vec, + pub result: String, +} + +impl DiagGroup { + pub fn new(diag_type: Type) -> Self { + DiagGroup { + complete: false, + diag_type, + passed: true, + logs: Vec::new(), + result: String::new(), + } + } + + pub fn get_logs_raw(&self) -> String { + let raw_logs: Vec = self + .logs + .iter() + .map(|log| format!("-- {} --\n{}", &log.label, &log.raw)) + .collect(); + raw_logs.join("\n\n\n") + } + + pub fn get_logs_summary(&self) -> Vec { + let mut summaries: Vec = Vec::new(); + self.logs + .iter() + .for_each(|log| summaries.extend(log.summary.clone().into_iter())); + summaries } } + +pub fn get_diag_type(label: &str) -> Type { + info!("Getting Diag type for {label}"); + match label { + "Bitlocker" => Type::Bitlocker, + "Boot Files" => Type::BootConfigData, + "DISM ScanHealth" => Type::ComponentStore, + "CHKDSK" => Type::CheckDisk, + "Registry" => Type::Registry, + "System Files" => Type::SystemFiles, + _ => { + warn!("Failed to determine type"); + Type::Unknown + } + } +} + +pub fn parse_chkdsk(diag_group: &mut DiagGroup, task_result: TaskResult) { + if !cfg!(windows) { + sleep(Duration::from_millis(500)); + return (); + } + match task_result { + TaskResult::Error(err) => { + diag_group.passed = false; + diag_group.result = String::from("Error"); + diag_group.logs.push(Log { + label: String::from("CHKDSK"), + summary: vec![DVLine { + line_parts: vec![String::from("CHKDSK: "), String::from("Error")], + line_colors: vec![Color::Reset, Color::Red], + }], + raw: err, + }); + } + TaskResult::Output(stdout, stderr, _) => { + if stdout.contains("Windows has scanned the file system and found no problems.") { + diag_group.passed &= true; + diag_group.result = String::from("OK"); + diag_group.logs.push(Log { + label: String::from("CHKDSK"), + summary: vec![DVLine { + line_parts: vec![String::from("CHKDSK: "), String::from("OK")], + line_colors: vec![Color::Reset, Color::Green], + }], + raw: format!("{stdout}\n\n-------\n\n{stderr}"), + }); + } else { + diag_group.passed = false; + diag_group.result = String::from("Error"); + diag_group.logs.push(Log { + label: String::from("CHKDSK"), + summary: vec![DVLine { + line_parts: vec![String::from("CHKDSK: "), String::from("Error")], + line_colors: vec![Color::Reset, Color::Red], + }], + raw: format!("{stdout}\n\n-------\n\n{stderr}"), + }); + } + } + } + + // let mut summary = Vec::new(); + // let raw = if stderr.is_empty() { + // stdout.to_string() + // } else { + // format!("{stdout}\n\n --stderr-- \n\n{stderr}") + // }; + // // TODO: Implement actual logic for result + // Log { + // label: String::from("CHKDSK"), + // raw, + // summary, + // } + // // Split lines + // let lines: Vec<_> = output.split("\r\n").collect(); + // + // // Omit progress lines and unhelpful messages + // lines + // .into_iter() + // .filter(|line| { + // !(line.contains("\r") + // || line.contains("Class not registered") + // || line.contains("/F parameter") + // || line.contains("Running CHKDSK") + // || line.contains("Total duration:") + // || line.contains("Failed to transfer logged messages")) + // }) + // .collect::>() + // .join("\n") +}