frontend/backend: propagate install logs to frontend

This commit is contained in:
Tyler Wilding 2023-03-04 16:23:21 -05:00
parent e8cdd57e8a
commit dc23e2931c
No known key found for this signature in database
GPG key ID: 77CB07796494137E
15 changed files with 245 additions and 97 deletions

View file

@ -33,20 +33,7 @@ We are using Tauri to build a native app, but still with simple Web technology.
> Additionally, this presumes your environment has WebView2 (windows) or webkit2 (linux) already available. This is a requirement for end-users as well! Many modern OSes already ship with such a thing, but it's something we'll need to investigate.
- `npm install`
- `npm run tauri dev`
- `yarn install`
- `yarn tauri dev`
This builds the app with Tauri (this is a rust compilation, the first run will take a while) and the frontend is served via Vite -- a nice web server that will hot-reload any changes as you develop.
## Release Process
```mermaid
sequenceDiagram
jak-project->>jak-project: New tag is manually cut and built
jak-project->>launcher: Repository Dispatch to start release
launcher->>launcher: Alternatively, manually triggered release here
launcher->>launcher: Build App for all supported platforms
launcher->>launcher: Publish release and update latest release metadata file in repo
website->>GitHub API: Website will display latest release
app->>launcher: Detect new version and will prompt the user to update
```
This builds the app with Tauri (this is a rust compilation, the first run will take a while) and the frontend is served via Vite -- a fast alternative to webpack that offers HMR.

View file

@ -27,8 +27,10 @@
"@tauri-apps/cli": "^1.2.3",
"@tauri-apps/tauricon": "github:tauri-apps/tauricon",
"@tsconfig/svelte": "^3.0.0",
"ansi-to-span": "^0.0.1",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
"escape-html": "^1.0.3",
"execa": "^7.0.0",
"flowbite": "^1.6.3",
"flowbite-svelte": "^0.29.7",

View file

@ -38,5 +38,3 @@ if (existsSync(`src-tauri/bin/glewinfo${extension}`)) {
);
}
}

View file

@ -1,16 +1,18 @@
use std::{
collections::HashMap,
io::BufRead,
path::{Path, PathBuf},
process::Command,
process::{Command, ExitStatus},
};
use log::info;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::Manager;
use crate::{
config::LauncherConfig,
util::file::{create_dir, overwrite_dir},
util::file::{create_dir, overwrite_dir, read_lines_in_file},
};
use super::CommandError;
@ -62,39 +64,6 @@ fn common_prelude(
})
}
#[tauri::command]
pub async fn update_data_directory(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<(), CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let src_dir = config_info
.install_path
.join("versions")
.join(config_info.active_version_folder)
.join(config_info.active_version)
.join("data");
let dst_dir = config_info
.install_path
.join("active")
.join(game_name)
.join("data");
info!("Copying {} into {}", src_dir.display(), dst_dir.display());
overwrite_dir(&src_dir, &dst_dir).map_err(|err| {
CommandError::Installation(format!(
"Unable to copy data directory: '{}'",
err.to_string()
))
})?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
struct LauncherErrorCode {
msg: String,
@ -210,13 +179,61 @@ fn create_log_file(
Ok(file)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallStepOutput {
pub success: bool,
pub msg: Option<String>,
}
#[tauri::command]
pub async fn update_data_directory(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
game_name: String,
) -> Result<InstallStepOutput, CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
let src_dir = config_info
.install_path
.join("versions")
.join(config_info.active_version_folder)
.join(config_info.active_version)
.join("data");
let dst_dir = config_info
.install_path
.join("active")
.join(game_name)
.join("data");
info!("Copying {} into {}", src_dir.display(), dst_dir.display());
overwrite_dir(&src_dir, &dst_dir).map_err(|err| {
CommandError::Installation(format!(
"Unable to copy data directory: '{}'",
err.to_string()
))
})?;
Ok(InstallStepOutput {
success: true,
msg: None,
})
}
#[derive(Clone, serde::Serialize)]
struct LogPayload {
stdout: String,
}
#[tauri::command]
pub async fn extract_and_validate_iso(
config: tauri::State<'_, tokio::sync::Mutex<LauncherConfig>>,
app_handle: tauri::AppHandle,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
) -> Result<InstallStepOutput, CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
@ -237,14 +254,44 @@ pub async fn extract_and_validate_iso(
// This is the first install step, reset the file
let log_file = create_log_file(&app_handle, "extractor.log", false)?;
// TODO - exit codes
let output = Command::new(exec_info.executable_path)
.args(args)
.current_dir(exec_info.executable_dir)
.stdout(log_file.try_clone().unwrap())
.stderr(log_file)
.stderr(log_file.try_clone().unwrap())
.output()?;
Ok(())
// TODO - we should instead capture the logs while simulanteously streaming them to a file
// right now we aren't so I just read the file after it's done
app_handle.emit_all(
"updateJobLogs",
LogPayload {
stdout: read_lines_in_file(
&app_handle
.path_resolver()
.app_log_dir()
.unwrap()
.join("extractor.log"),
)
.expect("TODO"),
},
)?;
match output.status.code() {
Some(code) => {
let error_code_map = get_error_codes(&config_info);
let default_error = LauncherErrorCode {
msg: format!("Unexpected error occured with code {}", code).to_owned(),
};
let message = error_code_map.get(&code).unwrap_or(&default_error);
Ok(InstallStepOutput {
success: false,
msg: Some(message.msg.clone()),
})
}
None => Ok(InstallStepOutput {
success: true,
msg: None,
}),
}
}
#[tauri::command]
@ -253,7 +300,7 @@ pub async fn run_decompiler(
app_handle: tauri::AppHandle,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
) -> Result<InstallStepOutput, CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
@ -269,7 +316,6 @@ pub async fn run_decompiler(
.to_string();
}
// TODO - handle error codes
let log_file = create_log_file(&app_handle, "extractor.log", true)?;
let output = Command::new(&exec_info.executable_path)
.args([
@ -282,7 +328,38 @@ pub async fn run_decompiler(
.stderr(log_file)
.current_dir(exec_info.executable_dir)
.output()?;
Ok(())
// TODO - we should instead capture the logs while simulanteously streaming them to a file
// right now we aren't so I just read the file after it's done
app_handle.emit_all(
"updateJobLogs",
LogPayload {
stdout: read_lines_in_file(
&app_handle
.path_resolver()
.app_log_dir()
.unwrap()
.join("extractor.log"),
)
.expect("TODO"),
},
)?;
match output.status.code() {
Some(code) => {
let error_code_map = get_error_codes(&config_info);
let default_error = LauncherErrorCode {
msg: format!("Unexpected error occured with code {}", code).to_owned(),
};
let message = error_code_map.get(&code).unwrap_or(&default_error);
Ok(InstallStepOutput {
success: false,
msg: Some(message.msg.clone()),
})
}
None => Ok(InstallStepOutput {
success: true,
msg: None,
}),
}
}
#[tauri::command]
@ -291,7 +368,7 @@ pub async fn run_compiler(
app_handle: tauri::AppHandle,
path_to_iso: String,
game_name: String,
) -> Result<(), CommandError> {
) -> Result<InstallStepOutput, CommandError> {
let config_lock = config.lock().await;
let config_info = common_prelude(&config_lock)?;
@ -307,7 +384,6 @@ pub async fn run_compiler(
.to_string();
}
// TODO - handle error codes
let log_file = create_log_file(&app_handle, "extractor.log", true)?;
let output = Command::new(&exec_info.executable_path)
.args([
@ -320,7 +396,38 @@ pub async fn run_compiler(
.stderr(log_file)
.current_dir(exec_info.executable_dir)
.output()?;
Ok(())
// TODO - we should instead capture the logs while simulanteously streaming them to a file
// right now we aren't so I just read the file after it's done
app_handle.emit_all(
"updateJobLogs",
LogPayload {
stdout: read_lines_in_file(
&app_handle
.path_resolver()
.app_log_dir()
.unwrap()
.join("extractor.log"),
)
.expect("TODO"),
},
)?;
match output.status.code() {
Some(code) => {
let error_code_map = get_error_codes(&config_info);
let default_error = LauncherErrorCode {
msg: format!("Unexpected error occured with code {}", code).to_owned(),
};
let message = error_code_map.get(&code).unwrap_or(&default_error);
Ok(InstallStepOutput {
success: false,
msg: Some(message.msg.clone()),
})
}
None => Ok(InstallStepOutput {
success: true,
msg: None,
}),
}
}
#[tauri::command]
@ -337,7 +444,6 @@ pub async fn open_repl(
let data_folder = get_data_dir(&config_info, &game_name)?;
let exec_info = get_exec_location(&config_info, "goalc")?;
// TODO - handle error codes
let output = Command::new("cmd")
.args([
"/K",
@ -371,7 +477,6 @@ pub async fn launch_game(
}
args.push("-proj-path".to_string());
args.push(data_folder.to_string_lossy().into_owned());
// TODO - handle error codes
let log_file = create_log_file(&app_handle, "game.log", false)?;
let output = Command::new(exec_info.executable_path)
.args(args)

View file

@ -50,7 +50,7 @@ pub async fn is_avx_requirement_met(
match config_lock.requirements.avx {
None => {
if is_x86_feature_detected!("avx") || is_x86_feature_detected!("avx2") {
config_lock.requirements.avx = Some(false);
config_lock.requirements.avx = Some(true);
} else {
config_lock.requirements.avx = Some(false);
}

View file

@ -92,9 +92,9 @@ fn main() {
commands::config::get_installed_version_folder,
commands::config::get_installed_version,
commands::config::is_avx_requirement_met,
commands::config::is_avx_supported,
commands::config::is_game_installed,
commands::config::is_opengl_requirement_met,
commands::config::set_opengl_requirement_met,
commands::config::save_active_version_change,
commands::config::set_install_directory,
commands::game::reset_game_settings,

View file

@ -32,3 +32,7 @@ pub fn overwrite_dir(src: &PathBuf, dst: &PathBuf) -> Result<(), fs_extra::error
}
Ok(())
}
pub fn read_lines_in_file(path: &PathBuf) -> Result<String, Box<dyn std::error::Error>> {
Ok(std::fs::read_to_string(path)?)
}

View file

@ -15,6 +15,7 @@
} from "$lib/rpc/extractor";
import { finalizeInstallation } from "$lib/rpc/config";
import { generateSupportPackage } from "$lib/rpc/support";
import { listen } from "@tauri-apps/api/event";
export let activeGame: SupportedGame;
export let jobType: Job;
@ -27,6 +28,11 @@
// It's used to provide almost the same interface as the normal installation, with logs, etc
// but for arbitrary jobs. Such as updating versions, decompiling, or compiling.
onMount(async () => {
const unlistenLogListener = await listen("newJobLogs", async (event) => {
console.log(event.payload);
progressTracker.updateLogs(event.payload["stdout"]);
});
if (jobType === "decompile") {
progressTracker.init([
{
@ -95,7 +101,7 @@
<div class="flex flex-col justify-content">
<Progress />
{#if $progressTracker.logs}
{#if $progressTracker.logs.length > 0}
<LogViewer />
{/if}
</div>

View file

@ -22,12 +22,13 @@
import { progressTracker } from "$lib/stores/ProgressStore";
import { generateSupportPackage } from "$lib/rpc/support";
import { isOpenGLVersionSupported } from "$lib/sidecars/glewinfo";
import { listen } from "@tauri-apps/api/event";
export let activeGame: SupportedGame;
const dispatch = createEventDispatcher();
let requirementsMet = false;
let requirementsMet = true;
let installing = false;
onMount(async () => {
@ -39,6 +40,11 @@
await setOpenGLRequirementMet(isOpenGLMet);
}
requirementsMet = isAvxMet && isOpenGLMet;
const unlistenLogListener = await listen("updateJobLogs", async (event) => {
console.log(event.payload);
progressTracker.updateLogs(event.payload["stdout"]);
});
});
async function install(viaFolder: boolean) {
@ -94,7 +100,7 @@
{:else if installing}
<div class="flex flex-col justify-content">
<Progress />
{#if $progressTracker.logs}
{#if $progressTracker.logs.length > 0}
<LogViewer />
{/if}
</div>

View file

@ -2,6 +2,12 @@
import { progressTracker } from "$lib/stores/ProgressStore";
import Icon from "@iconify/svelte";
import { Accordion, AccordionItem } from "flowbite-svelte";
import { ansiSpan } from "ansi-to-span";
import escapeHtml from "escape-html";
function convertLogColors(text) {
return ansiSpan(escapeHtml(text)).replaceAll("\n", "<br/>");
}
</script>
<Accordion class="log-accordian" defaultClass="p-0">
@ -14,8 +20,8 @@
slot="default"
class="bg-slate-900 px-4 max-h-60 overflow-y-scroll scrollbar"
>
<p class="py-4 text-clip overflow-hidden font-mono">
{$progressTracker.logs}
<p class="py-4 text-clip overflow-hidden font-mono log-output">
{@html convertLogColors($progressTracker.logs)}
</p>
</div>
</AccordionItem>

View file

@ -67,3 +67,12 @@ body {
/* TODO - no idea how else to customize the div that wraps the body slot in the accordian? */
padding: 0;
}
.log-output {
font-size: 9pt;
font-family: monospace;
}
.log-output > span {
font-family: monospace;
}

View file

@ -1 +1 @@
export type Job = "decompile" | "compile";
export type Job = "decompile" | "compile" | "updateGame";

View file

@ -3,13 +3,14 @@ import { Command } from "@tauri-apps/api/shell";
export async function isOpenGLVersionSupported(
version: string
): Promise<boolean> {
): Promise<boolean | undefined> {
if ((await os.platform()) === "darwin") {
console.log("[OG]: MacOS isn't supported, OpenGL won't work here!");
return false;
}
// Otherwise, query for the version
let command = Command.sidecar("bin/glewinfo", ["-version", version]);
try {
const output = await command.execute();
if (output.code === 0) {
return true;
@ -21,4 +22,8 @@ export async function isOpenGLVersionSupported(
stderr: output.stderr,
});
return false;
} catch (e) {
console.log("[OG] Unable to check for OpenGL support via glewinfo", e);
return undefined;
}
}

View file

@ -66,6 +66,11 @@ function createProgressTracker() {
val.steps[val.currentStep].status = "failed";
return val;
}),
updateLogs: (logs: string[]) =>
update((val) => {
val.logs = logs;
return val;
}),
};
}

View file

@ -820,6 +820,11 @@ ansi-escapes@^4.2.1:
dependencies:
type-fest "^0.21.3"
ansi-html-community@^0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@ -842,6 +847,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
ansi-to-span@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/ansi-to-span/-/ansi-to-span-0.0.1.tgz#22da2f8774862e1a632ade2d75c85ee937c2415e"
integrity sha512-XLdA+dwBbMrzZQpsJ/ZDbAxuj8g81X2G/UWztQUVGQOaEfFVDk5XhROsaPhYXMMlpJ3WXQrPJ9ecrDyZxzJaUw==
any-base@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe"
@ -1690,6 +1700,11 @@ escape-goat@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"