Include crash reports and attempt to order by age. (#1178)

* Include crash reports and attempt to order by age.

* Do all sorting within rust.

* Remove excess debug.

* Remove new once_cell dep.

* Use EPOCH as fallback instead of now()

* Fix prettier lint warnings.
This commit is contained in:
Corey Shupe
2024-05-09 13:29:19 -04:00
committed by GitHub
parent 53007465cd
commit a4f133eb46
7 changed files with 173 additions and 90 deletions

View File

@@ -1,4 +1,5 @@
[workspace] [workspace]
resolver = "2"
members = [ members = [
"theseus", "theseus",

View File

@@ -1,23 +1,33 @@
use std::io::{Read, SeekFrom}; 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::{ use crate::{
prelude::{Credentials, DirectoryInfo}, prelude::{Credentials, DirectoryInfo},
util::io::{self, IOError}, util::io::{self, IOError},
{state::ProfilePathId, State}, {state::ProfilePathId, State},
}; };
use futures::TryFutureExt;
use serde::Serialize;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt},
};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct Logs { pub struct Logs {
pub log_type: LogType,
pub filename: String, pub filename: String,
pub age: u64,
pub output: Option<CensoredString>, pub output: Option<CensoredString>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum LogType {
InfoLog,
CrashReport,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct LatestLogCursor { pub struct LatestLogCursor {
pub cursor: u64, pub cursor: u64,
@@ -54,15 +64,29 @@ impl CensoredString {
impl Logs { impl Logs {
async fn build( async fn build(
log_type: LogType,
age: SystemTime,
profile_subpath: &ProfilePathId, profile_subpath: &ProfilePathId,
filename: String, filename: String,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
Ok(Self { 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) { output: if clear_contents.unwrap_or(false) {
None None
} else { } else {
Some(get_output_by_filename(profile_subpath, &filename).await?) Some(
get_output_by_filename(
profile_subpath,
log_type,
&filename,
)
.await?,
)
}, },
filename, filename,
}) })
@@ -70,74 +94,118 @@ impl Logs {
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn get_logs( pub async fn get_logs_from_type(
profile_path: ProfilePathId, profile_path: &ProfilePathId,
log_type: LogType,
clear_contents: Option<bool>, clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> { logs: &mut Vec<crate::Result<Logs>>,
let profile_path = ) -> crate::Result<()> {
if let Some(p) = crate::profile::get(&profile_path, None).await? { let logs_folder = match log_type {
p.profile_id() LogType::InfoLog => {
} else { DirectoryInfo::profile_logs_dir(profile_path).await?
return Err(crate::ErrorKind::UnmanagedProfileError( }
profile_path.to_string(), LogType::CrashReport => {
) DirectoryInfo::crash_reports_dir(profile_path).await?
.into()); }
}; };
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let mut logs = Vec::new();
if logs_folder.exists() { if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder) for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))? .map_err(|e| IOError::with_path(e, &logs_folder))?
{ {
let entry: std::fs::DirEntry = let entry: std::fs::DirEntry =
entry.map_err(|e| IOError::with_path(e, &logs_folder))?; 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(); let path = entry.path();
if !path.is_file() { if !path.is_file() {
continue; continue;
} }
if let Some(file_name) = path.file_name() { if let Some(file_name) = path.file_name() {
let file_name = file_name.to_string_lossy().to_string(); let file_name = file_name.to_string_lossy().to_string();
logs.push( 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<bool>,
) -> crate::Result<Vec<Logs>> {
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::<crate::Result<Vec<Logs>>>()?; let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
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) Ok(logs)
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn get_logs_by_filename( pub async fn get_logs_by_filename(
profile_path: ProfilePathId, profile_path_id: ProfilePathId,
log_type: LogType,
filename: String, filename: String,
) -> crate::Result<Logs> { ) -> crate::Result<Logs> {
let profile_path = let profile_path = profile_path_id.profile_path().await?;
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id() let path = match log_type {
} else { LogType::InfoLog => {
return Err(crate::ErrorKind::UnmanagedProfileError( DirectoryInfo::profile_logs_dir(&profile_path).await
profile_path.to_string(), }
) LogType::CrashReport => {
.into()); DirectoryInfo::crash_reports_dir(&profile_path).await
}; }
Ok(Logs { }?
output: Some(get_output_by_filename(&profile_path, &filename).await?), .join(&filename);
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] #[tracing::instrument]
pub async fn get_output_by_filename( pub async fn get_output_by_filename(
profile_subpath: &ProfilePathId, profile_subpath: &ProfilePathId,
log_type: LogType,
file_name: &str, file_name: &str,
) -> crate::Result<CensoredString> { ) -> crate::Result<CensoredString> {
let state = State::get().await?; 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 path = logs_folder.join(file_name);
let credentials: Vec<Credentials> = state let credentials: Vec<Credentials> = state
@@ -168,7 +236,7 @@ pub async fn get_output_by_filename(
contents = [0; 1024]; contents = [0; 1024];
} }
return Ok(CensoredString::censor(result, &credentials)); return Ok(CensoredString::censor(result, &credentials));
} else if ext == "log" { } else if ext == "log" || ext == "txt" {
let mut result = String::new(); let mut result = String::new();
let mut contents = [0; 1024]; let mut contents = [0; 1024];
let mut file = std::fs::File::open(&path) let mut file = std::fs::File::open(&path)
@@ -194,16 +262,8 @@ pub async fn get_output_by_filename(
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> { pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
let profile_path = let profile_path = profile_path_id.profile_path().await?;
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 logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
for entry in std::fs::read_dir(&logs_folder) 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] #[tracing::instrument]
pub async fn delete_logs_by_filename( pub async fn delete_logs_by_filename(
profile_path: ProfilePathId, profile_path_id: ProfilePathId,
log_type: LogType,
filename: &str, filename: &str,
) -> crate::Result<()> { ) -> crate::Result<()> {
let profile_path = let profile_path = profile_path_id.profile_path().await?;
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id() let logs_folder = match log_type {
} else { LogType::InfoLog => {
return Err(crate::ErrorKind::UnmanagedProfileError( DirectoryInfo::profile_logs_dir(&profile_path).await
profile_path.to_string(), }
) LogType::CrashReport => {
.into()); DirectoryInfo::crash_reports_dir(&profile_path).await
}; }
}?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(filename); let path = logs_folder.join(filename);
io::remove_dir_all(&path).await?; io::remove_dir_all(&path).await?;
Ok(()) Ok(())
@@ -249,19 +310,11 @@ pub async fn get_latest_log_cursor(
#[tracing::instrument] #[tracing::instrument]
pub async fn get_generic_live_log_cursor( pub async fn get_generic_live_log_cursor(
profile_path: ProfilePathId, profile_path_id: ProfilePathId,
log_file_name: &str, log_file_name: &str,
mut cursor: u64, // 0 to start at beginning of file mut cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> { ) -> crate::Result<LatestLogCursor> {
let profile_path = let profile_path = profile_path_id.profile_path().await?;
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 state = State::get().await?; let state = State::get().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?; let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;

View File

@@ -165,6 +165,14 @@ impl DirectoryInfo {
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
Ok(profile_id.get_full_path().await?.join("logs")) 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<PathBuf> {
Ok(profile_id.get_full_path().await?.join("crash-reports"))
}
#[inline] #[inline]
pub fn launcher_logs_dir() -> Option<PathBuf> { pub fn launcher_logs_dir() -> Option<PathBuf> {

View File

@@ -88,6 +88,15 @@ impl ProfilePathId {
.ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?; .ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?;
Ok(self) Ok(self)
} }
pub async fn profile_path(&self) -> crate::Result<ProfilePathId> {
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 { impl std::fmt::Display for ProfilePathId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@@ -3,6 +3,7 @@ use theseus::{
logs::{self, CensoredString, LatestLogCursor, Logs}, logs::{self, CensoredString, LatestLogCursor, Logs},
prelude::ProfilePathId, prelude::ProfilePathId,
}; };
use theseus::logs::LogType;
/* /*
A log is a struct containing the filename string, stdout, and stderr, as follows: 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] #[tauri::command]
pub async fn logs_get_logs_by_filename( pub async fn logs_get_logs_by_filename(
profile_path: ProfilePathId, profile_path: ProfilePathId,
log_type: LogType,
filename: String, filename: String,
) -> Result<Logs> { ) -> Result<Logs> {
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 /// Get the stdout for a profile by profile id and filename string
#[tauri::command] #[tauri::command]
pub async fn logs_get_output_by_filename( pub async fn logs_get_output_by_filename(
profile_path: ProfilePathId, profile_path: ProfilePathId,
log_type: LogType,
filename: String, filename: String,
) -> Result<CensoredString> { ) -> Result<CensoredString> {
let profile_path = if let Some(p) = let profile_path = if let Some(p) =
@@ -64,7 +67,7 @@ pub async fn logs_get_output_by_filename(
.into()); .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 /// 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] #[tauri::command]
pub async fn logs_delete_logs_by_filename( pub async fn logs_delete_logs_by_filename(
profile_path: ProfilePathId, profile_path: ProfilePathId,
log_type: LogType,
filename: String, filename: String,
) -> Result<()> { ) -> 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 /// Get live log from a cursor

View File

@@ -22,18 +22,22 @@ export async function get_logs(profilePath, clearContents) {
} }
/// Get a profile's log by filename /// Get a profile's log by filename
export async function 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, filename }) return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
} }
/// Get a profile's log text only by filename /// Get a profile's log text only by filename
export async function 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, filename }) return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, logType, filename })
} }
/// Delete a profile's log by filename /// Delete a profile's log by filename
export async function 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, filename }) return await invoke('plugin:logs|logs_delete_logs_by_filename', {
profilePath,
logType,
filename,
})
} }
/// Delete all logs for a given profile /// 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. 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 // From latest.log directly
export async function get_latest_log_cursor(profilePath, cursor) { export async function get_latest_log_cursor(profilePath, cursor) {
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor }) return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })

View File

@@ -54,8 +54,8 @@
v-model="levelFilters[level.toLowerCase()]" v-model="levelFilters[level.toLowerCase()]"
class="filter-checkbox" class="filter-checkbox"
> >
{{ level }}</Checkbox {{ level }}
> </Checkbox>
</div> </div>
</div> </div>
<div class="log-text"> <div class="log-text">
@@ -225,7 +225,7 @@ async function getLiveStdLog() {
} else { } else {
const logCursor = await get_latest_log_cursor( const logCursor = await get_latest_log_cursor(
props.instance.path, props.instance.path,
currentLiveLogCursor.value, currentLiveLogCursor.value
).catch(handleError) ).catch(handleError)
if (logCursor.new_file) { if (logCursor.new_file) {
currentLiveLog.value = '' currentLiveLog.value = ''
@@ -241,14 +241,13 @@ async function getLiveStdLog() {
async function getLogs() { async function getLogs() {
return (await get_logs(props.instance.path, true).catch(handleError)) return (await get_logs(props.instance.path, true).catch(handleError))
.reverse()
.filter( .filter(
// filter out latest_stdout.log or anything without .log in it // filter out latest_stdout.log or anything without .log in it
(log) => (log) =>
log.filename !== 'latest_stdout.log' && log.filename !== 'latest_stdout.log' &&
log.filename !== 'latest_stdout' && log.filename !== 'latest_stdout' &&
log.stdout !== '' && log.stdout !== '' &&
log.filename.includes('.log'), (log.filename.includes('.log') || log.filename.endsWith('.txt'))
) )
.map((log) => { .map((log) => {
log.name = log.filename || 'Unknown' log.name = log.filename || 'Unknown'
@@ -291,7 +290,8 @@ watch(selectedLogIndex, async (newIndex) => {
logs.value[newIndex].stdout = 'Loading...' logs.value[newIndex].stdout = 'Loading...'
logs.value[newIndex].stdout = await get_output_by_filename( logs.value[newIndex].stdout = await get_output_by_filename(
props.instance.path, props.instance.path,
logs.value[newIndex].filename, logs.value[newIndex].log_type,
logs.value[newIndex].filename
).catch(handleError) ).catch(handleError)
} }
}) })
@@ -306,9 +306,11 @@ const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) { if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
let deleteIndex = selectedLogIndex.value let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1 selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_filename(props.instance.path, logs.value[deleteIndex].filename).catch( await delete_logs_by_filename(
handleError, props.instance.path,
) logs.value[deleteIndex].log_type,
logs.value[deleteIndex].filename
).catch(handleError)
await setLogs() await setLogs()
} }
} }
@@ -512,6 +514,7 @@ onUnmounted(() => {
justify-self: center; justify-self: center;
} }
} }
.filter-group { .filter-group {
display: flex; display: flex;
padding: 0.6rem; padding: 0.6rem;