Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b94e7dbbe | ||
|
|
0f35a5f707 | ||
|
|
6fb73490d3 | ||
|
|
2dcf26c9d9 |
No files matched your search
Generated
+2
-2
@@ -6841,7 +6841,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "1.0.2-beta4"
|
||||
version = "1.0.2-beta7"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"async-minecraft-ping",
|
||||
@@ -6945,7 +6945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus_gui"
|
||||
version = "1.0.2-beta4"
|
||||
version = "1.0.2-beta7"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"async_zip",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.2-beta4",
|
||||
"version": "1.0.2-beta7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -32,6 +32,55 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
||||
]
|
||||
|
||||
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
||||
{
|
||||
id: 'launcher-1.0.2-beta6',
|
||||
version: '1.0.2-beta6',
|
||||
publishedAt: '2026-10-11',
|
||||
title: {
|
||||
'en-US': 'Starlight Launcher 1.0.2-beta6',
|
||||
'zh-CN': 'Starlight Launcher 1.0.2-beta6',
|
||||
},
|
||||
changes: {
|
||||
added: [
|
||||
{
|
||||
'en-US': 'Added keyboard shortcut settings: enable and rebind navigation and quick-scrolling shortcuts.',
|
||||
'zh-CN': '新增键盘快捷键设置:可启用并自定义导航与快速滚动快捷键。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Added crash log sharing and AI analysis: share crash diagnostics to LogShare for a structured summary and optional AI analysis, with a rebuilt crash window that browses log files and streams the analysis.',
|
||||
'zh-CN': '新增崩溃日志分享与 AI 分析:可将崩溃诊断分享到 LogShare 获取结构化摘要与 AI 分析;崩溃窗口重做为双栏布局,可浏览日志文件并流式查看分析结果。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Crash logs now also collect JVM crash reports (hs_err_pid*.log) and launcher logs.',
|
||||
'zh-CN': '崩溃日志现在也会收集 JVM 崩溃报告(hs_err_pid*.log)与启动器日志。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Added a launcher log export entry.',
|
||||
'zh-CN': '新增启动器日志导出入口。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Instance groups can now be reordered by dragging.',
|
||||
'zh-CN': '实例分组支持拖拽重排。',
|
||||
},
|
||||
],
|
||||
changed: [
|
||||
{
|
||||
'en-US': 'Synced the latest supported AI chat model list from LobeHub.',
|
||||
'zh-CN': '同步 LobeHub 最新的 AI 聊天模型列表。',
|
||||
},
|
||||
],
|
||||
fixed: [
|
||||
{
|
||||
'en-US': 'Fixed remote announcements not refreshing.',
|
||||
'zh-CN': '修复远端公告不刷新的问题。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Completed the Chinese translations for the armor preview feature.',
|
||||
'zh-CN': '补齐盔甲预览功能的中文翻译。',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'launcher-1.0.2-beta5',
|
||||
version: '1.0.2-beta5',
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
# The actual version is set by the theseus-build workflow on tagging
|
||||
version = "1.0.2-beta4"
|
||||
version = "1.0.2-beta7"
|
||||
edition.workspace = true
|
||||
description = "Axolotl Launcher is a cross-platform Minecraft launcher"
|
||||
homepage = "https://www.ghs.red"
|
||||
|
||||
@@ -339,7 +339,7 @@ fn get_launcher_root_dir() -> api::Result<String> {
|
||||
fn get_update_channel(app: tauri::AppHandle) -> api::Result<String> {
|
||||
let channel = read_update_channel_state(&app)?
|
||||
.active_channel
|
||||
.unwrap_or_else(|| "release".to_string());
|
||||
.unwrap_or_else(|| theseus::default_update_channel().to_string());
|
||||
|
||||
match channel.as_str() {
|
||||
"release" | "beta" => Ok(channel),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus"
|
||||
# The actual version is set by the theseus-build workflow on tagging
|
||||
version = "1.0.2-beta4"
|
||||
version = "1.0.2-beta7"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -33,7 +33,7 @@ pub use logger::start_logger;
|
||||
pub use state::db::{
|
||||
backup_current_app_db_for_update, beta_database_exists,
|
||||
copy_database_between_channels, copy_release_database_to_beta,
|
||||
current_app_database_path,
|
||||
current_app_database_path, default_update_channel,
|
||||
};
|
||||
pub use state::{DirectoryInfo, State};
|
||||
pub use storage::*;
|
||||
|
||||
@@ -304,6 +304,18 @@ async fn app_db_path(settings_dir: &Path) -> crate::Result<PathBuf> {
|
||||
Ok(settings_dir.join(channel).join(LEGACY_APP_DB_FILE))
|
||||
}
|
||||
|
||||
/// The update channel a fresh install should default to. Beta builds carry a
|
||||
/// pre-release version (e.g. `1.0.2-beta7`), so their installers should start
|
||||
/// on the Beta channel instead of silently falling back to Release.
|
||||
pub fn default_update_channel() -> &'static str {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
if version.contains('-') {
|
||||
"beta"
|
||||
} else {
|
||||
"release"
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_update_channel(
|
||||
settings_dir: &Path,
|
||||
) -> crate::Result<&'static str> {
|
||||
@@ -311,7 +323,7 @@ async fn read_update_channel(
|
||||
let contents = match tokio::fs::read_to_string(&path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok("release");
|
||||
return Ok(default_update_channel());
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
|
||||
@@ -713,16 +713,37 @@ impl Process {
|
||||
|
||||
pending.push_str(&String::from_utf8_lossy(&chunk[..read]));
|
||||
|
||||
// Drain every complete frame currently buffered.
|
||||
while let Some(frame) = take_next_log4j_frame(&mut pending) {
|
||||
Self::handle_log4j_frame(
|
||||
instance_id,
|
||||
instance_name,
|
||||
process_id,
|
||||
log_path.as_ref(),
|
||||
&frame,
|
||||
)
|
||||
.await;
|
||||
// Mods can write plain text to either pipe even when the
|
||||
// launcher selected XML logging. Forward it as soon as a line
|
||||
// is complete instead of retaining it until the game exits.
|
||||
while let Some(output) = take_next_log_output(&mut pending) {
|
||||
match output {
|
||||
LogOutput::Xml(frame) => {
|
||||
Self::handle_log4j_frame(
|
||||
instance_id,
|
||||
instance_name,
|
||||
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
|
||||
@@ -1360,32 +1381,119 @@ impl Process {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Cuts the next complete `<log4j:Event …>…</log4j:Event>` frame out of the
|
||||
/// live buffer and returns it as an owned string, leaving any trailing partial
|
||||
/// frame in place.
|
||||
///
|
||||
/// 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.
|
||||
fn take_next_log4j_frame(buffer: &mut String) -> Option<String> {
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum LogOutput {
|
||||
Xml(String),
|
||||
Legacy(String),
|
||||
}
|
||||
|
||||
/// Drains complete XML frames or plain text in stream order. Partial frames
|
||||
/// 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 CLOSE: &str = "</log4j:Event>";
|
||||
|
||||
let start = buffer.find(OPEN)?;
|
||||
// Discard anything before the frame (raw text, XML prolog, …).
|
||||
if start > 0 {
|
||||
buffer.drain(..start);
|
||||
match buffer.find(OPEN) {
|
||||
Some(0) => {
|
||||
let close = buffer.find(CLOSE)?;
|
||||
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)]
|
||||
mod post_upgrade_tests {
|
||||
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]
|
||||
fn live_log_snapshot_sequences_batches_and_preserves_position_after_clear()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user