aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-14 11:12:54 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-14 11:12:54 +0800
commitf896913b51eb53f885fe58ea261e34b05ca84f56 (patch)
treed7c10c63ba90d06343e5b0479ebc5ecf615a0685
parent7291c4dde8696be459b3f60b2844c87f012be676 (diff)
downloadDropOut-f896913b51eb53f885fe58ea261e34b05ca84f56.tar.gz
DropOut-f896913b51eb53f885fe58ea261e34b05ca84f56.zip
feat: enhance logging in game launch process and update GameConsole to display launcher logs
-rw-r--r--src-tauri/src/main.rs81
-rw-r--r--ui/src/lib/GameConsole.svelte62
2 files changed, 124 insertions, 19 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 54da124..d7ae9a4 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -7,6 +7,17 @@ use tauri::{Emitter, Manager, State, Window}; // Added Emitter
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+/// Helper macro to emit launcher log events
+macro_rules! emit_log {
+ ($window:expr, $msg:expr) => {
+ let _ = $window.emit("launcher-log", $msg);
+ println!("[Launcher] {}", $msg);
+ };
+}
+
mod core;
mod utils;
@@ -30,17 +41,26 @@ async fn start_game(
config_state: State<'_, core::config::ConfigState>,
version_id: String,
) -> Result<String, String> {
- println!("Backend received StartGame for {}", version_id);
+ emit_log!(window, format!("Starting game launch for version: {}", version_id));
// Check for active account
+ emit_log!(window, "Checking for active account...".to_string());
let account = auth_state
.active_account
.lock()
.unwrap()
.clone()
.ok_or("No active account found. Please login first.")?;
+
+ let account_type = match &account {
+ core::auth::Account::Offline(_) => "Offline",
+ core::auth::Account::Microsoft(_) => "Microsoft",
+ };
+ emit_log!(window, format!("Account found: {} ({})", account.username(), account_type));
let config = config_state.config.lock().unwrap().clone();
+ emit_log!(window, format!("Java path: {}", config.java_path));
+ emit_log!(window, format!("Memory: {}MB - {}MB", config.min_memory, config.max_memory));
// Get App Data Directory (e.g., ~/.local/share/com.dropout.launcher or similar)
// The identifier is set in tauri.conf.json.
@@ -56,12 +76,14 @@ async fn start_game(
.await
.map_err(|e| e.to_string())?;
- println!("Game Directory: {:?}", game_dir);
+ emit_log!(window, format!("Game directory: {:?}", game_dir));
// 1. Fetch manifest to find the version URL
+ emit_log!(window, "Fetching version manifest...".to_string());
let manifest = core::manifest::fetch_version_manifest()
.await
.map_err(|e| e.to_string())?;
+ emit_log!(window, format!("Found {} versions in manifest", manifest.versions.len()));
// Find the version info
let version_info = manifest
@@ -71,6 +93,7 @@ async fn start_game(
.ok_or_else(|| format!("Version {} not found in manifest", version_id))?;
// 2. Fetch specific version JSON (client.jar info)
+ emit_log!(window, format!("Fetching version details for {}...", version_id));
let version_url = &version_info.url;
let version_details: core::game_version::GameVersion = reqwest::get(version_url)
.await
@@ -78,8 +101,10 @@ async fn start_game(
.json()
.await
.map_err(|e| e.to_string())?;
+ emit_log!(window, format!("Version details loaded: main class = {}", version_details.main_class));
// 3. Prepare download tasks
+ emit_log!(window, "Preparing download tasks...".to_string());
let mut download_tasks = Vec::new();
// --- Client Jar ---
@@ -231,18 +256,20 @@ async fn start_game(
});
}
- println!(
- "Total download tasks (Client + Libs + Assets): {}",
+ emit_log!(window, format!(
+ "Total download tasks: {} (Client + Libraries + Assets)",
download_tasks.len()
- );
+ ));
// 4. Start Download
+ emit_log!(window, "Starting downloads...".to_string());
core::downloader::download_files(window.clone(), download_tasks)
.await
.map_err(|e| e.to_string())?;
+ emit_log!(window, "All downloads completed successfully".to_string());
// 5. Extract Natives
- println!("Extracting natives...");
+ emit_log!(window, "Extracting native libraries...".to_string());
let natives_dir = game_dir.join("versions").join(&version_id).join("natives");
// Clean old natives if they exist to prevent conflicts
@@ -402,22 +429,35 @@ async fn start_game(
}
}
- println!("Launching game with {} args...", args.len());
- // Debug: Print arguments to help diagnose issues
- println!("Launch Args: {:?}", args);
+ emit_log!(window, format!("Preparing to launch game with {} arguments...", args.len()));
+ // Debug: Log arguments (only first few to avoid spam)
+ if args.len() > 10 {
+ emit_log!(window, format!("First 10 args: {:?}", &args[..10]));
+ }
// Spawn the process
+ emit_log!(window, format!("Starting Java process: {}", config.java_path));
let mut command = Command::new(&config.java_path);
command.args(&args);
command.current_dir(&game_dir); // Run in game directory
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
+ // On Windows, use CREATE_NO_WINDOW flag to hide the console window
+ #[cfg(target_os = "windows")]
+ {
+ const CREATE_NO_WINDOW: u32 = 0x08000000;
+ command.creation_flags(CREATE_NO_WINDOW);
+ emit_log!(window, "Applied CREATE_NO_WINDOW flag for Windows".to_string());
+ }
+
// Spawn and handle output
let mut child = command
.spawn()
.map_err(|e| format!("Failed to launch java: {}", e))?;
+ emit_log!(window, "Java process started successfully".to_string());
+
let stdout = child
.stdout
.take()
@@ -427,20 +467,43 @@ async fn start_game(
.take()
.expect("child did not have a handle to stderr");
+ // Emit launcher log that game is running
+ emit_log!(window, "Game is now running, capturing output...".to_string());
+
let window_rx = window.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(stdout).lines();
while let Ok(Some(line)) = reader.next_line().await {
let _ = window_rx.emit("game-stdout", line);
}
+ // Emit log when stdout stream ends (game closing)
+ let _ = window_rx.emit("launcher-log", "Game stdout stream ended");
});
let window_rx_err = window.clone();
+ let window_exit = window.clone();
tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
let _ = window_rx_err.emit("game-stderr", line);
}
+ // Emit log when stderr stream ends
+ let _ = window_rx_err.emit("launcher-log", "Game stderr stream ended");
+ });
+
+ // Monitor game process exit
+ tokio::spawn(async move {
+ match child.wait().await {
+ Ok(status) => {
+ let msg = format!("Game process exited with status: {}", status);
+ let _ = window_exit.emit("launcher-log", &msg);
+ let _ = window_exit.emit("game-exited", status.code().unwrap_or(-1));
+ }
+ Err(e) => {
+ let msg = format!("Error waiting for game process: {}", e);
+ let _ = window_exit.emit("launcher-log", &msg);
+ }
+ }
});
Ok(format!("Launched Minecraft {} successfully!", version_id))
diff --git a/ui/src/lib/GameConsole.svelte b/ui/src/lib/GameConsole.svelte
index d6913a5..281dc85 100644
--- a/ui/src/lib/GameConsole.svelte
+++ b/ui/src/lib/GameConsole.svelte
@@ -4,30 +4,51 @@
export let visible = false;
- let logs: { type: 'stdout' | 'stderr', line: string }[] = [];
+ let logs: { type: 'stdout' | 'stderr' | 'launcher', line: string, timestamp: string }[] = [];
let consoleElement: HTMLDivElement;
let unlistenStdout: () => void;
let unlistenStderr: () => void;
+ let unlistenLauncher: () => void;
+ let unlistenGameExited: () => void;
+
+ function getTimestamp(): string {
+ const now = new Date();
+ return now.toTimeString().split(' ')[0]; // HH:MM:SS
+ }
onMount(async () => {
+ // Listen for launcher logs (preparation, downloads, launch status)
+ unlistenLauncher = await listen<string>("launcher-log", (event) => {
+ addLog('launcher', event.payload);
+ });
+
+ // Listen for game stdout
unlistenStdout = await listen<string>("game-stdout", (event) => {
addLog('stdout', event.payload);
});
+ // Listen for game stderr
unlistenStderr = await listen<string>("game-stderr", (event) => {
addLog('stderr', event.payload);
});
+
+ // Listen for game exit event
+ unlistenGameExited = await listen<number>("game-exited", (event) => {
+ addLog('launcher', `Game process exited with code: ${event.payload}`);
+ });
});
onDestroy(() => {
+ if (unlistenLauncher) unlistenLauncher();
if (unlistenStdout) unlistenStdout();
if (unlistenStderr) unlistenStderr();
+ if (unlistenGameExited) unlistenGameExited();
});
- function addLog(type: 'stdout' | 'stderr', line: string) {
- logs = [...logs, { type, line }];
- if (logs.length > 1000) {
- logs = logs.slice(logs.length - 1000);
+ function addLog(type: 'stdout' | 'stderr' | 'launcher', line: string) {
+ logs = [...logs, { type, line, timestamp: getTimestamp() }];
+ if (logs.length > 2000) {
+ logs = logs.slice(logs.length - 2000);
}
// Auto-scroll
setTimeout(() => {
@@ -40,25 +61,46 @@
function clearLogs() {
logs = [];
}
+
+ function exportLogs() {
+ const logText = logs.map(l => `[${l.timestamp}] [${l.type.toUpperCase()}] ${l.line}`).join('\n');
+ const blob = new Blob([logText], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `dropout-logs-${new Date().toISOString().split('T')[0]}.txt`;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
</script>
{#if visible}
<div class="fixed bottom-0 left-0 right-0 h-64 bg-zinc-950/95 border-t border-zinc-700 backdrop-blur flex flex-col z-50 transition-transform duration-300 transform translate-y-0">
<div class="flex items-center justify-between px-4 py-2 border-b border-zinc-800 bg-zinc-900/50">
- <span class="text-xs font-bold text-zinc-400 uppercase tracking-widest">Game Console</span>
+ <div class="flex items-center gap-4">
+ <span class="text-xs font-bold text-zinc-400 uppercase tracking-widest">Logs</span>
+ <div class="flex gap-1 text-[10px]">
+ <span class="px-1.5 py-0.5 rounded bg-indigo-900/50 text-indigo-300">LAUNCHER</span>
+ <span class="px-1.5 py-0.5 rounded bg-zinc-800 text-zinc-300">GAME</span>
+ <span class="px-1.5 py-0.5 rounded bg-red-900/50 text-red-300">ERROR</span>
+ </div>
+ </div>
<div class="flex gap-2">
+ <button on:click={exportLogs} class="text-xs text-zinc-500 hover:text-white px-2 py-1 rounded transition">Export</button>
<button on:click={clearLogs} class="text-xs text-zinc-500 hover:text-white px-2 py-1 rounded transition">Clear</button>
<button on:click={() => visible = false} class="text-xs text-zinc-500 hover:text-white px-2 py-1 rounded transition">Close</button>
</div>
</div>
- <div bind:this={consoleElement} class="flex-1 overflow-y-auto p-4 font-mono text-xs space-y-1">
+ <div bind:this={consoleElement} class="flex-1 overflow-y-auto p-4 font-mono text-xs space-y-0.5">
{#each logs as log}
- <div class="{log.type === 'stderr' ? 'text-red-400' : 'text-zinc-300'} whitespace-pre-wrap break-all border-l-2 pl-2 {log.type === 'stderr' ? 'border-red-900/50' : 'border-transparent'}">
- {log.line}
+ <div class="flex whitespace-pre-wrap break-all {log.type === 'stderr' ? 'text-red-400' : log.type === 'launcher' ? 'text-indigo-300' : 'text-zinc-300'}">
+ <span class="text-zinc-600 mr-2 shrink-0">{log.timestamp}</span>
+ <span class="shrink-0 mr-2 {log.type === 'stderr' ? 'text-red-500' : log.type === 'launcher' ? 'text-indigo-500' : 'text-zinc-500'}">[{log.type === 'launcher' ? 'LAUNCHER' : log.type === 'stderr' ? 'ERROR' : 'GAME'}]</span>
+ <span class="break-all">{log.line}</span>
</div>
{/each}
{#if logs.length === 0}
- <div class="text-zinc-600 italic">Waiting for game output...</div>
+ <div class="text-zinc-600 italic">Waiting for output... Click "Show Logs" and start a game to see logs here.</div>
{/if}
</div>
</div>