Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a56f4200 | ||
|
|
0f35a5f707 |
No files matched your search
Generated
+2
-2
@@ -6841,7 +6841,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "1.0.2-beta6"
|
version = "1.0.2-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ariadne",
|
"ariadne",
|
||||||
"async-minecraft-ping",
|
"async-minecraft-ping",
|
||||||
@@ -6945,7 +6945,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "1.0.2-beta6"
|
version = "1.0.2-beta7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2-beta6",
|
"version": "1.0.2-beta7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
# The actual version is set by the theseus-build workflow on tagging
|
# The actual version is set by the theseus-build workflow on tagging
|
||||||
version = "1.0.2-beta6"
|
version = "1.0.2-beta7"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description = "Axolotl Launcher is a cross-platform Minecraft launcher"
|
description = "Axolotl Launcher is a cross-platform Minecraft launcher"
|
||||||
homepage = "https://www.ghs.red"
|
homepage = "https://www.ghs.red"
|
||||||
|
|||||||
@@ -324,12 +324,9 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
/// external game-directory root for the one-click StarLight install flow.
|
/// external game-directory root for the one-click StarLight install flow.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_launcher_root_dir() -> api::Result<String> {
|
fn get_launcher_root_dir() -> api::Result<String> {
|
||||||
let exe = std::env::current_exe().map_err(|error| {
|
let dir = theseus::default_game_dir_root().ok_or_else(|| {
|
||||||
theseus::Error::from(theseus::ErrorKind::FSError(error.to_string()))
|
|
||||||
})?;
|
|
||||||
let dir = exe.parent().ok_or_else(|| {
|
|
||||||
theseus::Error::from(theseus::ErrorKind::FSError(
|
theseus::Error::from(theseus::ErrorKind::FSError(
|
||||||
"Launcher executable has no parent directory".to_string(),
|
"Could not determine a default game directory root".to_string(),
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(dir.to_string_lossy().into_owned())
|
Ok(dir.to_string_lossy().into_owned())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
# The actual version is set by the theseus-build workflow on tagging
|
# The actual version is set by the theseus-build workflow on tagging
|
||||||
version = "1.0.2-beta6"
|
version = "1.0.2-beta7"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub use state::{DirectoryInfo, State};
|
|||||||
pub use storage::*;
|
pub use storage::*;
|
||||||
pub use util::fetch::build_proxied_client;
|
pub use util::fetch::build_proxied_client;
|
||||||
pub use util::file_lock::{LockingProcess, get_locking_processes};
|
pub use util::file_lock::{LockingProcess, get_locking_processes};
|
||||||
pub use util::platform::is_process_elevated;
|
pub use util::platform::{default_game_dir_root, is_process_elevated};
|
||||||
pub use util::proxy::{ProxyConfig, ProxyMode, ProxyTestResult};
|
pub use util::proxy::{ProxyConfig, ProxyMode, ProxyTestResult};
|
||||||
pub use util::symlink::SymlinkCapability;
|
pub use util::symlink::SymlinkCapability;
|
||||||
|
|
||||||
|
|||||||
@@ -713,16 +713,37 @@ impl Process {
|
|||||||
|
|
||||||
pending.push_str(&String::from_utf8_lossy(&chunk[..read]));
|
pending.push_str(&String::from_utf8_lossy(&chunk[..read]));
|
||||||
|
|
||||||
// Drain every complete frame currently buffered.
|
// Mods can write plain text to either pipe even when the
|
||||||
while let Some(frame) = take_next_log4j_frame(&mut pending) {
|
// launcher selected XML logging. Forward it as soon as a line
|
||||||
Self::handle_log4j_frame(
|
// is complete instead of retaining it until the game exits.
|
||||||
instance_id,
|
while let Some(output) = take_next_log_output(&mut pending) {
|
||||||
instance_name,
|
match output {
|
||||||
process_id,
|
LogOutput::Xml(frame) => {
|
||||||
log_path.as_ref(),
|
Self::handle_log4j_frame(
|
||||||
&frame,
|
instance_id,
|
||||||
)
|
instance_name,
|
||||||
.await;
|
process_id,
|
||||||
|
log_path.as_ref(),
|
||||||
|
&frame,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
LogOutput::Legacy(text) => {
|
||||||
|
if let Err(e) =
|
||||||
|
Self::append_to_log_file(&log_path, &text)
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to write to log file: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !text.trim_end().is_empty() {
|
||||||
|
Self::emit_legacy_log(
|
||||||
|
instance_id,
|
||||||
|
text.trim_end(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard against a runaway buffer if no frame delimiters ever
|
// Guard against a runaway buffer if no frame delimiters ever
|
||||||
@@ -1360,32 +1381,119 @@ impl Process {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Cuts the next complete `<log4j:Event …>…</log4j:Event>` frame out of the
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
/// live buffer and returns it as an owned string, leaving any trailing partial
|
enum LogOutput {
|
||||||
/// frame in place.
|
Xml(String),
|
||||||
///
|
Legacy(String),
|
||||||
/// Returns `None` when the buffer does not yet contain a full frame. This is a
|
}
|
||||||
/// plain string operation on purpose: it never poisons any parser state, so a
|
|
||||||
/// split or malformed frame on the live pipe cannot stop log forwarding.
|
/// Drains complete XML frames or plain text in stream order. Partial frames
|
||||||
fn take_next_log4j_frame(buffer: &mut String) -> Option<String> {
|
/// and plain text lines remain buffered until their next chunk arrives.
|
||||||
|
fn take_next_log_output(buffer: &mut String) -> Option<LogOutput> {
|
||||||
const OPEN: &str = "<log4j:Event";
|
const OPEN: &str = "<log4j:Event";
|
||||||
const CLOSE: &str = "</log4j:Event>";
|
const CLOSE: &str = "</log4j:Event>";
|
||||||
|
|
||||||
let start = buffer.find(OPEN)?;
|
match buffer.find(OPEN) {
|
||||||
// Discard anything before the frame (raw text, XML prolog, …).
|
Some(0) => {
|
||||||
if start > 0 {
|
let close = buffer.find(CLOSE)?;
|
||||||
buffer.drain(..start);
|
let end = close + CLOSE.len();
|
||||||
|
Some(LogOutput::Xml(buffer.drain(..end).collect()))
|
||||||
|
}
|
||||||
|
Some(start) => Some(LogOutput::Legacy(buffer.drain(..start).collect())),
|
||||||
|
None => {
|
||||||
|
let end = buffer.find('\n')? + 1;
|
||||||
|
Some(LogOutput::Legacy(buffer.drain(..end).collect()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let close = buffer.find(CLOSE)?;
|
|
||||||
let end = close + CLOSE.len();
|
|
||||||
let frame = buffer[..end].to_string();
|
|
||||||
buffer.drain(..end);
|
|
||||||
Some(frame)
|
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod post_upgrade_tests {
|
mod post_upgrade_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_log_output_keeps_partial_xml_and_multiline_messages() {
|
||||||
|
let mut pending = "raw\n<log4j:Ev".to_string();
|
||||||
|
assert_eq!(
|
||||||
|
take_next_log_output(&mut pending),
|
||||||
|
Some(LogOutput::Legacy("raw\n".into()))
|
||||||
|
);
|
||||||
|
assert_eq!(take_next_log_output(&mut pending), None);
|
||||||
|
pending.push_str("ent><log4j:Message><![CDATA[first\nsecond");
|
||||||
|
assert_eq!(take_next_log_output(&mut pending), None);
|
||||||
|
pending.push_str("]]></log4j:Message></log4j:Event>\nafter\n");
|
||||||
|
assert_eq!(take_next_log_output(&mut pending), Some(LogOutput::Xml("<log4j:Event><log4j:Message><![CDATA[first\nsecond]]></log4j:Message></log4j:Event>".into())));
|
||||||
|
assert_eq!(
|
||||||
|
take_next_log_output(&mut pending),
|
||||||
|
Some(LogOutput::Legacy("\n".into()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
take_next_log_output(&mut pending),
|
||||||
|
Some(LogOutput::Legacy("after\n".into()))
|
||||||
|
);
|
||||||
|
assert!(pending.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn xml_logging_forwards_plain_text_before_pipe_closes() {
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
let instance_id = Uuid::new_v4().to_string();
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let log_path = directory.path().join("live.log");
|
||||||
|
let (mut writer, reader) = tokio::io::duplex(1024);
|
||||||
|
let output = Process::process_output(
|
||||||
|
&instance_id,
|
||||||
|
"instance",
|
||||||
|
"Instance",
|
||||||
|
"process",
|
||||||
|
reader,
|
||||||
|
&log_path,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tokio::pin!(output);
|
||||||
|
writer.write_all(b"mod output\n").await.unwrap();
|
||||||
|
let forwarded = async {
|
||||||
|
loop {
|
||||||
|
if get_log_buffer(&instance_id) == ["mod output"] {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut output => panic!("reader ended while writer is still open"),
|
||||||
|
result = tokio::time::timeout(std::time::Duration::from_secs(1), forwarded) => {
|
||||||
|
result.expect("plain text was buffered until EOF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(std::fs::read_to_string(&log_path).unwrap(), "mod output\n");
|
||||||
|
drop(writer);
|
||||||
|
output.await;
|
||||||
|
remove_log_buffer(&instance_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn xml_logging_preserves_plain_text_before_xml_frames() {
|
||||||
|
let instance_id = Uuid::new_v4().to_string();
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let log_path = directory.path().join("mixed.log");
|
||||||
|
let input = "mod output\n<log4j:Event level=\"INFO\"><log4j:Message><![CDATA[xml output]]></log4j:Message></log4j:Event>\n";
|
||||||
|
Process::process_output(
|
||||||
|
&instance_id,
|
||||||
|
"instance",
|
||||||
|
"Instance",
|
||||||
|
"process",
|
||||||
|
std::io::Cursor::new(input),
|
||||||
|
&log_path,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let lines = get_log_buffer(&instance_id);
|
||||||
|
assert_eq!(lines.first().map(String::as_str), Some("mod output"));
|
||||||
|
assert!(lines.iter().any(|line| line.contains("xml output")));
|
||||||
|
remove_log_buffer(&instance_id);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn live_log_snapshot_sequences_batches_and_preserves_position_after_clear()
|
fn live_log_snapshot_sequences_batches_and_preserves_position_after_clear()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Platform-related code
|
//! Platform-related code
|
||||||
use daedalus::minecraft::{Os, OsRule};
|
use daedalus::minecraft::{Os, OsRule};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// (e.g. "Run as administrator" on Windows).
|
/// (e.g. "Run as administrator" on Windows).
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -143,3 +144,31 @@ pub fn classpath_separator(java_arch: &str) -> &'static str {
|
|||||||
Os::Windows | Os::WindowsArm64 => ";",
|
Os::Windows | Os::WindowsArm64 => ";",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Root directory used as the default for one-click StarLight instance
|
||||||
|
/// installs. Portable builds live next to their executable, which is a fine
|
||||||
|
/// place for game data. System installs (e.g. a Linux `.deb` under `/usr/bin`)
|
||||||
|
/// do not, so fall back to the user's home directory there.
|
||||||
|
pub fn default_game_dir_root() -> Option<PathBuf> {
|
||||||
|
let exe_dir = std::env::current_exe().ok()?.parent()?.to_path_buf();
|
||||||
|
if cfg!(target_os = "linux") && is_system_install_dir(&exe_dir) {
|
||||||
|
return dirs::home_dir();
|
||||||
|
}
|
||||||
|
Some(exe_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn is_system_install_dir(path: &std::path::Path) -> bool {
|
||||||
|
const SYSTEM_PREFIXES: &[&str] = &[
|
||||||
|
"/usr/", "/opt/", "/snap/", "/bin/", "/sbin/", "/lib/", "/lib64/",
|
||||||
|
];
|
||||||
|
let display = path.to_string_lossy();
|
||||||
|
SYSTEM_PREFIXES
|
||||||
|
.iter()
|
||||||
|
.any(|prefix| display.starts_with(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn is_system_install_dir(_path: &std::path::Path) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
Reference in new issue
Block a user