diff options
| author | 2026-01-14 11:12:54 +0800 | |
|---|---|---|
| committer | 2026-01-14 11:12:54 +0800 | |
| commit | f896913b51eb53f885fe58ea261e34b05ca84f56 (patch) | |
| tree | d7c10c63ba90d06343e5b0479ebc5ecf615a0685 | |
| parent | 7291c4dde8696be459b3f60b2844c87f012be676 (diff) | |
| download | DropOut-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.rs | 81 | ||||
| -rw-r--r-- | ui/src/lib/GameConsole.svelte | 62 |
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> |