From 03eeff0a8424f0812b1dce2567ae7d1bfdf72a41 Mon Sep 17 00:00:00 2001 From: dark0dave Date: Sat, 18 Nov 2023 22:37:49 +0000 Subject: [PATCH] refactor(cleanup): Seperate files out a bit better introduce some constants for performance Signed-off-by: dark0dave --- README.md | 29 ++++--- src/args.rs | 5 +- src/main.rs | 10 ++- src/state.rs | 8 ++ src/utils.rs | 7 ++ src/weidu.rs | 187 +++++--------------------------------------- src/weidu_parser.rs | 137 ++++++++++++++++++++++++++++++++ 7 files changed, 201 insertions(+), 182 deletions(-) create mode 100644 src/state.rs create mode 100644 src/weidu_parser.rs diff --git a/README.md b/README.md index ea91ea2..39e12c0 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,26 @@ Usage: mod_installer [OPTIONS] --log-file \ --mod-directories Options: - --log-file Full path to target log [env: LOG_FILE] - -g, --game-directory Full path to game directory [env: GAME_DIRECTORY] - -w, --weidu-binary Full Path to weidu binary [env: WEIDU_BINARY] - -m, --mod-directories Full Path to mod directories [env: MOD_DIRECTORIES] - -l, --language Game Language [default: en_US] - -d, --depth Depth to walk folder structure [default: 3] - -s, --skip-installed Compare against installed weidu log, note this is best effort - -h, --help Print help - -V, --version Print version + --log-file + Full path to target log [env: LOG_FILE=] + -g, --game-directory + Full path to game directory [env: GAME_DIRECTORY=] + -w, --weidu-binary + Full Path to weidu binary [env: WEIDU_BINARY=] + -m, --mod-directories + Full Path to mod directories [env: MOD_DIRECTORIES=] + -l, --language + Game Language [default: en_US] + -d, --depth + Depth to walk folder structure [default: 3] + -s, --skip-installed + Compare against installed weidu log, note this is best effort + -a, --abort-on-warnings + If a warning occurs in the weidu child process exit + -h, --help + Print help + -V, --version + Print version ``` ## Log levels diff --git a/src/args.rs b/src/args.rs index 25cea4d..751b5f1 100644 --- a/src/args.rs +++ b/src/args.rs @@ -41,8 +41,9 @@ pub struct Args { #[clap(long, short, action=ArgAction::SetTrue)] pub skip_installed: bool, - #[clap(long, action=ArgAction::SetTrue)] - pub stop_on_warnings: bool, + /// If a warning occurs in the weidu child process exit + #[clap(long, short, action=ArgAction::SetTrue)] + pub abort_on_warnings: bool, } fn parse_absolute_path(arg: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index bc8f395..75b4254 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,12 +15,14 @@ use crate::{ mod args; mod mod_component; +mod state; mod utils; mod weidu; +mod weidu_parser; fn main() { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); - println!( + log::info!( r" /\/\ ___ __| | (_)_ __ ___| |_ __ _| | | ___ _ __ / \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__| @@ -90,11 +92,11 @@ fn main() { log::info!("Installed mod {:?}", &weidu_mod); } InstallationResult::Warnings => { - if args.stop_on_warnings { - log::info!("Installed mod {:?} with warnings, stopping", &weidu_mod); + if args.abort_on_warnings { + log::error!("Installed mod {:?} with warnings, stopping", &weidu_mod); break; } else { - log::info!("Installed mod {:?} with warnings, keep going", &weidu_mod); + log::warn!("Installed mod {:?} with warnings, keep going", &weidu_mod); } } } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..be6c772 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,8 @@ +#[derive(Debug)] +pub enum State { + RequiresInput { question: String }, + InProgress, + Completed, + CompletedWithErrors { error_details: String }, + CompletedWithWarnings, +} diff --git a/src/utils.rs b/src/utils.rs index c621fa1..da3ddad 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,9 @@ +use core::time; use fs_extra::dir::{copy, CopyOptions}; use std::{ fs::File, path::{Path, PathBuf}, + thread, }; use walkdir::WalkDir; @@ -70,6 +72,11 @@ fn find_mod_folder(mod_component: &ModComponent, mod_dir: &Path, depth: usize) - }) } +pub fn sleep(millis: u64) { + let duration = time::Duration::from_millis(millis); + thread::sleep(duration); +} + #[cfg(test)] mod tests { diff --git a/src/weidu.rs b/src/weidu.rs index 826ec29..c255922 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -1,14 +1,15 @@ -use core::time; use std::{ io::{self, BufRead, BufReader, ErrorKind, Write}, panic, path::PathBuf, process::{Child, ChildStdout, Command, Stdio}, - sync::mpsc::{self, Receiver, Sender, TryRecvError}, + sync::mpsc::{self, Receiver, TryRecvError}, thread, }; -use crate::mod_component::ModComponent; +use crate::{ + mod_component::ModComponent, state::State, utils::sleep, weidu_parser::parse_raw_output, +}; pub fn get_user_input() -> String { let stdin = io::stdin(); @@ -56,19 +57,11 @@ pub fn install( handle_io(child) } -#[derive(Debug)] -enum ProcessStateChange { - RequiresInput { question: String }, - InProgress, - Completed, - CompletedWithErrors { error_details: String }, - CompletedWithWarnings, -} - pub fn handle_io(mut child: Child) -> InstallationResult { let mut weidu_stdin = child.stdin.take().unwrap(); - let output_lines_receiver = create_output_reader(child.stdout.take().unwrap()); - let parsed_output_receiver = create_parsed_output_receiver(output_lines_receiver); + let raw_output_receiver = create_output_reader(child.stdout.take().unwrap()); + let (sender, parsed_output_receiver) = mpsc::channel::(); + parse_raw_output(sender, raw_output_receiver); let mut wait_counter = 0; loop { @@ -76,28 +69,28 @@ pub fn handle_io(mut child: Child) -> InstallationResult { Ok(state) => { log::debug!("Current installer state is {:?}", state); match state { - ProcessStateChange::Completed => { + State::Completed => { log::debug!("Weidu process completed"); break; } - ProcessStateChange::CompletedWithErrors { error_details } => { - log::debug!("Weidu process seem to have completed with errors"); + State::CompletedWithErrors { error_details } => { + log::error!("Weidu process seem to have completed with errors"); weidu_stdin .write_all("\n".as_bytes()) .expect("Failed to send final ENTER to weidu process"); return InstallationResult::Fail(error_details); } - ProcessStateChange::CompletedWithWarnings => { - log::debug!("Weidu process seem to have completed with warnings"); + State::CompletedWithWarnings => { + log::warn!("Weidu process seem to have completed with warnings"); weidu_stdin .write_all("\n".as_bytes()) .expect("Failed to send final ENTER to weidu process"); return InstallationResult::Warnings; } - ProcessStateChange::InProgress => { + State::InProgress => { log::debug!("In progress..."); } - ProcessStateChange::RequiresInput { question } => { + State::RequiresInput { question } => { log::info!("User Input required"); log::info!("Question is"); log::info!("{}\n", question); @@ -110,15 +103,17 @@ pub fn handle_io(mut child: Child) -> InstallationResult { } } Err(TryRecvError::Empty) => { - print!("Waiting for child process to end"); - print!("{}\r", ".".repeat(wait_counter)); + log::info!("Waiting for child process to end"); + log::info!("{}\r", ".".repeat(wait_counter)); std::io::stdout().flush().expect("Failed to flush stdout"); wait_counter += 1; wait_counter %= 10; sleep(500); - print!("\r \r"); + log::info!( + "\r \r" + ); std::io::stdout().flush().expect("Failed to flush stdout"); } Err(TryRecvError::Disconnected) => break, @@ -127,143 +122,6 @@ pub fn handle_io(mut child: Child) -> InstallationResult { InstallationResult::Success } -#[derive(Debug)] -enum ParserState { - CollectingQuestion, - WaitingForMoreQuestionContent, - LookingForInterestingOutput, -} - -fn create_parsed_output_receiver( - raw_output_receiver: Receiver, -) -> Receiver { - let (sender, receiver) = mpsc::channel::(); - parse_raw_output(sender, raw_output_receiver); - receiver -} - -fn parse_raw_output(sender: Sender, receiver: Receiver) { - let mut current_state = ParserState::LookingForInterestingOutput; - let mut question = String::new(); - sender - .send(ProcessStateChange::InProgress) - .expect("Failed to send process start event"); - thread::spawn(move || loop { - match receiver.try_recv() { - Ok(string) => match current_state { - ParserState::CollectingQuestion | ParserState::WaitingForMoreQuestionContent => { - if string_looks_like_weidu_is_doing_something_useful(&string) { - log::debug!( - "Weidu seems to know an answer for the last question, ignoring it" - ); - current_state = ParserState::LookingForInterestingOutput; - question.clear(); - } else { - log::debug!("Appending line '{}' to user question", string); - question.push_str(string.as_str()); - current_state = ParserState::CollectingQuestion; - } - } - ParserState::LookingForInterestingOutput => { - let may_be_weidu_finished_state = detect_weidu_finished_state(&string); - if let Some(weidu_finished_state) = may_be_weidu_finished_state { - sender - .send(weidu_finished_state) - .expect("Failed to send process error event"); - break; - } else if string_looks_like_question(&string) { - log::debug!( - "Changing parser state to '{:?}' due to line {}", - ParserState::CollectingQuestion, - string - ); - current_state = ParserState::CollectingQuestion; - question.push_str(string.as_str()); - } else { - log::debug!("Ignoring line {}", string); - } - } - }, - Err(TryRecvError::Empty) => { - match current_state { - ParserState::CollectingQuestion => { - log::debug!( - "Changing parser state to '{:?}'", - ParserState::WaitingForMoreQuestionContent - ); - current_state = ParserState::WaitingForMoreQuestionContent; - } - ParserState::WaitingForMoreQuestionContent => { - log::debug!("No new weidu otput, sending question to user"); - sender - .send(ProcessStateChange::RequiresInput { question }) - .expect("Failed to send question"); - current_state = ParserState::LookingForInterestingOutput; - question = String::new(); - } - _ => { - // there is no new weidu output and we are not waiting for any, so there is nothing to do - } - } - sleep(100); - } - Err(TryRecvError::Disconnected) => { - sender - .send(ProcessStateChange::Completed) - .expect("Failed to send provess end event"); - break; - } - } - }); -} - -fn detect_weidu_finished_state(string: &str) -> Option { - if string_looks_like_weidu_completed_with_errors(string) { - Some(ProcessStateChange::CompletedWithErrors { - error_details: string.trim().to_string(), - }) - } else if string_looks_like_weidu_completed_with_warnings(string) { - Some(ProcessStateChange::CompletedWithWarnings) - } else { - None - } -} - -fn string_looks_like_question(string: &str) -> bool { - let lowercase_string = string.trim().to_lowercase(); - !lowercase_string.contains("installing") - && (lowercase_string.contains("choice") - || lowercase_string.starts_with("choose") - || lowercase_string.starts_with("select") - || lowercase_string.starts_with("do you want") - || lowercase_string.starts_with("would you like") - || lowercase_string.starts_with("enter")) - || lowercase_string.ends_with('?') - || lowercase_string.ends_with(':') -} - -fn string_looks_like_weidu_is_doing_something_useful(string: &str) -> bool { - let lowercase_string = string.trim().to_lowercase(); - lowercase_string.contains("copying") - || lowercase_string.contains("copied") - || lowercase_string.contains("installing") - || lowercase_string.contains("installed") - || lowercase_string.contains("patching") - || lowercase_string.contains("patched") - || lowercase_string.contains("processing") - || lowercase_string.contains("processed") -} - -fn string_looks_like_weidu_completed_with_errors(string: &str) -> bool { - let lowercase_string = string.trim().to_lowercase(); - lowercase_string.contains("not installed due to errors") -} - -fn string_looks_like_weidu_completed_with_warnings(string: &str) -> bool { - let lowercase_string = string.trim().to_lowercase(); - lowercase_string.contains("installed with warnings") -} - fn create_output_reader(out: ChildStdout) -> Receiver { let (tx, rx) = mpsc::channel::(); let mut buffered_reader = BufReader::new(out); @@ -275,7 +133,7 @@ fn create_output_reader(out: ChildStdout) -> Receiver { break; } Ok(_) => { - log::debug!("Got line from process: '{}'", line); + log::debug!("{}", line); tx.send(line).expect("Failed to sent process output line"); } Err(ref e) if e.kind() == ErrorKind::InvalidData => { @@ -291,8 +149,3 @@ fn create_output_reader(out: ChildStdout) -> Receiver { }); rx } - -fn sleep(millis: u64) { - let duration = time::Duration::from_millis(millis); - thread::sleep(duration); -} diff --git a/src/weidu_parser.rs b/src/weidu_parser.rs new file mode 100644 index 0000000..f5606df --- /dev/null +++ b/src/weidu_parser.rs @@ -0,0 +1,137 @@ +use std::{ + sync::mpsc::{Receiver, Sender, TryRecvError}, + thread, +}; + +use crate::{state::State, utils::sleep}; + +const WEIDU_USEFUL_STATUS: [&str; 8] = [ + "copying", + "copied", + "installing", + "installed", + "patching", + "patched", + "processing", + "processed", +]; + +const WEIDU_CHOICE: [&str; 6] = [ + "choice", + "choose", + "select", + "do you want", + "would you like", + "enter", +]; + +const WEIDU_COMPLETED_WITH_WARNINGS: &str = "installed with warnings"; + +const WEIDU_FAILED_WITH_ERROR: &str = "not installed due to errors"; + +#[derive(Debug)] +enum ParserState { + CollectingQuestion, + WaitingForMoreQuestionContent, + LookingForInterestingOutput, +} + +pub fn parse_raw_output(sender: Sender, receiver: Receiver) { + let mut current_state = ParserState::LookingForInterestingOutput; + let mut question = String::new(); + sender + .send(State::InProgress) + .expect("Failed to send process start event"); + thread::spawn(move || loop { + match receiver.try_recv() { + Ok(string) => match current_state { + ParserState::CollectingQuestion | ParserState::WaitingForMoreQuestionContent => { + if WEIDU_USEFUL_STATUS.contains(&string.as_str()) { + log::debug!( + "Weidu seems to know an answer for the last question, ignoring it" + ); + current_state = ParserState::LookingForInterestingOutput; + question.clear(); + } else { + log::debug!("Appending line '{}' to user question", string); + question.push_str(&string); + current_state = ParserState::CollectingQuestion; + } + } + ParserState::LookingForInterestingOutput => { + let may_be_weidu_finished_state = detect_weidu_finished_state(&string); + if let Some(weidu_finished_state) = may_be_weidu_finished_state { + sender + .send(weidu_finished_state) + .expect("Failed to send process error event"); + break; + } else if string_looks_like_question(&string) { + log::debug!( + "Changing parser state to '{:?}' due to line {}", + ParserState::CollectingQuestion, + string + ); + current_state = ParserState::CollectingQuestion; + question.push_str(string.as_str()); + } else { + if !string.trim().is_empty() { + log::trace!("{}", string); + } + } + } + }, + Err(TryRecvError::Empty) => { + match current_state { + ParserState::CollectingQuestion => { + log::debug!( + "Changing parser state to '{:?}'", + ParserState::WaitingForMoreQuestionContent + ); + current_state = ParserState::WaitingForMoreQuestionContent; + } + ParserState::WaitingForMoreQuestionContent => { + log::debug!("No new weidu otput, sending question to user"); + sender + .send(State::RequiresInput { question }) + .expect("Failed to send question"); + current_state = ParserState::LookingForInterestingOutput; + question = String::new(); + } + _ => { + // there is no new weidu output and we are not waiting for any, so there is nothing to do + } + } + sleep(100); + } + Err(TryRecvError::Disconnected) => { + sender + .send(State::Completed) + .expect("Failed to send provess end event"); + break; + } + } + }); +} + +fn string_looks_like_question(weidu_output: &str) -> bool { + let comparable_output = weidu_output.trim().to_lowercase(); + if comparable_output.contains("installing") { + return false; + } + (WEIDU_CHOICE.contains(&comparable_output.as_str())) + || comparable_output.ends_with('?') + || comparable_output.ends_with(':') +} + +fn detect_weidu_finished_state(weidu_output: &str) -> Option { + let comparable_output = weidu_output.trim().to_lowercase(); + if WEIDU_FAILED_WITH_ERROR.eq(&comparable_output) { + Some(State::CompletedWithErrors { + error_details: comparable_output, + }) + } else if WEIDU_COMPLETED_WITH_WARNINGS.eq(&comparable_output) { + Some(State::CompletedWithWarnings) + } else { + None + } +}