diff --git a/Cargo.toml b/Cargo.toml index fe2ab100..4a3a2ec8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "theseus", diff --git a/theseus/src/api/logs.rs b/theseus/src/api/logs.rs index 4d6614cd..f1a14987 100644 --- a/theseus/src/api/logs.rs +++ b/theseus/src/api/logs.rs @@ -1,23 +1,33 @@ use std::io::{Read, SeekFrom}; +use std::time::SystemTime; + +use futures::TryFutureExt; +use serde::{Deserialize, Serialize}; +use tokio::{ + fs::File, + io::{AsyncReadExt, AsyncSeekExt}, +}; use crate::{ prelude::{Credentials, DirectoryInfo}, util::io::{self, IOError}, {state::ProfilePathId, State}, }; -use futures::TryFutureExt; -use serde::Serialize; -use tokio::{ - fs::File, - io::{AsyncReadExt, AsyncSeekExt}, -}; #[derive(Serialize, Debug)] pub struct Logs { + pub log_type: LogType, pub filename: String, + pub age: u64, pub output: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)] +pub enum LogType { + InfoLog, + CrashReport, +} + #[derive(Serialize, Debug)] pub struct LatestLogCursor { pub cursor: u64, @@ -54,15 +64,29 @@ impl CensoredString { impl Logs { async fn build( + log_type: LogType, + age: SystemTime, profile_subpath: &ProfilePathId, filename: String, clear_contents: Option, ) -> crate::Result { Ok(Self { + log_type, + age: age + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|_| std::time::Duration::from_secs(0)) + .as_secs(), output: if clear_contents.unwrap_or(false) { None } else { - Some(get_output_by_filename(profile_subpath, &filename).await?) + Some( + get_output_by_filename( + profile_subpath, + log_type, + &filename, + ) + .await?, + ) }, filename, }) @@ -70,74 +94,118 @@ impl Logs { } #[tracing::instrument] -pub async fn get_logs( - profile_path: ProfilePathId, +pub async fn get_logs_from_type( + profile_path: &ProfilePathId, + log_type: LogType, clear_contents: Option, -) -> crate::Result> { - let profile_path = - if let Some(p) = crate::profile::get(&profile_path, None).await? { - p.profile_id() - } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - .into()); - }; - - let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; - let mut logs = Vec::new(); + logs: &mut Vec>, +) -> crate::Result<()> { + let logs_folder = match log_type { + LogType::InfoLog => { + DirectoryInfo::profile_logs_dir(profile_path).await? + } + LogType::CrashReport => { + DirectoryInfo::crash_reports_dir(profile_path).await? + } + }; if logs_folder.exists() { for entry in std::fs::read_dir(&logs_folder) .map_err(|e| IOError::with_path(e, &logs_folder))? { let entry: std::fs::DirEntry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?; + let age = entry.metadata()?.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH); let path = entry.path(); if !path.is_file() { continue; } if let Some(file_name) = path.file_name() { let file_name = file_name.to_string_lossy().to_string(); - logs.push( - Logs::build(&profile_path, file_name, clear_contents).await, + Logs::build( + log_type, + age, + &profile_path, + file_name, + clear_contents, + ) + .await, ); } } } + Ok(()) +} + +#[tracing::instrument] +pub async fn get_logs( + profile_path_id: ProfilePathId, + clear_contents: Option, +) -> crate::Result> { + let profile_path = profile_path_id.profile_path().await?; + + let mut logs = Vec::new(); + get_logs_from_type( + &profile_path, + LogType::InfoLog, + clear_contents, + &mut logs, + ) + .await?; + get_logs_from_type( + &profile_path, + LogType::CrashReport, + clear_contents, + &mut logs, + ) + .await?; let mut logs = logs.into_iter().collect::>>()?; - logs.sort_by_key(|x| x.filename.clone()); + logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.filename.cmp(&a.filename))); Ok(logs) } #[tracing::instrument] pub async fn get_logs_by_filename( - profile_path: ProfilePathId, + profile_path_id: ProfilePathId, + log_type: LogType, filename: String, ) -> crate::Result { - let profile_path = - if let Some(p) = crate::profile::get(&profile_path, None).await? { - p.profile_id() - } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - .into()); - }; - Ok(Logs { - output: Some(get_output_by_filename(&profile_path, &filename).await?), - filename, - }) + let profile_path = profile_path_id.profile_path().await?; + + let path = match log_type { + LogType::InfoLog => { + DirectoryInfo::profile_logs_dir(&profile_path).await + } + LogType::CrashReport => { + DirectoryInfo::crash_reports_dir(&profile_path).await + } + }? + .join(&filename); + + let metadata = std::fs::metadata(&path)?; + let age = metadata.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH); + + Logs::build(log_type, age, &profile_path, filename, Some(true)).await } #[tracing::instrument] pub async fn get_output_by_filename( profile_subpath: &ProfilePathId, + log_type: LogType, file_name: &str, ) -> crate::Result { let state = State::get().await?; - let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?; + + let logs_folder = match log_type { + LogType::InfoLog => { + DirectoryInfo::profile_logs_dir(profile_subpath).await? + } + LogType::CrashReport => { + DirectoryInfo::crash_reports_dir(profile_subpath).await? + } + }; + let path = logs_folder.join(file_name); let credentials: Vec = state @@ -168,7 +236,7 @@ pub async fn get_output_by_filename( contents = [0; 1024]; } return Ok(CensoredString::censor(result, &credentials)); - } else if ext == "log" { + } else if ext == "log" || ext == "txt" { let mut result = String::new(); let mut contents = [0; 1024]; let mut file = std::fs::File::open(&path) @@ -194,16 +262,8 @@ pub async fn get_output_by_filename( } #[tracing::instrument] -pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> { - let profile_path = - if let Some(p) = crate::profile::get(&profile_path, None).await? { - p.profile_id() - } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - .into()); - }; +pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> { + let profile_path = profile_path_id.profile_path().await?; let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; for entry in std::fs::read_dir(&logs_folder) @@ -220,20 +280,21 @@ pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> { #[tracing::instrument] pub async fn delete_logs_by_filename( - profile_path: ProfilePathId, + profile_path_id: ProfilePathId, + log_type: LogType, filename: &str, ) -> crate::Result<()> { - let profile_path = - if let Some(p) = crate::profile::get(&profile_path, None).await? { - p.profile_id() - } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - .into()); - }; + let profile_path = profile_path_id.profile_path().await?; + + let logs_folder = match log_type { + LogType::InfoLog => { + DirectoryInfo::profile_logs_dir(&profile_path).await + } + LogType::CrashReport => { + DirectoryInfo::crash_reports_dir(&profile_path).await + } + }?; - let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; let path = logs_folder.join(filename); io::remove_dir_all(&path).await?; Ok(()) @@ -249,19 +310,11 @@ pub async fn get_latest_log_cursor( #[tracing::instrument] pub async fn get_generic_live_log_cursor( - profile_path: ProfilePathId, + profile_path_id: ProfilePathId, log_file_name: &str, mut cursor: u64, // 0 to start at beginning of file ) -> crate::Result { - let profile_path = - if let Some(p) = crate::profile::get(&profile_path, None).await? { - p.profile_id() - } else { - return Err(crate::ErrorKind::UnmanagedProfileError( - profile_path.to_string(), - ) - .into()); - }; + let profile_path = profile_path_id.profile_path().await?; let state = State::get().await?; let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; diff --git a/theseus/src/state/dirs.rs b/theseus/src/state/dirs.rs index 365638cf..add3b67a 100644 --- a/theseus/src/state/dirs.rs +++ b/theseus/src/state/dirs.rs @@ -165,6 +165,14 @@ impl DirectoryInfo { ) -> crate::Result { Ok(profile_id.get_full_path().await?.join("logs")) } + + /// Gets the crash reports dir for a given profile + #[inline] + pub async fn crash_reports_dir( + profile_id: &ProfilePathId, + ) -> crate::Result { + Ok(profile_id.get_full_path().await?.join("crash-reports")) + } #[inline] pub fn launcher_logs_dir() -> Option { diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index b62f9854..587ceeab 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -88,6 +88,15 @@ impl ProfilePathId { .ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?; Ok(self) } + + pub async fn profile_path(&self) -> crate::Result { + if let Some(p) = crate::profile::get(&self, None).await? { + Ok(p.profile_id()) + } else { + Err(crate::ErrorKind::UnmanagedProfileError(self.to_string()) + .into()) + } + } } impl std::fmt::Display for ProfilePathId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/theseus_gui/src-tauri/src/api/logs.rs b/theseus_gui/src-tauri/src/api/logs.rs index d328aac4..e22a17aa 100644 --- a/theseus_gui/src-tauri/src/api/logs.rs +++ b/theseus_gui/src-tauri/src/api/logs.rs @@ -3,6 +3,7 @@ use theseus::{ logs::{self, CensoredString, LatestLogCursor, Logs}, prelude::ProfilePathId, }; +use theseus::logs::LogType; /* A log is a struct containing the filename string, stdout, and stderr, as follows: @@ -42,15 +43,17 @@ pub async fn logs_get_logs( #[tauri::command] pub async fn logs_get_logs_by_filename( profile_path: ProfilePathId, + log_type: LogType, filename: String, ) -> Result { - Ok(logs::get_logs_by_filename(profile_path, filename).await?) + Ok(logs::get_logs_by_filename(profile_path, log_type, filename).await?) } /// Get the stdout for a profile by profile id and filename string #[tauri::command] pub async fn logs_get_output_by_filename( profile_path: ProfilePathId, + log_type: LogType, filename: String, ) -> Result { let profile_path = if let Some(p) = @@ -64,7 +67,7 @@ pub async fn logs_get_output_by_filename( .into()); }; - Ok(logs::get_output_by_filename(&profile_path, &filename).await?) + Ok(logs::get_output_by_filename(&profile_path, log_type, &filename).await?) } /// Delete all logs for a profile by profile id @@ -77,9 +80,10 @@ pub async fn logs_delete_logs(profile_path: ProfilePathId) -> Result<()> { #[tauri::command] pub async fn logs_delete_logs_by_filename( profile_path: ProfilePathId, + log_type: LogType, filename: String, ) -> Result<()> { - Ok(logs::delete_logs_by_filename(profile_path, &filename).await?) + Ok(logs::delete_logs_by_filename(profile_path, log_type, &filename).await?) } /// Get live log from a cursor diff --git a/theseus_gui/src/helpers/logs.js b/theseus_gui/src/helpers/logs.js index cd9fa70a..5694d6a0 100644 --- a/theseus_gui/src/helpers/logs.js +++ b/theseus_gui/src/helpers/logs.js @@ -22,18 +22,22 @@ export async function get_logs(profilePath, clearContents) { } /// Get a profile's log by filename -export async function get_logs_by_filename(profilePath, filename) { - return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, filename }) +export async function get_logs_by_filename(profilePath, logType, filename) { + return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename }) } /// Get a profile's log text only by filename -export async function get_output_by_filename(profilePath, filename) { - return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, filename }) +export async function get_output_by_filename(profilePath, logType, filename) { + return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, logType, filename }) } /// Delete a profile's log by filename -export async function delete_logs_by_filename(profilePath, filename) { - return await invoke('plugin:logs|logs_delete_logs_by_filename', { profilePath, filename }) +export async function delete_logs_by_filename(profilePath, logType, filename) { + return await invoke('plugin:logs|logs_delete_logs_by_filename', { + profilePath, + logType, + filename, + }) } /// Delete all logs for a given profile @@ -50,6 +54,7 @@ export async function delete_logs(profilePath) { new_file: bool <- the cursor was too far, meaning that the file was likely rotated/reset. This signals to the frontend to clear the log and start over with this struct. } */ + // From latest.log directly export async function get_latest_log_cursor(profilePath, cursor) { return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor }) diff --git a/theseus_gui/src/pages/instance/Logs.vue b/theseus_gui/src/pages/instance/Logs.vue index 0036e8fb..a92e646a 100644 --- a/theseus_gui/src/pages/instance/Logs.vue +++ b/theseus_gui/src/pages/instance/Logs.vue @@ -54,8 +54,8 @@ v-model="levelFilters[level.toLowerCase()]" class="filter-checkbox" > - {{ level }} + {{ level }} +
@@ -225,7 +225,7 @@ async function getLiveStdLog() { } else { const logCursor = await get_latest_log_cursor( props.instance.path, - currentLiveLogCursor.value, + currentLiveLogCursor.value ).catch(handleError) if (logCursor.new_file) { currentLiveLog.value = '' @@ -241,14 +241,13 @@ async function getLiveStdLog() { async function getLogs() { return (await get_logs(props.instance.path, true).catch(handleError)) - .reverse() .filter( // filter out latest_stdout.log or anything without .log in it (log) => log.filename !== 'latest_stdout.log' && log.filename !== 'latest_stdout' && log.stdout !== '' && - log.filename.includes('.log'), + (log.filename.includes('.log') || log.filename.endsWith('.txt')) ) .map((log) => { log.name = log.filename || 'Unknown' @@ -291,7 +290,8 @@ watch(selectedLogIndex, async (newIndex) => { logs.value[newIndex].stdout = 'Loading...' logs.value[newIndex].stdout = await get_output_by_filename( props.instance.path, - logs.value[newIndex].filename, + logs.value[newIndex].log_type, + logs.value[newIndex].filename ).catch(handleError) } }) @@ -306,9 +306,11 @@ const deleteLog = async () => { if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) { let deleteIndex = selectedLogIndex.value selectedLogIndex.value = deleteIndex - 1 - await delete_logs_by_filename(props.instance.path, logs.value[deleteIndex].filename).catch( - handleError, - ) + await delete_logs_by_filename( + props.instance.path, + logs.value[deleteIndex].log_type, + logs.value[deleteIndex].filename + ).catch(handleError) await setLogs() } } @@ -512,6 +514,7 @@ onUnmounted(() => { justify-self: center; } } + .filter-group { display: flex; padding: 0.6rem;