Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch from stdout log to latest log MOD-595 #964

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions theseus/src/api/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,14 +241,6 @@ pub async fn get_latest_log_cursor(
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
}

#[tracing::instrument]
pub async fn get_std_log_cursor(
profile_path: ProfilePathId,
cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
get_generic_live_log_cursor(profile_path, "latest_stdout.log", cursor).await
}

#[tracing::instrument]
pub async fn get_generic_live_log_cursor(
profile_path: ProfilePathId,
Expand Down
6 changes: 2 additions & 4 deletions theseus/src/launcher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use daedalus as d;
use daedalus::minecraft::{RuleAction, VersionInfo};
use st::Profile;
use std::collections::HashMap;
use std::{process::Stdio, sync::Arc};
use std::sync::Arc;
use tokio::process::Command;
use uuid::Uuid;

Expand Down Expand Up @@ -511,9 +511,7 @@ pub async fn launch_minecraft(
.into_iter()
.collect::<Vec<_>>(),
)
.current_dir(instance_path.clone())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
.current_dir(instance_path.clone());

// CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground
#[cfg(target_os = "macos")]
Expand Down
165 changes: 2 additions & 163 deletions theseus/src/state/children.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
use super::DirectoryInfo;
use super::{Profile, ProfilePathId};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use serde::Serialize;
use std::path::Path;
use std::{collections::HashMap, sync::Arc};
use sysinfo::PidExt;
use tokio::fs::File;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::process::Child;
use tokio::process::ChildStderr;
use tokio::process::ChildStdout;
use tokio::process::Command;
use tokio::sync::RwLock;
use tracing::error;

use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
Expand Down Expand Up @@ -201,7 +192,6 @@ impl ChildType {
pub struct MinecraftChild {
pub uuid: Uuid,
pub profile_relative_path: ProfilePathId,
pub output: Option<SharedOutput>,
pub manager: Option<JoinHandle<crate::Result<i32>>>, // None when future has completed and been handled
pub current_child: Arc<RwLock<ChildType>>,
pub last_updated_playtime: DateTime<Utc>, // The last time we updated the playtime for the associated profile
Expand Down Expand Up @@ -281,44 +271,9 @@ impl Children {
censor_strings: HashMap<String, String>,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
// Takes the first element of the commands vector and spawns it
let mut child = mc_command.spawn().map_err(IOError::from)?;

// Create std watcher threads for stdout and stderr
let log_path = DirectoryInfo::profile_logs_dir(&profile_relative_path)
.await?
.join("latest_stdout.log");
let shared_output =
SharedOutput::build(&log_path, censor_strings).await?;
if let Some(child_stdout) = child.stdout.take() {
let stdout_clone = shared_output.clone();
tokio::spawn(async move {
if let Err(e) = stdout_clone.read_stdout(child_stdout).await {
error!("Stdout process died with error: {}", e);
let _ = stdout_clone
.push_line(format!(
"Stdout process died with error: {}",
e
))
.await;
}
});
}
if let Some(child_stderr) = child.stderr.take() {
let stderr_clone = shared_output.clone();
tokio::spawn(async move {
if let Err(e) = stderr_clone.read_stderr(child_stderr).await {
error!("Stderr process died with error: {}", e);
let _ = stderr_clone
.push_line(format!(
"Stderr process died with error: {}",
e
))
.await;
}
});
}
let mc_proc = mc_command.spawn().map_err(IOError::from)?;

let child = ChildType::TokioChild(child);
let child = ChildType::TokioChild(mc_proc);

// Slots child into manager
let pid = child.id().ok_or_else(|| {
Expand Down Expand Up @@ -358,7 +313,6 @@ impl Children {
let mchild = MinecraftChild {
uuid,
profile_relative_path,
output: Some(shared_output),
current_child,
manager,
last_updated_playtime,
Expand Down Expand Up @@ -449,7 +403,6 @@ impl Children {
let mchild = MinecraftChild {
uuid: cached_process.uuid,
profile_relative_path: cached_process.profile_relative_path,
output: None, // No output for cached/rescued processes
current_child,
manager,
last_updated_playtime,
Expand Down Expand Up @@ -758,117 +711,3 @@ impl Default for Children {
Self::new()
}
}

// SharedOutput, a wrapper around a String that can be read from and written to concurrently
// Designed to be used with ChildStdout and ChildStderr in a tokio thread to have a simple String storage for the output of a child process
#[derive(Debug, Clone)]
pub struct SharedOutput {
log_file: Arc<RwLock<File>>,
censor_strings: HashMap<String, String>,
}

impl SharedOutput {
#[tracing::instrument(skip(censor_strings))]
async fn build(
log_file_path: &Path,
censor_strings: HashMap<String, String>,
) -> crate::Result<Self> {
// create log_file_path parent if it doesn't exist
let parent_folder = log_file_path.parent().ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Could not get parent folder of {:?}",
log_file_path
))
})?;
tokio::fs::create_dir_all(parent_folder)
.await
.map_err(|e| IOError::with_path(e, parent_folder))?;

Ok(SharedOutput {
log_file: Arc::new(RwLock::new(
File::create(log_file_path)
.await
.map_err(|e| IOError::with_path(e, log_file_path))?,
)),
censor_strings,
})
}

async fn read_stdout(
&self,
child_stdout: ChildStdout,
) -> crate::Result<()> {
let mut buf_reader = BufReader::new(child_stdout);
let mut buf = Vec::new();

while buf_reader
.read_until(b'\n', &mut buf)
.await
.map_err(IOError::from)?
> 0
{
let line = String::from_utf8_lossy(&buf).into_owned();
let val_line = self.censor_log(line.clone());
{
let mut log_file = self.log_file.write().await;
log_file
.write_all(val_line.as_bytes())
.await
.map_err(IOError::from)?;
}

buf.clear();
}
Ok(())
}

async fn read_stderr(
&self,
child_stderr: ChildStderr,
) -> crate::Result<()> {
let mut buf_reader = BufReader::new(child_stderr);
let mut buf = Vec::new();

// TODO: these can be asbtracted into noe function
while buf_reader
.read_until(b'\n', &mut buf)
.await
.map_err(IOError::from)?
> 0
{
let line = String::from_utf8_lossy(&buf).into_owned();
let val_line = self.censor_log(line.clone());
{
let mut log_file = self.log_file.write().await;
log_file
.write_all(val_line.as_bytes())
.await
.map_err(IOError::from)?;
}

buf.clear();
}
Ok(())
}

async fn push_line(&self, line: String) -> crate::Result<()> {
let val_line = self.censor_log(line.clone());
{
let mut log_file = self.log_file.write().await;
log_file
.write_all(val_line.as_bytes())
.await
.map_err(IOError::from)?;
}

Ok(())
}

fn censor_log(&self, mut val: String) -> String {
for (find, replace) in &self.censor_strings {
val = val.replace(find, replace);
}

val
}
}
10 changes: 0 additions & 10 deletions theseus_gui/src-tauri/src/api/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
logs_delete_logs,
logs_delete_logs_by_filename,
logs_get_latest_log_cursor,
logs_get_std_log_cursor,
])
.build()
}
Expand Down Expand Up @@ -91,12 +90,3 @@ pub async fn logs_get_latest_log_cursor(
) -> Result<LatestLogCursor> {
Ok(logs::get_latest_log_cursor(profile_path, cursor).await?)
}

/// Get live stdout log from a cursor
#[tauri::command]
pub async fn logs_get_std_log_cursor(
profile_path: ProfilePathId,
cursor: u64, // 0 to start at beginning of file
) -> Result<LatestLogCursor> {
Ok(logs::get_std_log_cursor(profile_path, cursor).await?)
}
4 changes: 0 additions & 4 deletions theseus_gui/src/helpers/logs.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,3 @@ export async function delete_logs(profilePath) {
export async function get_latest_log_cursor(profilePath, cursor) {
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
}
// For std log (from modrinth app written latest_stdout.log, contains stdout and stderr)
export async function get_std_log_cursor(profilePath, cursor) {
return await invoke('plugin:logs|logs_get_std_log_cursor', { profilePath, cursor })
}
30 changes: 7 additions & 23 deletions theseus_gui/src/pages/instance/Logs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ import {
delete_logs_by_filename,
get_logs,
get_output_by_filename,
get_std_log_cursor,
get_latest_log_cursor,
} from '@/helpers/logs.js'
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import dayjs from 'dayjs'
Expand Down Expand Up @@ -139,7 +139,7 @@ const props = defineProps({

const currentLiveLog = ref(null)
const currentLiveLogCursor = ref(0)
const emptyText = ['No live game detected.', 'Start your game to proceed']
const emptyText = ['No live game detected.', 'Start your game to proceed.']

const logs = ref([])
await setLogs()
Expand Down Expand Up @@ -223,7 +223,7 @@ async function getLiveStdLog() {
if (uuids.length === 0) {
returnValue = emptyText.join('\n')
} else {
const logCursor = await get_std_log_cursor(
const logCursor = await get_latest_log_cursor(
props.instance.path,
currentLiveLogCursor.value
).catch(handleError)
Expand All @@ -243,31 +243,15 @@ 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.stdout !== '' &&
log.filename.includes('.log')
)
.map((log) => {
if (log.filename == 'latest.log') {
log.name = 'Latest Log'
} else {
let filename = log.filename.split('.')[0]
let day = dayjs(filename.slice(0, 10))
if (day.isValid()) {
if (day.isToday()) {
log.name = 'Today'
} else if (day.isYesterday()) {
log.name = 'Yesterday'
} else {
log.name = day.format('MMMM D, YYYY')
}
// Displays as "Today-1", "Today-2", etc, matching minecraft log naming but with the date
log.name = log.name + filename.slice(10)
} else {
log.name = filename
}
}
log.name = log.filename || 'Unknown'
log.stdout = 'Loading...'
return log
})
Expand Down
Loading