diff --git a/boot_diags/src/app.rs b/boot_diags/src/app.rs index ee754c8..61ed244 100644 --- a/boot_diags/src/app.rs +++ b/boot_diags/src/app.rs @@ -15,7 +15,7 @@ // use crate::diags; use core::{ - action::Action, + action::{Action, DiagResult}, components::{ Component, footer::Footer, @@ -39,6 +39,7 @@ use core::{ tui::{Event, Tui}, }; use std::{ + collections::VecDeque, env, iter::zip, path::PathBuf, @@ -75,6 +76,7 @@ pub struct App { selections: Vec>, system32: String, tasks: Tasks, + task_groups: VecDeque>, } impl App { @@ -100,6 +102,8 @@ impl App { Box::new(Right::new()), Box::new(Footer::new()), Box::new(popup::Popup::new()), + Box::new(crate::components::progress::Progress::new()), + Box::new(crate::components::results::Results::new()), ], config: Config::new()?, diag_groups: diags::Groups::new(), @@ -116,6 +120,7 @@ impl App { system32: String::new(), selections: vec![None, None], tasks, + task_groups: VecDeque::new(), }) } @@ -124,10 +129,10 @@ impl App { if let Some(disk_index) = self.clone.disk_index_dest { let disk_list = self.clone.disk_list.lock().unwrap(); if let Some(disk) = disk_list.get(disk_index) { - if let Some(boot_index) = self.clone.part_index_boot { + if let Some(os_index) = self.clone.part_index_os { if let Ok(task) = boot::inject_driver( driver, - disk.get_part_letter(boot_index).as_str(), + disk.get_part_letter(os_index).as_str(), &self.system32, ) { self.tasks.add(task); @@ -192,122 +197,7 @@ impl App { popup::Type::Info, String::from("Gathering info..."), ))?; - - // Get System32 path - let system32 = if cfg!(windows) { - if let Ok(path) = env::var("SYSTEMROOT") { - format!("{path}\\System32") - } else { - self.action_tx.send(Action::Error(String::from( - "ERROR\n\n\nFailed to find SYSTEMROOT", - )))?; - return Ok(()); - } - } else { - String::from(".") - }; - - // Add tasks - let disk_list = self.clone.disk_list.lock().unwrap(); - if let Some(disk_index) = self.clone.disk_index_dest { - if let Some(disk) = disk_list.get(disk_index) { - let table_type = disk.part_type.clone(); - let letter_boot = disk.get_part_letter(self.clone.part_index_boot.unwrap()); - let letter_os = disk.get_part_letter(self.clone.part_index_os.unwrap()); - - // Safety check - if letter_boot.is_empty() || letter_os.is_empty() { - self.action_tx.send(Action::Error(String::from( - "ERROR\n\n\nFailed to get drive letters for the destination", - )))?; - return Ok(()); - } - - // BCD - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\bcdedit.exe")), - vec![ - String::from("/store"), - format!( - "{letter_boot}:{}\\Boot\\BCD", - if table_type == PartitionTableType::Guid { - "\\EFI\\Microsoft" - } else { - "" - } - ), - String::from("/enum"), - ], - )); - - // Bitlocker - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\manage-bde.exe")), - vec![String::from("-status"), format!("{letter_os}:")], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\manage-bde.exe")), - vec![ - String::from("-protectors"), - String::from("-get"), - format!("{letter_os}:"), - ], - )); - - // DISM Health - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\dism.exe")), - vec![format!("{letter_os}:")], - )); - - // Filesystem Health - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\chkdsk.exe")), - vec![format!("{letter_os}:")], - )); - - // Registry - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![ - String::from("load"), - String::from("HKLM\\TmpSoftware"), - format!("{letter_os}:\\Windows\\System32\\config\\SOFTWARE"), - ], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![ - String::from("load"), - String::from("HKLM\\TmpSystem"), - format!("{letter_os}:\\Windows\\System32\\config\\SYSTEM"), - ], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![ - String::from("load"), - String::from("HKU\\TmpDefault"), - format!("{letter_os}:\\Windows\\System32\\config\\DEFAULT"), - ], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![String::from("unload"), String::from("HKLM\\TmpSoftware")], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![String::from("unload"), String::from("HKLM\\TmpSystem")], - )); - self.tasks.add(TaskType::CommandWait( - PathBuf::from(format!("{system32}\\reg.exe")), - vec![String::from("unload"), String::from("HKU\\TmpDefault")], - )); - - // Files/Folders - // TODO: Check for critical folders (e.g. /Windows, /Windows/System32, etc) - } - } + self.queue_boot_scan_tasks()?; } Mode::InjectDrivers | Mode::InstallDrivers => self.clone.scan_drivers(), Mode::ScanDisks => { @@ -510,6 +400,34 @@ impl App { self.action_tx.send(build_right_items(self))?; self.action_tx.send(Action::Select(None, None))?; } + Action::TaskStart(ref task_type) => { + if self.cur_mode == Mode::BootScan { + let title: Option = match task_type { + TaskType::CommandWait(cmd, _args) => { + let cmd_str = cmd.to_string_lossy().into_owned(); + let diag_type = diags::get_type(&cmd_str); + Some(format!("{diag_type}")) + } + TaskType::TestPaths(_) => Some(String::from("Critical Paths")), + _ => None, + }; + if let Some(title) = title { + info!("{:?}", self.task_groups.front()); + if self.task_groups.front().is_some() { + if self.task_groups.front().unwrap().is_some() { + // None here means that we're in the middle of a group of tasks + // i.e. don't start a new diag line + self.action_tx.send(Action::DiagStartLine { + text: title.clone(), + })?; + } + } + if !self.diag_groups.contains(&title) { + self.diag_groups.update(title, None, None); + } + } + } + } Action::TasksComplete => { if self.cur_mode == Mode::BootDiags { self.action_tx.send(Action::DismissPopup)?; @@ -536,71 +454,109 @@ impl App { fn handle_task(&mut self, task: &Task) -> Result<()> { info!("Handling Task: {task:?}"); + let title: Option; match self.cur_mode { Mode::BootScan => { - if let TaskType::CommandWait(cmd_path, _cmd_args) = &task.task_type { - let mut cmd_name = ""; - if let Some(path) = cmd_path.file_name() { - if let Some(cmd_str) = path.to_str() { - cmd_name = cmd_str; - } - }; - let diag_title = match diags::get_type(cmd_name) { - diags::Type::BootConfigData => Some("Boot Files"), - diags::Type::Bitlocker => Some("Bitlocker"), - diags::Type::FileSystem => { - if let Some(result) = &task.result { - let passed: bool; - let info: String; - match result { - TaskResult::Error(msg) => { - passed = false; - info = msg.to_owned(); - } - TaskResult::Output(stdout, _stderr, success) => { - passed = *success; - info = parse_chkdsk(stdout); - } - } - self.diag_groups.update( - String::from("Filesystem"), - passed, - info.to_owned(), - ); + let task_group = self.task_groups.pop_front(); + match &task.task_type { + TaskType::CommandWait(cmd_path, _cmd_args) => { + let mut cmd_name = ""; + if let Some(path) = cmd_path.file_name() { + if let Some(cmd_str) = path.to_str() { + cmd_name = cmd_str; } - None // Don't set title since we handle the logic here - } - diags::Type::Registry => Some("Registry"), - diags::Type::FilesAndFolders => Some("Critical Files"), - _ => { - warn!("Unrecognized command: {:?}", &cmd_path); - None - } - }; - if let Some(title) = diag_title { - // Just use command output - if let Some(result) = &task.result { - let passed: bool; - let info: String; - match result { - TaskResult::Error(msg) => { - passed = false; - info = msg.to_owned(); - } - TaskResult::Output(stdout, stderr, success) => { - passed = *success; - let div = if !(stdout.is_empty() || stderr.is_empty()) { - "\n\n-----------\n\n" - } else { - "" - }; - info = format!("{stdout}{div}{stderr}"); + }; + let diag_type = diags::get_type(cmd_name); + match diag_type { + diags::Type::Bitlocker + | diags::Type::BootConfigData + | diags::Type::Registry + | diags::Type::System => { + title = Some(format!("{diag_type}")); + } + diags::Type::FileSystem => { + title = None; + if let Some(result) = &task.result { + let passed: bool; + let info: String; + match result { + TaskResult::Error(msg) => { + passed = false; + info = msg.to_owned(); + } + TaskResult::Output(stdout, _stderr, success) => { + passed = *success; + info = parse_chkdsk(stdout); + } + } + self.diag_groups.update( + String::from("Filesystem"), + Some(passed), + Some(info.to_owned()), + ); } } - self.diag_groups - .update(title.to_string(), passed, info.to_owned()); - } + diags::Type::Unknown => { + title = None; + warn!("Unrecognized command: {:?}", &cmd_path); + } + }; } + TaskType::TestPaths(_) => { + title = Some(format!("{}", diags::Type::FileSystem)); + } + _ => title = None, + } + if let Some(title_str) = title { + if let Some(result) = &task.result { + let passed: bool; + let info: String; + match result { + TaskResult::Error(msg) => { + passed = false; + info = msg.to_owned(); + } + TaskResult::Output(stdout, stderr, success) => { + passed = *success; + let div = if !(stdout.is_empty() || stderr.is_empty()) { + "\n\n-----------\n\n" + } else { + "" + }; + info = format!("{stdout}{div}{stderr}"); + } + } + self.diag_groups + .update(title_str, Some(passed), Some(info.to_owned())); + if let Some(group) = task_group { + if let Some(wat) = group { + info!("WAT? // {wat:?}"); + if passed { + self.action_tx.send(Action::DiagEndLine { + result: DiagResult::Pass, + text: String::from("Maybe?"), + })?; + } else { + self.action_tx.send(Action::DiagEndLine { + result: DiagResult::Fail, + text: String::from("Nope?"), + })?; + }; + } + } + } else { + // If title was set but there wasn't a result + self.action_tx.send(Action::DiagEndLine { + result: DiagResult::Warn, + text: String::from("Yellow result?"), + })?; + } + } else { + // title was not set + self.action_tx.send(Action::DiagEndLine { + result: DiagResult::Warn, + text: String::from("Yellow title?"), + })?; } } _ => { @@ -639,13 +595,156 @@ impl App { Ok(()) } + fn queue_boot_scan_tasks(&mut self) -> Result<()> { + let disk_list = self.clone.disk_list.lock().unwrap(); + if let Some(disk_index) = self.clone.disk_index_dest { + if let Some(disk) = disk_list.get(disk_index) { + let table_type = disk.part_type.clone(); + let letter_boot = disk.get_part_letter(self.clone.part_index_boot.unwrap()); + let letter_os = disk.get_part_letter(self.clone.part_index_os.unwrap()); + + // Safety check + if letter_os.is_empty() { + if letter_boot.is_empty() { + self.action_tx.send(Action::Error(String::from( + "ERROR\n\n\nFailed to get drive letters for the boot and OS volumes", + )))?; + } else { + self.action_tx.send(Action::Error(String::from( + "ERROR\n\n\nFailed to get drive letter for the OS volume", + )))?; + } + return Ok(()); + } + + // BCD + if !letter_boot.is_empty() { + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\bcdedit.exe", &self.system32)), + vec![ + String::from("/store"), + format!( + "{letter_boot}:{}\\Boot\\BCD", + if table_type == PartitionTableType::Guid { + "\\EFI\\Microsoft" + } else { + "" + } + ), + String::from("/enum"), + ], + )); + self.task_groups + .push_back(Some(format!("{}", diags::Type::BootConfigData))); + } + + // Bitlocker + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\manage-bde.exe", &self.system32)), + vec![String::from("-status"), format!("{letter_os}:")], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\manage-bde.exe", &self.system32)), + vec![ + String::from("-protectors"), + String::from("-get"), + format!("{letter_os}:"), + ], + )); + self.task_groups + .push_back(Some(format!("{}", diags::Type::Bitlocker))); + + // Filesystem Health + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\chkdsk.exe", &self.system32)), + vec![format!("{letter_os}:")], + )); + self.task_groups.push_back(None); + + // Files/Folders + let paths: Vec = [ + "Users", + "Program Files", + "Program Files (x86)", + "ProgramData", + "Windows\\System32\\config", + ] + .iter() + .map(|s| PathBuf::from(format!("{letter_os}:\\{s}"))) + .collect(); + self.tasks.add(TaskType::TestPaths(paths)); + self.task_groups + .push_back(Some(format!("{}", diags::Type::FileSystem))); + + // DISM Health + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\dism.exe", &self.system32)), + vec![format!("{letter_os}:")], + )); + self.task_groups + .push_back(Some(format!("{}", diags::Type::System))); + + // Registry + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![ + String::from("load"), + String::from("HKLM\\TmpSoftware"), + format!("{letter_os}:\\Windows\\System32\\config\\SOFTWARE"), + ], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![ + String::from("load"), + String::from("HKLM\\TmpSystem"), + format!("{letter_os}:\\Windows\\System32\\config\\SYSTEM"), + ], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![ + String::from("load"), + String::from("HKU\\TmpDefault"), + format!("{letter_os}:\\Windows\\System32\\config\\DEFAULT"), + ], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![String::from("unload"), String::from("HKLM\\TmpSoftware")], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![String::from("unload"), String::from("HKLM\\TmpSystem")], + )); + self.task_groups.push_back(None); + self.tasks.add(TaskType::CommandWait( + PathBuf::from(format!("{}\\reg.exe", &self.system32)), + vec![String::from("unload"), String::from("HKU\\TmpDefault")], + )); + self.task_groups + .push_back(Some(format!("{}", diags::Type::Registry))); + self.tasks.add(TaskType::Sleep); // NOTE: DELETEME + } + } + Ok(()) + } + fn render(&mut self, tui: &mut Tui) -> Result<()> { tui.draw(|frame| { - if let [header, _body, footer, left, right, popup] = get_chunks(frame.area())[..] { + if let [header, _body, footer, left, right, popup, progress] = + get_chunks(frame.area())[..] + { let component_areas = vec![ header, // Title Bar header, // FPS Counter - left, right, footer, popup, + left, right, footer, popup, // core + progress, header, // boot-diags ]; for (component, area) in zip(self.components.iter_mut(), component_areas) { if let Err(err) = component.draw(frame, area) { @@ -710,6 +809,9 @@ fn get_chunks(r: Rect) -> Vec { // Popup chunks.push(centered_rect(60, 25, r)); + // Progress + chunks.push(centered_rect(60, 80, chunks[1])); + // Done chunks } @@ -849,12 +951,7 @@ fn build_right_items(app: &App) -> Action { let mut labels: Vec> = Vec::new(); let mut start_index = 0; // TODO: DELETE THIS SECTION - start_index = 1; items.push(vec![ - DVLine { - line_parts: vec![format!("Mode: {:?}", app.cur_mode)], - line_colors: vec![Color::Reset], - }, DVLine { line_parts: vec![format!( "Parts: {:?} // {:?}", @@ -875,6 +972,7 @@ fn build_right_items(app: &App) -> Action { line_colors: vec![Color::Reset], }, ]); + start_index += 1; // TODO: DELETE THIS SECTION match app.cur_mode { Mode::DiagMenu => { @@ -904,10 +1002,8 @@ fn build_right_items(app: &App) -> Action { // Add header if !header_lines.is_empty() { - items[0].append(&mut header_lines); - // TODO: Replace line above with lines below - // items.push(header_lines); - // start_index = 1; + items.push(header_lines); + start_index += 1; } } } @@ -952,7 +1048,7 @@ fn build_right_items(app: &App) -> Action { line_parts: vec![get_cpu_name()], line_colors: vec![Color::Reset], }]); - start_index = 2; + start_index += 2; } Mode::SelectDisks => { let dest_dv_line = DVLine { @@ -973,7 +1069,7 @@ fn build_right_items(app: &App) -> Action { }]) }); if let Some(index) = app.clone.disk_index_dest { - start_index = 1; + start_index += 1; let disk_list = app.clone.disk_list.lock().unwrap(); if let Some(disk) = disk_list.get(index) { // Disk Details diff --git a/boot_diags/src/components.rs b/boot_diags/src/components.rs new file mode 100644 index 0000000..ef613dc --- /dev/null +++ b/boot_diags/src/components.rs @@ -0,0 +1,2 @@ +pub mod progress; +pub mod results; diff --git a/boot_diags/src/components/progress.rs b/boot_diags/src/components/progress.rs new file mode 100644 index 0000000..224fcbe --- /dev/null +++ b/boot_diags/src/components/progress.rs @@ -0,0 +1,142 @@ +// This file is part of Deja-Vu. +// +// Deja-Vu is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Deja-Vu is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Deja-Vu. If not, see . +// +use color_eyre::Result; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, Paragraph}, +}; +use tokio::sync::mpsc::UnboundedSender; +use tracing::info; + +use core::{ + action::{Action, DiagResult}, + components::Component, + config::Config, + state::Mode, +}; + +#[derive(Debug, Clone)] +struct ProgressLine { + name: String, + text: String, + result: DiagResult, +} + +impl ProgressLine { + pub fn len_name(&self) -> usize { + self.name.chars().count() + } + + pub fn len_text(&self) -> usize { + self.text.chars().count() + } +} + +#[derive(Default, Debug, Clone)] +pub struct Progress { + command_tx: Option>, + config: Config, + lines: Vec, + mode: Mode, +} + +impl Progress { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } +} + +impl Component for Progress { + fn update(&mut self, action: Action) -> Result> { + match action { + Action::DiagStartLine { text } => { + info!("Caught Action::DiagStartLine"); + self.lines.push(ProgressLine { + name: text, + text: String::new(), + result: DiagResult::Pass, + }); + } + Action::DiagEndLine { result, text } => { + info!("Caught Action::DiagEndLine"); + if let Some(line) = self.lines.last_mut() { + line.result = result; + line.text = text; + } + } + Action::SetMode(mode) => self.mode = mode, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.mode != Mode::BootScan { + return Ok(()); + } + + let mut body_text = Vec::with_capacity(20); // TODO: Needs fine tuning? + body_text.push(Line::default()); + + // Add line(s) + self.lines.iter().for_each(|line| { + //.for_each(|(part, color)| spans.push(Span::styled(part, Style::default().fg(color)))); + //Line::from(spans) + let color = match line.result { + DiagResult::Pass => Color::Green, + DiagResult::Fail => Color::Red, + DiagResult::Warn => Color::Yellow, + }; + let text = if line.text.is_empty() { + String::from("...") + } else { + line.text.clone() + }; + let text = format!(" [ {text} ] "); + let width = area.width as usize - line.len_name() - 1 - text.chars().count() - 7; // ' [ ]' + let spans = vec![ + Span::raw(format!("{} {:.. +// +use color_eyre::Result; +use ratatui::{Frame, layout::Rect}; +use tokio::sync::mpsc::UnboundedSender; + +use core::{action::Action, components::Component, config::Config}; + +#[derive(Default, Debug, Clone)] +pub struct Results { + command_tx: Option>, + config: Config, +} + +impl Results { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.command_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + self.config = config; + Ok(()) + } +} + +impl Component for Results { + fn update(&mut self, action: Action) -> Result> { + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + Ok(()) + } +} diff --git a/boot_diags/src/diags.rs b/boot_diags/src/diags.rs index 05c5dbb..6620261 100644 --- a/boot_diags/src/diags.rs +++ b/boot_diags/src/diags.rs @@ -13,17 +13,30 @@ // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; pub enum Type { Bitlocker, BootConfigData, FileSystem, - FilesAndFolders, Registry, + System, Unknown, } +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::FileSystem => write!(f, "Filesystem"), + Type::Registry => write!(f, "Registry"), + Type::System => write!(f, "System Files"), + Type::Unknown => write!(f, "Unknown Type"), + } + } +} + #[derive(Debug)] pub struct Groups { items: HashMap, @@ -38,6 +51,10 @@ impl Groups { } } + pub fn contains(&self, name: &str) -> bool { + self.items.contains_key(name) + } + pub fn get(&self) -> Vec<&Line> { let mut lines = Vec::new(); self.order.iter().for_each(|key| { @@ -48,17 +65,22 @@ impl Groups { lines } - pub fn update(&mut self, title: String, passed: bool, info: String) { + pub fn update(&mut self, title: String, passed: Option, info: Option) { if let Some(line) = self.items.get_mut(&title) { line.update(passed, info); } else { + let info_list = if info.is_some() { + vec![info.unwrap()] + } else { + Vec::new() + }; self.order.push(title.clone()); self.items.insert( title.clone(), Line { title, - passed, - info: vec![info], + passed: passed.unwrap_or(false), + info: info_list, }, ); } @@ -73,29 +95,33 @@ pub struct Line { } 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); + pub fn update(&mut self, passed: Option, info: Option) { + if let Some(result) = passed { + self.passed &= result; // We fail if any tests in this group fail + } + if let Some(info_str) = info { + self.info.push(String::from(info_str)); + } } } pub fn get_type(cmd_name: &str) -> Type { - if cmd_name == "exa" { + if cmd_name.ends_with("exa") { return Type::BootConfigData; } - if cmd_name == "bcdedit.exe" { + if cmd_name.ends_with("bcdedit.exe") { return Type::BootConfigData; } - if cmd_name == "dir" { - return Type::FilesAndFolders; + if cmd_name.ends_with("dism.exe") { + return Type::System; } - if cmd_name == "reg.exe" { + if cmd_name.ends_with("reg.exe") { return Type::Registry; } - if cmd_name == "chkdsk.exe" { + if cmd_name.ends_with("chkdsk.exe") { return Type::FileSystem; } - if cmd_name == "manage-bde.exe" { + if cmd_name.ends_with("manage-bde.exe") { return Type::Bitlocker; } Type::Unknown diff --git a/boot_diags/src/main.rs b/boot_diags/src/main.rs index fe8b922..ac77131 100644 --- a/boot_diags/src/main.rs +++ b/boot_diags/src/main.rs @@ -19,6 +19,7 @@ use color_eyre::Result; use crate::app::App; mod app; +mod components; mod diags; #[tokio::main] diff --git a/core/src/action.rs b/core/src/action.rs index 5580390..5fd0549 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -21,12 +21,22 @@ use crate::{ line::DVLine, state::Mode, system::disk::Disk, + tasks::TaskType, }; +#[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] +pub enum DiagResult { + Pass, + Fail, + Warn, +} + #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum Action { // App (Boot-Diags) BootScan, + DiagStartLine { text: String }, + DiagEndLine { result: DiagResult, text: String }, // App (Clone) Highlight(usize), InstallDriver, @@ -34,6 +44,7 @@ pub enum Action { ScanDisks, Select(Option, Option), // indicies for (source, dest) etc SelectRight(Option, Option), // indicies for right info pane + TaskStart(TaskType), TasksComplete, UpdateDiskList(Vec), UpdateFooter(String), diff --git a/core/src/tasks.rs b/core/src/tasks.rs index ca9ba66..5e2015e 100644 --- a/core/src/tasks.rs +++ b/core/src/tasks.rs @@ -26,6 +26,7 @@ use std::{ }; use color_eyre::Result; +use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::info; @@ -41,13 +42,14 @@ pub enum TaskResult { Output(String, String, bool), // stdout, stderr, success } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum TaskType { CommandNoWait(PathBuf, Vec), // (command, args) CommandWait(PathBuf, Vec), // (command, args) Diskpart(String), // (script_as_string) ScanDisks, Sleep, + TestPaths(Vec), UpdateDestDisk(usize), // (disk_index) UpdateDiskList, } @@ -67,6 +69,7 @@ impl fmt::Display for TaskType { } TaskType::ScanDisks => write!(f, "ScanDisks"), TaskType::Sleep => write!(f, "Sleep"), + TaskType::TestPaths(_) => write!(f, "TestPaths"), TaskType::UpdateDestDisk(_) => write!(f, "UpdateDestDisk"), TaskType::UpdateDiskList => write!(f, "UpdateDiskList"), } @@ -170,6 +173,8 @@ impl Tasks { self.cur_task = self.task_list.pop_front(); if let Some(task) = self.cur_task.take() { let task_tx = self.task_tx.clone(); + self.action_tx + .send(Action::TaskStart(task.task_type.clone()))?; match task.task_type { TaskType::CommandNoWait(ref cmd_path, ref cmd_args) => { self.cur_handle = None; @@ -199,6 +204,9 @@ impl Tasks { TaskType::Sleep => { self.cur_handle = Some(thread::spawn(|| sleep(Duration::from_millis(250)))); } + TaskType::TestPaths(ref list) => { + self.cur_handle = Some(test_paths(list.clone(), task_tx.clone())); + } TaskType::UpdateDestDisk(index) => { self.action_tx.send(Action::DisplayPopup( popup::Type::Info, @@ -272,7 +280,7 @@ fn run_task_command( }) } else { // Simulate task if not running under Windows - thread::spawn(|| sleep(Duration::from_millis(250))) + thread::spawn(|| sleep(Duration::from_millis(500))) } } @@ -294,3 +302,34 @@ fn run_task_diskpart(script: &str, task_tx: mpsc::UnboundedSender) - thread::spawn(|| sleep(Duration::from_millis(250))) } } + +fn test_paths( + path_list: Vec, + task_tx: mpsc::UnboundedSender, +) -> JoinHandle<()> { + thread::spawn(move || { + let mut missing_paths = Vec::new(); + let task_result: TaskResult; + path_list.iter().for_each(|path| { + if !path.exists() { + missing_paths.push(String::from(path.to_string_lossy())); + } + }); + + if missing_paths.is_empty() { + // No missing paths + task_result = TaskResult::Output(String::from("OK"), String::new(), true); + } else { + task_result = TaskResult::Output( + String::from("Missing item(s)"), + missing_paths.join(",\n"), + false, + ); + }; + + let err_str = format!("Failed to send TaskResult: {:?}", &task_result); + task_tx + .send(task_result) + .unwrap_or_else(|_| panic!("{}", err_str)); + }) +}