mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-19 14:47:36 -04:00
UX: Stream logs to the frontend during installation process instead of only updating after each step. (#565)
This commit is contained in:
parent
7b9ade5def
commit
135f200f4a
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
|
@ -3193,6 +3193,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"ts-rs",
|
||||
"walkdir",
|
||||
"wgpu",
|
||||
|
|
|
@ -43,6 +43,7 @@ zip = { version = "2.2.0", features = ["deflate-zlib-ng"] }
|
|||
zip-extract = "0.2.1"
|
||||
tempfile = "3.12.0"
|
||||
native-dialog = "0.7.0"
|
||||
tokio-util = "0.7.12"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52.0"
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::Stdio,
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use log::{info, warn};
|
||||
use semver::Version;
|
||||
|
@ -15,7 +14,10 @@ use tauri::Manager;
|
|||
|
||||
use crate::{
|
||||
config::LauncherConfig,
|
||||
util::file::{create_dir, overwrite_dir, read_last_lines_from_file},
|
||||
util::{
|
||||
file::{create_dir, overwrite_dir},
|
||||
process::{create_log_file, create_std_log_file, watch_process},
|
||||
},
|
||||
TAURI_APP,
|
||||
};
|
||||
|
||||
|
@ -204,31 +206,6 @@ fn get_exec_location(
|
|||
})
|
||||
}
|
||||
|
||||
fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: &str,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstallStepOutput {
|
||||
|
@ -252,18 +229,6 @@ pub async fn update_data_directory(
|
|||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_end_of_logs(app_handle: tauri::AppHandle) -> Result<String, CommandError> {
|
||||
Ok(read_last_lines_from_file(
|
||||
&app_handle
|
||||
.path_resolver()
|
||||
.app_log_dir()
|
||||
.unwrap()
|
||||
.join("extractor.log"),
|
||||
250,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn extract_and_validate_iso(
|
||||
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
|
||||
|
@ -306,23 +271,34 @@ pub async fn extract_and_validate_iso(
|
|||
args.push(game_name.clone());
|
||||
}
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), true).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
|
@ -337,8 +313,6 @@ pub async fn extract_and_validate_iso(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -346,8 +320,6 @@ pub async fn extract_and_validate_iso(
|
|||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -392,7 +364,6 @@ pub async fn run_decompiler(
|
|||
.to_string();
|
||||
}
|
||||
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", !truncate_logs)?;
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
|
||||
let mut decomp_config_overrides = vec![];
|
||||
|
@ -442,15 +413,35 @@ pub async fn run_decompiler(
|
|||
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}.log"),
|
||||
!truncate_logs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
|
||||
// Ensure all remaining data is flushed to the file
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("decompilation was successful");
|
||||
|
@ -465,8 +456,6 @@ pub async fn run_decompiler(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("decompilation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -474,8 +463,6 @@ pub async fn run_decompiler(
|
|||
}
|
||||
None => {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -519,7 +506,6 @@ pub async fn run_compiler(
|
|||
.to_string();
|
||||
}
|
||||
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", !truncate_logs)?;
|
||||
let mut args = vec![
|
||||
source_path,
|
||||
"--compile".to_string(),
|
||||
|
@ -537,15 +523,25 @@ pub async fn run_compiler(
|
|||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone().unwrap())
|
||||
.stderr(log_file)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}.log"),
|
||||
!truncate_logs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("compilation was successful");
|
||||
|
@ -560,8 +556,6 @@ pub async fn run_compiler(
|
|||
};
|
||||
let message = error_code_map.get(&code).unwrap_or(&default_error);
|
||||
log::error!("compilation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(message.msg.clone()),
|
||||
|
@ -569,8 +563,6 @@ pub async fn run_compiler(
|
|||
}
|
||||
None => {
|
||||
log::error!("compilation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -657,7 +649,7 @@ pub async fn run_game_gpu_test(
|
|||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
let output = command.output().await?;
|
||||
match output.status.code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
|
@ -782,10 +774,9 @@ pub async fn launch_game(
|
|||
args
|
||||
);
|
||||
|
||||
let log_file = create_log_file(&app_handle, "game.log", false)?;
|
||||
let log_file = create_std_log_file(&app_handle, format!("game-{game_name}.log"), false)?;
|
||||
|
||||
// TODO - log rotation here would be nice too, and for it to be game specific
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
let mut command = std::process::Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.stdout(log_file.try_clone().unwrap())
|
||||
|
@ -793,7 +784,7 @@ pub async fn launch_game(
|
|||
.current_dir(exec_info.executable_dir);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
std::os::windows::process::CommandExt::creation_flags(&mut command, 0x08000000);
|
||||
}
|
||||
// Start the process here so if there is an error, we can return immediately
|
||||
let mut child = command.spawn()?;
|
||||
|
|
|
@ -3,10 +3,11 @@ use std::os::windows::process::CommandExt;
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
use crate::{
|
||||
commands::{binaries::InstallStepOutput, CommandError},
|
||||
|
@ -14,6 +15,7 @@ use crate::{
|
|||
util::{
|
||||
file::{create_dir, delete_dir, to_image_base64},
|
||||
network::download_file,
|
||||
process::{create_log_file, create_std_log_file, watch_process},
|
||||
tar::{extract_and_delete_tar_ball, extract_tar_ball},
|
||||
zip::{extract_and_delete_zip_file, extract_zip_file},
|
||||
},
|
||||
|
@ -252,31 +254,6 @@ fn get_mod_exec_location(
|
|||
})
|
||||
}
|
||||
|
||||
fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: &str,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LauncherErrorCode {
|
||||
msg: String,
|
||||
|
@ -336,23 +313,37 @@ pub async fn extract_iso_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let mut log_file = create_log_file(
|
||||
&app_handle,
|
||||
format!("extractor-{game_name}-{mod_name}.log"),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
|
@ -365,8 +356,6 @@ pub async fn extract_iso_for_mod_install(
|
|||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
|
@ -374,8 +363,6 @@ pub async fn extract_iso_for_mod_install(
|
|||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -434,26 +421,38 @@ pub async fn decompile_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), false).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
|
||||
// Ensure all remaining data is flushed to the file
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
log::info!("decompilation was successful");
|
||||
return Ok(InstallStepOutput {
|
||||
success: true,
|
||||
msg: None,
|
||||
|
@ -462,18 +461,14 @@ pub async fn decompile_for_mod_install(
|
|||
let default_error = LauncherErrorCode {
|
||||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("decompilation was not successful. Code {code}");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("decompilation was not successful. No status code");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -532,26 +527,36 @@ pub async fn compile_for_mod_install(
|
|||
game_name.clone(),
|
||||
];
|
||||
|
||||
// This is the first install step, reset the file
|
||||
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
|
||||
|
||||
log::info!("Running extractor with args: {:?}", args);
|
||||
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
.current_dir(exec_info.executable_dir)
|
||||
.stdout(log_file.try_clone()?)
|
||||
.stderr(log_file.try_clone()?);
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
command.creation_flags(0x08000000);
|
||||
}
|
||||
let output = command.output()?;
|
||||
match output.status.code() {
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let mut log_file =
|
||||
create_log_file(&app_handle, format!("extractor-{game_name}.log"), false).await?;
|
||||
|
||||
let process_status = watch_process(&mut log_file, &mut child, &app_handle).await?;
|
||||
log_file.flush().await?;
|
||||
if process_status.is_none() {
|
||||
log::error!("compilation was not successful. No status code");
|
||||
return Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
});
|
||||
}
|
||||
match process_status.unwrap().code() {
|
||||
Some(code) => {
|
||||
if code == 0 {
|
||||
log::info!("extraction and validation was successful");
|
||||
log::info!("compilation was successful");
|
||||
return Ok(InstallStepOutput {
|
||||
success: true,
|
||||
msg: None,
|
||||
|
@ -560,18 +565,14 @@ pub async fn compile_for_mod_install(
|
|||
let default_error = LauncherErrorCode {
|
||||
msg: format!("Unexpected error occured with code {code}"),
|
||||
};
|
||||
log::error!("extraction and validation was not successful. Code {code}");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("compilation was not successful. Code {code}");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some(default_error.msg.clone()),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
log::error!("extraction and validation was not successful. No status code");
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("compilation was not successful. No status code");
|
||||
Ok(InstallStepOutput {
|
||||
success: false,
|
||||
msg: Some("Unexpected error occurred".to_owned()),
|
||||
|
@ -686,13 +687,17 @@ pub async fn launch_mod(
|
|||
&mod_name,
|
||||
&source_name,
|
||||
)?;
|
||||
let args = generate_launch_mod_args(game_name, in_debug, config_dir, false)?;
|
||||
let args = generate_launch_mod_args(game_name.clone(), in_debug, config_dir, false)?;
|
||||
|
||||
log::info!("Launching gk args: {:?}", args);
|
||||
|
||||
let log_file = create_log_file(&app_handle, "mod.log", false)?;
|
||||
let log_file = create_std_log_file(
|
||||
&app_handle,
|
||||
format!("game-{game_name}-{mod_name}.log"),
|
||||
false,
|
||||
)?;
|
||||
|
||||
// TODO - log rotation here would be nice too, and for it to be game/mod specific
|
||||
// TODO - log rotation here would be nice too
|
||||
let mut command = Command::new(exec_info.executable_path);
|
||||
command
|
||||
.args(args)
|
||||
|
|
|
@ -153,7 +153,6 @@ fn main() {
|
|||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::binaries::extract_and_validate_iso,
|
||||
commands::binaries::get_end_of_logs,
|
||||
commands::binaries::get_launch_game_string,
|
||||
commands::binaries::launch_game,
|
||||
commands::binaries::open_repl,
|
||||
|
|
|
@ -2,5 +2,6 @@ pub mod file;
|
|||
pub mod game_milestones;
|
||||
pub mod network;
|
||||
pub mod os;
|
||||
pub mod process;
|
||||
pub mod tar;
|
||||
pub mod zip;
|
||||
|
|
|
@ -47,24 +47,6 @@ pub fn read_lines_in_file(path: &PathBuf) -> Result<String, Box<dyn std::error::
|
|||
Ok(std::fs::read_to_string(path)?)
|
||||
}
|
||||
|
||||
pub fn read_last_lines_from_file(path: &PathBuf, lines: usize) -> Result<String, std::io::Error> {
|
||||
if !path.exists() {
|
||||
return Ok("".to_owned());
|
||||
}
|
||||
let buf = rev_buf_reader::RevBufReader::new(std::fs::File::open(path)?);
|
||||
Ok(
|
||||
buf
|
||||
.lines()
|
||||
.take(lines)
|
||||
.map(|l| l.unwrap_or("".to_owned()))
|
||||
.collect::<Vec<String>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn touch_file(path: &PathBuf) -> std::io::Result<()> {
|
||||
match std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
|
|
119
src-tauri/src/util/process.rs
Normal file
119
src-tauri/src/util/process.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use std::{process::ExitStatus, sync::Arc, time::Duration};
|
||||
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use crate::commands::CommandError;
|
||||
|
||||
use super::file::create_dir;
|
||||
use tauri::Manager;
|
||||
|
||||
pub async fn create_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: String,
|
||||
append: bool,
|
||||
) -> Result<tokio::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = tokio::fs::OpenOptions::new();
|
||||
file_options.read(true);
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name)).await?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct LogPayload {
|
||||
logs: String,
|
||||
}
|
||||
|
||||
pub async fn watch_process(
|
||||
log_file: &mut tokio::fs::File,
|
||||
child: &mut tokio::process::Child,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> Result<Option<ExitStatus>, CommandError> {
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
let mut stdout_reader = tokio::io::BufReader::new(stdout).lines();
|
||||
let mut stderr_reader = tokio::io::BufReader::new(stderr).lines();
|
||||
let combined_buffer = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(25));
|
||||
|
||||
let mut process_status = None;
|
||||
loop {
|
||||
let buffer_clone = Arc::clone(&combined_buffer);
|
||||
tokio::select! {
|
||||
Ok(Some(line)) = stdout_reader.next_line() => {
|
||||
let formatted_line = format!("{line}\n");
|
||||
log_file.write_all(formatted_line.as_bytes()).await?;
|
||||
if formatted_line != "\n" {
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
buf.push_str(&formatted_line);
|
||||
}
|
||||
},
|
||||
Ok(Some(line)) = stderr_reader.next_line() => {
|
||||
let formatted_line = format!("{line}\n");
|
||||
log_file.write_all(formatted_line.as_bytes()).await?;
|
||||
if formatted_line != "\n" {
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
buf.push_str(&formatted_line);
|
||||
}
|
||||
},
|
||||
_ = interval.tick() => {
|
||||
log_file.flush().await?;
|
||||
{
|
||||
let mut buf = buffer_clone.lock().await;
|
||||
let _ = app_handle.emit_all("log_update", LogPayload { logs: buf.clone() });
|
||||
buf.clear();
|
||||
}
|
||||
},
|
||||
// Wait for the child process to finish
|
||||
status = child.wait() => {
|
||||
process_status = Some(status?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(process_status);
|
||||
}
|
||||
|
||||
pub fn create_std_log_file(
|
||||
app_handle: &tauri::AppHandle,
|
||||
name: String,
|
||||
append: bool,
|
||||
) -> Result<std::fs::File, CommandError> {
|
||||
let log_path = &match app_handle.path_resolver().app_log_dir() {
|
||||
None => {
|
||||
return Err(CommandError::Installation(
|
||||
"Could not determine path to save installation logs".to_owned(),
|
||||
))
|
||||
}
|
||||
Some(path) => path,
|
||||
};
|
||||
create_dir(log_path)?;
|
||||
let mut file_options = std::fs::OpenOptions::new();
|
||||
file_options.create(true);
|
||||
if append {
|
||||
file_options.append(true);
|
||||
} else {
|
||||
file_options.write(true).truncate(true);
|
||||
}
|
||||
let file = file_options.open(log_path.join(name))?;
|
||||
Ok(file)
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
import type { Job } from "$lib/utils/jobs";
|
||||
import { getInternalName, type SupportedGame } from "$lib/constants";
|
||||
import {
|
||||
getEndOfLogs,
|
||||
runCompiler,
|
||||
runDecompiler,
|
||||
updateDataDirectory,
|
||||
|
@ -60,7 +59,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -84,7 +82,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await runCompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -116,7 +113,6 @@
|
|||
]);
|
||||
progressTracker.start();
|
||||
let resp = await updateDataDirectory(getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -124,7 +120,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -132,7 +127,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runCompiler("", getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -198,7 +192,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler("", getInternalName(activeGame), true);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -248,7 +241,6 @@
|
|||
modSourceName,
|
||||
sourcePath,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -266,7 +258,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -278,7 +269,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -338,7 +328,6 @@
|
|||
modSourceName,
|
||||
sourcePath,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -368,7 +357,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -380,7 +368,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -420,7 +407,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -449,8 +435,6 @@
|
|||
modName,
|
||||
modSourceName,
|
||||
);
|
||||
// TODO - stream logs
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -494,9 +478,7 @@
|
|||
|
||||
<div class="flex flex-col justify-content">
|
||||
<Progress />
|
||||
{#if $progressTracker.logs !== undefined}
|
||||
<LogViewer />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $progressTracker.overallStatus === "success"}
|
||||
<div class="flex flex-col justify-end items-end mt-auto">
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { Alert, Button } from "flowbite-svelte";
|
||||
import {
|
||||
extractAndValidateISO,
|
||||
getEndOfLogs,
|
||||
runCompiler,
|
||||
runDecompiler,
|
||||
} from "$lib/rpc/binaries";
|
||||
|
@ -92,7 +91,6 @@
|
|||
sourcePath,
|
||||
getInternalName(activeGame),
|
||||
);
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -100,7 +98,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runDecompiler(sourcePath, getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -108,7 +105,6 @@
|
|||
}
|
||||
progressTracker.proceed();
|
||||
resp = await runCompiler(sourcePath, getInternalName(activeGame));
|
||||
progressTracker.updateLogs(await getEndOfLogs());
|
||||
if (!resp.success) {
|
||||
progressTracker.halt();
|
||||
installationError = resp.msg;
|
||||
|
@ -129,11 +125,9 @@
|
|||
{#if !requirementsMet}
|
||||
<Requirements {activeGame} on:recheckRequirements={checkRequirements} />
|
||||
{:else if installing}
|
||||
<div class="flex flex-col justify-content">
|
||||
<div class="flex flex-col justify-content shrink">
|
||||
<Progress />
|
||||
{#if $progressTracker.logs !== undefined}
|
||||
<LogViewer />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $progressTracker.overallStatus === "success"}
|
||||
<div class="flex flex-col justify-end items-end mt-auto">
|
||||
|
|
|
@ -1,31 +1,40 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { progressTracker } from "$lib/stores/ProgressStore";
|
||||
import IconDocument from "~icons/mdi/file-document-outline";
|
||||
import { Accordion, AccordionItem } from "flowbite-svelte";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { ansiSpan } from "ansi-to-span";
|
||||
import escapeHtml from "escape-html";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let logListener: any = undefined;
|
||||
let logElement;
|
||||
|
||||
const scrollToBottom = async (node) => {
|
||||
node.scroll({ top: node.scrollHeight, behavior: "instant" });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
logListener = await listen("log_update", (event) => {
|
||||
progressTracker.appendLogs(event.payload.logs);
|
||||
if (logElement) {
|
||||
scrollToBottom(logElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (logListener !== undefined) {
|
||||
logListener();
|
||||
}
|
||||
});
|
||||
|
||||
function convertLogColors(text) {
|
||||
return ansiSpan(escapeHtml(text)).replaceAll("\n", "<br/>");
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion class="log-accordian p-0 mb-2">
|
||||
<AccordionItem class="bg-slate-900 rounded p-[1rem]">
|
||||
<span slot="header" class="text-sm font-semibold text-white flex gap-2">
|
||||
<IconDocument />
|
||||
<span>{$_("setup_logs_header")}</span>
|
||||
</span>
|
||||
<div
|
||||
slot="default"
|
||||
class="bg-slate-900 px-4 max-h-52 overflow-y-scroll scrollbar"
|
||||
>
|
||||
<p class="py-4 text-clip overflow-hidden font-mono log-output">
|
||||
...{$_("setup_logs_truncation")}:
|
||||
<br />
|
||||
{@html convertLogColors($progressTracker.logs)}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
{#if $progressTracker.logs}
|
||||
<pre
|
||||
class="rounded p-2 bg-[#141414] text-[11px] max-h-[300px] overflow-auto text-pretty font-mono"
|
||||
bind:this={logElement}>{@html convertLogColors($progressTracker.logs)}</pre>
|
||||
{/if}
|
||||
|
|
|
@ -60,3 +60,7 @@
|
|||
.font-mono {
|
||||
font-family: "Noto Sans Mono", monospace !important;
|
||||
}
|
||||
|
||||
.font-mono span {
|
||||
font-family: "Noto Sans Mono", monospace !important;
|
||||
}
|
||||
|
|
|
@ -17,10 +17,6 @@ export async function updateDataDirectory(
|
|||
);
|
||||
}
|
||||
|
||||
export async function getEndOfLogs(): Promise<string> {
|
||||
return await invoke_rpc("get_end_of_logs", {}, () => "");
|
||||
}
|
||||
|
||||
export async function extractAndValidateISO(
|
||||
pathToIso: string,
|
||||
gameName: string,
|
||||
|
|
|
@ -16,7 +16,7 @@ interface ProgressTracker {
|
|||
currentStep: number;
|
||||
overallStatus: ProgressStatus;
|
||||
steps: ProgressStep[];
|
||||
logs: string;
|
||||
logs: string | undefined;
|
||||
}
|
||||
|
||||
const storeValue: ProgressTracker = {
|
||||
|
@ -66,9 +66,12 @@ function createProgressTracker() {
|
|||
val.steps[val.currentStep].status = "failed";
|
||||
return val;
|
||||
}),
|
||||
updateLogs: (logs: string) =>
|
||||
appendLogs: (logs: string) =>
|
||||
update((val) => {
|
||||
val.logs = logs;
|
||||
if (val.logs === undefined) {
|
||||
val.logs = "";
|
||||
}
|
||||
val.logs += logs;
|
||||
return val;
|
||||
}),
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue