From 960ea1b46fafbb1b8fd58ce19cc520053834cc79 Mon Sep 17 00:00:00 2001 From: Ilya Lapshin Date: Tue, 14 Nov 2023 23:54:50 +0300 Subject: [PATCH 1/5] Rewrote weidu output parsing --- src/weidu.rs | 278 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 196 insertions(+), 82 deletions(-) diff --git a/src/weidu.rs b/src/weidu.rs index 92480a8..6f4a336 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -1,7 +1,10 @@ +use core::time; use std::{ io::{self, BufRead, BufReader, Write}, path::PathBuf, - process::{Command, Stdio}, + process::{Child, ChildStdout, Command, Stdio}, + sync::mpsc::{self, Receiver, Sender, TryRecvError}, + thread, }; use crate::mod_component::ModComponent; @@ -16,7 +19,7 @@ pub fn get_user_input() -> String { } fn generate_args(weidu_mod: &ModComponent, language: &str) -> Vec { - format!("{mod_name}/{mod_tp_file} --yes --ask-only {component} --use-lang {game_lang} --language {mod_lang}", mod_name = weidu_mod.name, mod_tp_file = weidu_mod.tp_file, component = weidu_mod.component, mod_lang = weidu_mod.lang, game_lang = language).split(' ').map(|x|x.to_string()).collect() + format!("{mod_name}/{mod_tp_file} --autolog --force-install {component} --use-lang {game_lang} --language {mod_lang}", mod_name = weidu_mod.name, mod_tp_file = weidu_mod.tp_file, component = weidu_mod.component, mod_lang = weidu_mod.lang, game_lang = language).split(' ').map(|x|x.to_string()).collect() } pub fn install( @@ -24,111 +27,222 @@ pub fn install( game_directory: &PathBuf, weidu_mod: &ModComponent, language: &str, -) { +) -> Result<(), String> { let weidu_args = generate_args(weidu_mod, language); let mut command = Command::new(weidu_binary); let weidu_process = command.current_dir(game_directory).args(weidu_args.clone()); - let mut child = weidu_process + let child = weidu_process .stdin(Stdio::piped()) .stdout(Stdio::piped()) + .stderr(Stdio::piped()) .spawn() .expect("Failed to spawn weidu process"); - let mut reader = BufReader::new(child.stdout.as_mut().unwrap()); - let stdin = &mut child.stdin.take().expect("Failed to open stdin"); - - let mut choice_flag = false; - let mut failure_flag = false; - while child.stderr.is_none() { - let mut text = String::new(); - if reader.read_line(&mut text).is_ok() { - if !text.is_empty() && !failure_flag && !choice_flag { - log::trace!("{}", text); - } else if choice_flag { - log::info!("{}", text); - } else { - log::error!("{}", text); - } + handle_io(child) +} - if text.to_ascii_lowercase().contains("failure") { - failure_flag = true; - } +#[derive(Debug)] +enum ProcessStateChange { + RequiresInput { question: String }, + InProgress, + Completed, + CompletedWithErrors { error_details: String }, +} - if text.contains("Stopping installation because of error.") { - log::error!("Weidu process failed with args: {:?}", weidu_args); - panic!(); - } +pub fn handle_io(mut child: Child) -> Result<(), String> { + let mut writer = child.stdin.take().unwrap(); - match text.clone() { - // Choice - _ if choice_flag => { - if !text.chars().nth(1).unwrap_or_default().is_numeric() { - stdin - .write_all(get_user_input().as_bytes()) - .expect("Failed to write to stdin"); + let child_state_channel = create_output_reader(child.stdout.take().unwrap()); + let (parsed_output_sender, parsed_output_receiver) = mpsc::channel::(); + parse_output(parsed_output_sender, child_state_channel); + let mut wait_counter = 0; + loop { + match parsed_output_receiver.try_recv() { + Ok( state) => { + log::debug!("Current installer state is {:?}", state); + match state { + ProcessStateChange::Completed => { + log::debug!("Weidu process completed"); break; } + ProcessStateChange::CompletedWithErrors { error_details } => { + log::debug!("Weidu process seem to have completed with errors"); + writer + .write("\n".as_bytes()) + .expect("Failed to send final ENTER to weidu process"); + return Err(error_details); + } + ProcessStateChange::InProgress => { + log::debug!("In progress..."); + } + ProcessStateChange::RequiresInput { question } => { + println!("User Input required"); + println!("Question is {}", question); + println!("Please do so something!"); + let user_input = get_user_input(); + println!(""); + log::debug!("Read user input {}, sending it to process ", user_input); + writer.write_all(user_input.as_bytes()).unwrap(); + log::debug!("Input sent"); + } } - x if x.starts_with("SKIPPING: ") || x.starts_with("Already Asked About") => { - stdin - .write_all("\n".as_bytes()) - .expect("Failed to write to stdin"); - log::debug!("Skiping component"); - break; - } - x if (x.trim_end().ends_with("[Q]uit or choose one:") - || x.trim_end().starts_with("Enter ")) - && !x.to_ascii_lowercase().starts_with("[r]e-install") => - { - log::info!("{}", text); - log::trace!("Choice found"); - choice_flag = true; - } - - // Success - x if x.contains("SUCCESSFULLY INSTALLED") - || x.starts_with("INSTALLED WITH WARNINGS") => - { - break; - } - - // Install - x if x.starts_with("Install") => { - log::info!("{}", text); - stdin - .write_all("\n".as_bytes()) - .expect("Failed to write to stdin"); - } - x if x.starts_with("[I]nstall") => { - stdin - .write_all("I\n".as_bytes()) - .expect("Failed to write to stdin"); - log::debug!("Installing"); - } - x if x.to_ascii_lowercase().starts_with("[r]e-install") => { - stdin - .write_all("Q\n".as_bytes()) - .expect("Could not quit out"); - log::debug!("Continue as already installed"); - break; - } - _ => {} } - } else { - break; + Err(TryRecvError::Empty) => { + print!("No relevant output from child process, waiting"); + print!("{}", ".".repeat(wait_counter)); + wait_counter += 1; + wait_counter %= 10; + sleep(1000); + print!("\r X\r"); + } + Err(TryRecvError::Disconnected) => break, } } match child.wait_with_output() { Ok(output) if !output.status.success() => { - panic!("{:#?}", output); + panic!("Something went wrong: {:#?}", output); } Err(err) => { panic!("Did not close properly: {}", err); } - Ok(output) => { - log::trace!("{:#?}", output); + Ok(_) => { + return Ok(()); } } } + +#[derive(Debug)] +enum ParserState { + CollectingQuestion, + SleepingForQuestionToComplete, + LookingForInterestingOutput, +} + +fn parse_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::SleepingForQuestionToComplete => { + if string_looks_like_processing(&string) + { + log::debug!("Weidu seems to know an answer for this question"); + 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 lowercase_string = string.to_lowercase(); + if lowercase_string.contains("not installed due to errors") + || lowercase_string.contains("installed with warnings") { + log::debug!("Weidu seems to have encountered errors during isntallation"); + sender + .send(ProcessStateChange::CompletedWithErrors { error_details: string.trim().to_string() }) + .expect("Failed to send process error event"); + break; + } else if string_looks_like_question(&lowercase_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::SleepingForQuestionToComplete + ); + current_state = ParserState::SleepingForQuestionToComplete; + } + ParserState::SleepingForQuestionToComplete => { + log::debug!("No new weidu otput, sending question to user"); + sender + .send(ProcessStateChange::RequiresInput { + question: question.clone(), + }) + .expect("Failed to send question"); + current_state = ParserState::LookingForInterestingOutput; + question.clear(); + } + _ => {} + } + sleep(100); + } + Err(TryRecvError::Disconnected) => { + sender + .send(ProcessStateChange::Completed) + .expect("Failed to send provess end event"); + break; + } + } + }); +} + +fn string_looks_like_question(string: &str) -> bool { + let lowercase_string = string.to_lowercase(); + 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") +} + +fn string_looks_like_processing(string: &str) -> bool { + let lowercase_string = string.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") + || lowercase_string.ends_with(":\n") + +} + +fn create_output_reader(out: ChildStdout) -> Receiver { + let (tx, rx) = mpsc::channel::(); + let mut buffered_reader = BufReader::new(out); + thread::spawn(move || loop { + let mut line = String::new(); + match buffered_reader.read_line(&mut line) { + Ok(0) => { + log::debug!("Weidu process ended"); + break; + } + Ok(_) => { + log::debug!("Got line from weidu: '{}'", line); + tx.send(line).expect("Failed to sent process output line"); + } + Err(_) => { + tx.send("Error".to_string()).expect("Oops"); + } + } + }); + rx +} + +fn sleep(millis: u64) { + let duration = time::Duration::from_millis(millis); + thread::sleep(duration); +} From 0c7552333c699edf045d345375ba49983932fb51 Mon Sep 17 00:00:00 2001 From: Ilya Lapshin Date: Wed, 15 Nov 2023 19:42:18 +0300 Subject: [PATCH 2/5] Minor refactorings --- src/main.rs | 12 +++-- src/weidu.rs | 128 +++++++++++++++++++++++++++++---------------------- 2 files changed, 82 insertions(+), 58 deletions(-) diff --git a/src/main.rs b/src/main.rs index b93b686..060796b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,12 +73,18 @@ fn main() { ); copy_mod_folder(&args.game_directory, mod_folder) } - install( + match install( &args.weidu_binary, &args.game_directory, &weidu_mod, &args.language, - ); - log::info!("Installed mod {:?}", &weidu_mod); + ) { + Err(message) => { + panic!("Failed to install mod {}, error is '{}'", weidu_mod.name, message); + } + Ok(_) => { + log::info!("Installed mod {:?}", &weidu_mod); + } + } } } diff --git a/src/weidu.rs b/src/weidu.rs index 6f4a336..1c4db01 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -4,7 +4,7 @@ use std::{ path::PathBuf, process::{Child, ChildStdout, Command, Stdio}, sync::mpsc::{self, Receiver, Sender, TryRecvError}, - thread, + thread, panic, }; use crate::mod_component::ModComponent; @@ -13,13 +13,20 @@ pub fn get_user_input() -> String { let stdin = io::stdin(); let mut input = String::new(); stdin.read_line(&mut input).unwrap_or_default(); - log::debug!("User input: {}", input); - input.to_string() } fn generate_args(weidu_mod: &ModComponent, language: &str) -> Vec { - format!("{mod_name}/{mod_tp_file} --autolog --force-install {component} --use-lang {game_lang} --language {mod_lang}", mod_name = weidu_mod.name, mod_tp_file = weidu_mod.tp_file, component = weidu_mod.component, mod_lang = weidu_mod.lang, game_lang = language).split(' ').map(|x|x.to_string()).collect() + format!("{mod_name}/{mod_tp_file} --autolog --force-install {component} --use-lang {game_lang} --language {mod_lang}", + mod_name = weidu_mod.name, + mod_tp_file = weidu_mod.tp_file, + component = weidu_mod.component, + mod_lang = weidu_mod.lang, + game_lang = language + ) + .split(' ') + .map(|x|x.to_string()) + .collect() } pub fn install( @@ -51,16 +58,15 @@ enum ProcessStateChange { } pub fn handle_io(mut child: Child) -> Result<(), String> { - let mut writer = child.stdin.take().unwrap(); + 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 child_state_channel = create_output_reader(child.stdout.take().unwrap()); - let (parsed_output_sender, parsed_output_receiver) = mpsc::channel::(); - parse_output(parsed_output_sender, child_state_channel); let mut wait_counter = 0; loop { match parsed_output_receiver.try_recv() { - Ok( state) => { - log::debug!("Current installer state is {:?}", state); + Ok(state) => { + log::debug!("Current installer state is {:?}", state); match state { ProcessStateChange::Completed => { log::debug!("Weidu process completed"); @@ -68,7 +74,7 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { } ProcessStateChange::CompletedWithErrors { error_details } => { log::debug!("Weidu process seem to have completed with errors"); - writer + weidu_stdin .write("\n".as_bytes()) .expect("Failed to send final ENTER to weidu process"); return Err(error_details); @@ -83,7 +89,7 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { let user_input = get_user_input(); println!(""); log::debug!("Read user input {}, sending it to process ", user_input); - writer.write_all(user_input.as_bytes()).unwrap(); + weidu_stdin.write_all(user_input.as_bytes()).unwrap(); log::debug!("Input sent"); } } @@ -91,36 +97,35 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { Err(TryRecvError::Empty) => { print!("No relevant output from child process, waiting"); print!("{}", ".".repeat(wait_counter)); + std::io::stdout().flush().expect("Failed to flush stdout"); + wait_counter += 1; wait_counter %= 10; - sleep(1000); - print!("\r X\r"); + sleep(500); + + print!("\r \r"); + std::io::stdout().flush().expect("Failed to flush stdout"); } Err(TryRecvError::Disconnected) => break, } } - - match child.wait_with_output() { - Ok(output) if !output.status.success() => { - panic!("Something went wrong: {:#?}", output); - } - Err(err) => { - panic!("Did not close properly: {}", err); - } - Ok(_) => { - return Ok(()); - } - } + Ok(()) } #[derive(Debug)] enum ParserState { CollectingQuestion, - SleepingForQuestionToComplete, + WaitingForMoreQuestionContent, LookingForInterestingOutput, } -fn parse_output(sender: Sender, receiver: Receiver) { +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 @@ -129,10 +134,9 @@ fn parse_output(sender: Sender, receiver: Receiver) thread::spawn(move || loop { match receiver.try_recv() { Ok(string) => match current_state { - ParserState::CollectingQuestion | ParserState::SleepingForQuestionToComplete => { - if string_looks_like_processing(&string) - { - log::debug!("Weidu seems to know an answer for this question"); + 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 { @@ -142,15 +146,14 @@ fn parse_output(sender: Sender, receiver: Receiver) } } ParserState::LookingForInterestingOutput => { - let lowercase_string = string.to_lowercase(); - if lowercase_string.contains("not installed due to errors") - || lowercase_string.contains("installed with warnings") { - log::debug!("Weidu seems to have encountered errors during isntallation"); + if string_looks_like_weidu_completed_with_errors(&string) + || string_looks_like_weidu_completed_with_warnings(&string) { + log::debug!("Weidu seems to have encountered errors during installation"); sender .send(ProcessStateChange::CompletedWithErrors { error_details: string.trim().to_string() }) .expect("Failed to send process error event"); break; - } else if string_looks_like_question(&lowercase_string) { + } else if string_looks_like_question(&string) { log::debug!( "Changing parser state to '{:?}' due to line {}", ParserState::CollectingQuestion, @@ -168,21 +171,23 @@ fn parse_output(sender: Sender, receiver: Receiver) ParserState::CollectingQuestion => { log::debug!( "Changing parser state to '{:?}'", - ParserState::SleepingForQuestionToComplete + ParserState::WaitingForMoreQuestionContent ); - current_state = ParserState::SleepingForQuestionToComplete; + current_state = ParserState::WaitingForMoreQuestionContent; } - ParserState::SleepingForQuestionToComplete => { + ParserState::WaitingForMoreQuestionContent => { log::debug!("No new weidu otput, sending question to user"); sender .send(ProcessStateChange::RequiresInput { - question: question.clone(), + question: question, }) .expect("Failed to send question"); current_state = ParserState::LookingForInterestingOutput; - question.clear(); + question = String::new(); + } + _ => { + // there is no new weidu output and we are not waiting for any, so there is nothing to do } - _ => {} } sleep(100); } @@ -197,17 +202,20 @@ fn parse_output(sender: Sender, receiver: Receiver) } fn string_looks_like_question(string: &str) -> bool { - let lowercase_string = string.to_lowercase(); - 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") + 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_processing(string: &str) -> bool { - let lowercase_string = string.to_lowercase(); +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") @@ -216,8 +224,18 @@ fn string_looks_like_processing(string: &str) -> bool { || lowercase_string.contains("patched") || lowercase_string.contains("processing") || lowercase_string.contains("processed") - || lowercase_string.ends_with(":\n") + +} + +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 { @@ -234,8 +252,8 @@ fn create_output_reader(out: ChildStdout) -> Receiver { log::debug!("Got line from weidu: '{}'", line); tx.send(line).expect("Failed to sent process output line"); } - Err(_) => { - tx.send("Error".to_string()).expect("Oops"); + Err(details) => { + panic!("Failed to read weidu output, error is '{:?}'", details); } } }); From 761fbbef2aa3f54d80e03a9c64b5e84c49fa7d03 Mon Sep 17 00:00:00 2001 From: Ilya Lapshin Date: Thu, 16 Nov 2023 22:58:08 +0300 Subject: [PATCH 3/5] Added command-line flag to stop on warnings --- src/args.rs | 3 +++ src/main.rs | 15 +++++++++++--- src/weidu.rs | 57 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/args.rs b/src/args.rs index 10cce29..25cea4d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -40,6 +40,9 @@ pub struct Args { /// Compare against installed weidu log, note this is best effort #[clap(long, short, action=ArgAction::SetTrue)] pub skip_installed: bool, + + #[clap(long, action=ArgAction::SetTrue)] + pub stop_on_warnings: bool, } fn parse_absolute_path(arg: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index 060796b..9b76d4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use crate::{ copy_mod_folder, create_weidu_log_if_not_exists, mod_folder_present_in_game_directory, search_mod_folders, }, - weidu::install, + weidu::{install, InstallationResult}, }; mod args; @@ -73,18 +73,27 @@ fn main() { ); copy_mod_folder(&args.game_directory, mod_folder) } + log::info!("Installing mod {:?}", &weidu_mod); match install( &args.weidu_binary, &args.game_directory, &weidu_mod, &args.language, ) { - Err(message) => { + InstallationResult::Fail(message) => { panic!("Failed to install mod {}, error is '{}'", weidu_mod.name, message); } - Ok(_) => { + InstallationResult::Success => { log::info!("Installed mod {:?}", &weidu_mod); } + InstallationResult::Warnings => { + if args.stop_on_warnings { + log::info!("Installed mod {:?} with warnings, stopping", &weidu_mod); + break; + } else { + log::info!("Installed mod {:?} with warnings, keep going", &weidu_mod); + } + } } } } diff --git a/src/weidu.rs b/src/weidu.rs index 1c4db01..f3bcb9a 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -1,6 +1,6 @@ use core::time; use std::{ - io::{self, BufRead, BufReader, Write}, + io::{self, BufRead, BufReader, Write, ErrorKind}, path::PathBuf, process::{Child, ChildStdout, Command, Stdio}, sync::mpsc::{self, Receiver, Sender, TryRecvError}, @@ -29,12 +29,18 @@ fn generate_args(weidu_mod: &ModComponent, language: &str) -> Vec { .collect() } +pub enum InstallationResult { + Success, + Warnings, + Fail(String) +} + pub fn install( weidu_binary: &PathBuf, game_directory: &PathBuf, weidu_mod: &ModComponent, language: &str, -) -> Result<(), String> { +) -> InstallationResult { let weidu_args = generate_args(weidu_mod, language); let mut command = Command::new(weidu_binary); let weidu_process = command.current_dir(game_directory).args(weidu_args.clone()); @@ -55,9 +61,10 @@ enum ProcessStateChange { InProgress, Completed, CompletedWithErrors { error_details: String }, + CompletedWithWarnings, } -pub fn handle_io(mut child: Child) -> Result<(), String> { +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); @@ -77,7 +84,14 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { weidu_stdin .write("\n".as_bytes()) .expect("Failed to send final ENTER to weidu process"); - return Err(error_details); + return InstallationResult::Fail(error_details); + } + ProcessStateChange::CompletedWithWarnings => { + log::debug!("Weidu process seem to have completed with warnings"); + weidu_stdin + .write("\n".as_bytes()) + .expect("Failed to send final ENTER to weidu process"); + return InstallationResult::Warnings; } ProcessStateChange::InProgress => { log::debug!("In progress..."); @@ -95,8 +109,8 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { } } Err(TryRecvError::Empty) => { - print!("No relevant output from child process, waiting"); - print!("{}", ".".repeat(wait_counter)); + print!("Waiting for child process to end"); + print!("{}\r", ".".repeat(wait_counter)); std::io::stdout().flush().expect("Failed to flush stdout"); wait_counter += 1; @@ -109,7 +123,7 @@ pub fn handle_io(mut child: Child) -> Result<(), String> { Err(TryRecvError::Disconnected) => break, } } - Ok(()) + InstallationResult::Success } #[derive(Debug)] @@ -146,11 +160,10 @@ fn parse_raw_output(sender: Sender, receiver: Receiver { - if string_looks_like_weidu_completed_with_errors(&string) - || string_looks_like_weidu_completed_with_warnings(&string) { - log::debug!("Weidu seems to have encountered errors during installation"); + let weidu_finished_state = detect_weidu_finished_state(&string); + if weidu_finished_state.is_some() { sender - .send(ProcessStateChange::CompletedWithErrors { error_details: string.trim().to_string() }) + .send(weidu_finished_state.unwrap()) .expect("Failed to send process error event"); break; } else if string_looks_like_question(&string) { @@ -201,6 +214,16 @@ fn parse_raw_output(sender: Sender, receiver: Receiver 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") @@ -245,15 +268,21 @@ fn create_output_reader(out: ChildStdout) -> Receiver { let mut line = String::new(); match buffered_reader.read_line(&mut line) { Ok(0) => { - log::debug!("Weidu process ended"); + log::debug!("Process ended"); break; } Ok(_) => { - log::debug!("Got line from weidu: '{}'", line); + log::debug!("Got line from process: '{}'", line); tx.send(line).expect("Failed to sent process output line"); } + Err(ref e) if e.kind() == ErrorKind::InvalidData => { + // sometimes there is a non-unicode gibberish in process output, it + // does not seem to be an indicator of error or break anything, ignore it + log::warn!("Failed to read weidu output"); + } Err(details) => { - panic!("Failed to read weidu output, error is '{:?}'", details); + log::error!("Failed to read process output, error is '{:?}'", details); + panic!("Failed to read process output, error is '{:?}'", details); } } }); From 7cd82c4688fd4105b937a0c6947d0e6895803266 Mon Sep 17 00:00:00 2001 From: Ilya Lapshin Date: Fri, 17 Nov 2023 01:24:50 +0300 Subject: [PATCH 4/5] Fixed formatting --- src/main.rs | 7 +++++-- src/weidu.rs | 33 ++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9b76d4f..bc8f395 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,14 +74,17 @@ fn main() { copy_mod_folder(&args.game_directory, mod_folder) } log::info!("Installing mod {:?}", &weidu_mod); - match install( + match install( &args.weidu_binary, &args.game_directory, &weidu_mod, &args.language, ) { InstallationResult::Fail(message) => { - panic!("Failed to install mod {}, error is '{}'", weidu_mod.name, message); + panic!( + "Failed to install mod {}, error is '{}'", + weidu_mod.name, message + ); } InstallationResult::Success => { log::info!("Installed mod {:?}", &weidu_mod); diff --git a/src/weidu.rs b/src/weidu.rs index f3bcb9a..6949682 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -1,10 +1,11 @@ use core::time; use std::{ - io::{self, BufRead, BufReader, Write, ErrorKind}, + io::{self, BufRead, BufReader, ErrorKind, Write}, + panic, path::PathBuf, process::{Child, ChildStdout, Command, Stdio}, sync::mpsc::{self, Receiver, Sender, TryRecvError}, - thread, panic, + thread, }; use crate::mod_component::ModComponent; @@ -32,7 +33,7 @@ fn generate_args(weidu_mod: &ModComponent, language: &str) -> Vec { pub enum InstallationResult { Success, Warnings, - Fail(String) + Fail(String), } pub fn install( @@ -133,7 +134,9 @@ enum ParserState { LookingForInterestingOutput, } -fn create_parsed_output_receiver(raw_output_receiver: Receiver) -> Receiver { +fn create_parsed_output_receiver( + raw_output_receiver: Receiver, +) -> Receiver { let (sender, receiver) = mpsc::channel::(); parse_raw_output(sender, raw_output_receiver); receiver @@ -150,7 +153,9 @@ fn parse_raw_output(sender: Sender, receiver: Receiver 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"); + log::debug!( + "Weidu seems to know an answer for the last question, ignoring it" + ); current_state = ParserState::LookingForInterestingOutput; question.clear(); } else { @@ -191,9 +196,7 @@ fn parse_raw_output(sender: Sender, receiver: Receiver { log::debug!("No new weidu otput, sending question to user"); sender - .send(ProcessStateChange::RequiresInput { - question: question, - }) + .send(ProcessStateChange::RequiresInput { question: question }) .expect("Failed to send question"); current_state = ParserState::LookingForInterestingOutput; question = String::new(); @@ -216,7 +219,9 @@ fn parse_raw_output(sender: Sender, receiver: Receiver Option { if string_looks_like_weidu_completed_with_errors(&string) { - Some(ProcessStateChange::CompletedWithErrors { error_details: string.trim().to_string() }) + Some(ProcessStateChange::CompletedWithErrors { + error_details: string.trim().to_string(), + }) } else if string_looks_like_weidu_completed_with_warnings(&string) { Some(ProcessStateChange::CompletedWithWarnings) } else { @@ -233,8 +238,8 @@ fn string_looks_like_question(string: &str) -> bool { || 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(":") + || lowercase_string.ends_with("?") + || lowercase_string.ends_with(":") } fn string_looks_like_weidu_is_doing_something_useful(string: &str) -> bool { @@ -247,8 +252,6 @@ fn string_looks_like_weidu_is_doing_something_useful(string: &str) -> bool { || lowercase_string.contains("patched") || lowercase_string.contains("processing") || lowercase_string.contains("processed") - - } fn string_looks_like_weidu_completed_with_errors(string: &str) -> bool { @@ -276,8 +279,8 @@ fn create_output_reader(out: ChildStdout) -> Receiver { tx.send(line).expect("Failed to sent process output line"); } Err(ref e) if e.kind() == ErrorKind::InvalidData => { - // sometimes there is a non-unicode gibberish in process output, it - // does not seem to be an indicator of error or break anything, ignore it + // sometimes there is a non-unicode gibberish in process output, it + // does not seem to be an indicator of error or break anything, ignore it log::warn!("Failed to read weidu output"); } Err(details) => { From 85d66d8cd7ae60862bc6bf4ab6e978d3d080034e Mon Sep 17 00:00:00 2001 From: Ilya Lapshin Date: Sun, 19 Nov 2023 00:10:55 +0300 Subject: [PATCH 5/5] Minor fixes --- src/weidu.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/weidu.rs b/src/weidu.rs index 6949682..826ec29 100644 --- a/src/weidu.rs +++ b/src/weidu.rs @@ -44,7 +44,7 @@ pub fn install( ) -> InstallationResult { let weidu_args = generate_args(weidu_mod, language); let mut command = Command::new(weidu_binary); - let weidu_process = command.current_dir(game_directory).args(weidu_args.clone()); + let weidu_process = command.current_dir(game_directory).args(weidu_args); let child = weidu_process .stdin(Stdio::piped()) @@ -83,14 +83,14 @@ pub fn handle_io(mut child: Child) -> InstallationResult { ProcessStateChange::CompletedWithErrors { error_details } => { log::debug!("Weidu process seem to have completed with errors"); weidu_stdin - .write("\n".as_bytes()) + .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"); weidu_stdin - .write("\n".as_bytes()) + .write_all("\n".as_bytes()) .expect("Failed to send final ENTER to weidu process"); return InstallationResult::Warnings; } @@ -98,11 +98,11 @@ pub fn handle_io(mut child: Child) -> InstallationResult { log::debug!("In progress..."); } ProcessStateChange::RequiresInput { question } => { - println!("User Input required"); - println!("Question is {}", question); - println!("Please do so something!"); + log::info!("User Input required"); + log::info!("Question is"); + log::info!("{}\n", question); + log::info!("Please do so something!"); let user_input = get_user_input(); - println!(""); log::debug!("Read user input {}, sending it to process ", user_input); weidu_stdin.write_all(user_input.as_bytes()).unwrap(); log::debug!("Input sent"); @@ -165,10 +165,10 @@ fn parse_raw_output(sender: Sender, receiver: Receiver { - let weidu_finished_state = detect_weidu_finished_state(&string); - if weidu_finished_state.is_some() { + 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.unwrap()) + .send(weidu_finished_state) .expect("Failed to send process error event"); break; } else if string_looks_like_question(&string) { @@ -196,7 +196,7 @@ fn parse_raw_output(sender: Sender, receiver: Receiver { log::debug!("No new weidu otput, sending question to user"); sender - .send(ProcessStateChange::RequiresInput { question: question }) + .send(ProcessStateChange::RequiresInput { question }) .expect("Failed to send question"); current_state = ParserState::LookingForInterestingOutput; question = String::new(); @@ -218,11 +218,11 @@ fn parse_raw_output(sender: Sender, receiver: Receiver Option { - if string_looks_like_weidu_completed_with_errors(&string) { + 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) { + } else if string_looks_like_weidu_completed_with_warnings(string) { Some(ProcessStateChange::CompletedWithWarnings) } else { None @@ -238,8 +238,8 @@ fn string_looks_like_question(string: &str) -> bool { || 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(":") + || lowercase_string.ends_with('?') + || lowercase_string.ends_with(':') } fn string_looks_like_weidu_is_doing_something_useful(string: &str) -> bool {