Add boot diagnostic sections

Still very much WIP
This commit is contained in:
2Shirt 2025-05-11 20:20:08 -07:00
parent 8f8f78a161
commit 7b0deb4cc7
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C
8 changed files with 578 additions and 208 deletions

View file

@ -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<Option<usize>>,
system32: String,
tasks: Tasks,
task_groups: VecDeque<Option<String>>,
}
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<String> = 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<String>;
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<PathBuf> = [
"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<Rect> {
// 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<DVLine>> = 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

View file

@ -0,0 +1,2 @@
pub mod progress;
pub mod results;

View file

@ -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 <https://www.gnu.org/licenses/>.
//
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<UnboundedSender<Action>>,
config: Config,
lines: Vec<ProgressLine>,
mode: Mode,
}
impl Progress {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
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!("{} {:.<width$}", &line.name, "")),
Span::styled(text, Style::default().fg(color)),
];
body_text.push(Line::from(spans).centered());
body_text.push(Line::default());
body_text.push(Line::default());
});
// Build block
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().bold());
let body = Paragraph::new(body_text).block(block);
frame.render_widget(Clear, area);
frame.render_widget(body, area);
Ok(())
}
}

View file

@ -0,0 +1,53 @@
// 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 <https://www.gnu.org/licenses/>.
//
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<UnboundedSender<Action>>,
config: Config,
}
impl Results {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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<Option<Action>> {
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
Ok(())
}
}

View file

@ -13,17 +13,30 @@
// You should have received a copy of the GNU General Public License
// along with Deja-Vu. If not, see <https://www.gnu.org/licenses/>.
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<String, Line>,
@ -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<bool>, info: Option<String>) {
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<bool>, info: Option<String>) {
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

View file

@ -19,6 +19,7 @@ use color_eyre::Result;
use crate::app::App;
mod app;
mod components;
mod diags;
#[tokio::main]

View file

@ -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<usize>, Option<usize>), // indicies for (source, dest) etc
SelectRight(Option<usize>, Option<usize>), // indicies for right info pane
TaskStart(TaskType),
TasksComplete,
UpdateDiskList(Vec<Disk>),
UpdateFooter(String),

View file

@ -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<String>), // (command, args)
CommandWait(PathBuf, Vec<String>), // (command, args)
Diskpart(String), // (script_as_string)
ScanDisks,
Sleep,
TestPaths(Vec<PathBuf>),
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<TaskResult>) -
thread::spawn(|| sleep(Duration::from_millis(250)))
}
}
fn test_paths(
path_list: Vec<PathBuf>,
task_tx: mpsc::UnboundedSender<TaskResult>,
) -> 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));
})
}