Refactor diagnostic groups (yet again)
This commit is contained in:
parent
dc49006af8
commit
a9e929585f
2 changed files with 248 additions and 197 deletions
|
|
@ -13,9 +13,8 @@
|
|||
// 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 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>,
|
||||
action_tx: mpsc::UnboundedSender<Action>,
|
||||
components: Vec<Box<dyn Component>>,
|
||||
config: Config,
|
||||
diag_groups: diags::Groups,
|
||||
frame_rate: f64,
|
||||
last_tick_key_events: Vec<KeyEvent>,
|
||||
should_quit: bool,
|
||||
|
|
@ -72,11 +72,11 @@ pub struct App {
|
|||
// App
|
||||
clone: CloneSettings,
|
||||
cur_mode: Mode,
|
||||
diag_groups: Arc<Mutex<Vec<DiagGroup>>>,
|
||||
list: StatefulList<Mode>,
|
||||
boot_modes: Vec<SafeMode>,
|
||||
selections: Vec<Option<usize>>,
|
||||
system32: String,
|
||||
results: Arc<Mutex<HashMap<String, String>>>,
|
||||
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<PathBuf> = [
|
||||
// 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<Rect> {
|
|||
|
||||
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<String> = 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<DVLine> = 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::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,78 +13,169 @@
|
|||
// 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 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<String, Line>,
|
||||
order: Vec<String>,
|
||||
#[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<String>,
|
||||
pub struct Log {
|
||||
pub label: String,
|
||||
pub summary: Vec<DVLine>,
|
||||
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<Log>,
|
||||
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<String> = 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<DVLine> {
|
||||
let mut summaries: Vec<DVLine> = 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::<Vec<_>>()
|
||||
// .join("\n")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue