UX: Stream logs to the frontend during installation process instead of only updating after each step. (#565)

This commit is contained in:
Tyler Wilding 2024-09-22 13:45:47 -04:00 committed by GitHub
parent 7b9ade5def
commit 135f200f4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 305 additions and 218 deletions

1
src-tauri/Cargo.lock generated
View file

@ -3193,6 +3193,7 @@ dependencies = [
"tempfile",
"thiserror",
"tokio",
"tokio-util",
"ts-rs",
"walkdir",
"wgpu",

View file

@ -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"

View file

@ -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()?;

View file

@ -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)

View file

@ -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,

View file

@ -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;

View file

@ -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)

View 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)
}

View 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}
<LogViewer />
</div>
{#if $progressTracker.overallStatus === "success"}
<div class="flex flex-col justify-end items-end mt-auto">

View file

@ -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}
<LogViewer />
</div>
{#if $progressTracker.overallStatus === "success"}
<div class="flex flex-col justify-end items-end mt-auto">

View file

@ -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}

View file

@ -60,3 +60,7 @@
.font-mono {
font-family: "Noto Sans Mono", monospace !important;
}
.font-mono span {
font-family: "Noto Sans Mono", monospace !important;
}

View file

@ -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,

View file

@ -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;
}),
};