diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 832357e..4786560 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -27,6 +27,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager'); const { syncModsForCurrentProfile } = require('./modManager'); const { getUserDataPath } = require('../utils/userDataMigration'); const { syncServerList } = require('../utils/serverListSync'); +const { killGameProcesses } = require('./gameManager'); // Client patcher for custom auth server (sanasol.ws) let clientPatcher = null; @@ -436,6 +437,22 @@ exec "$REAL_JAVA" "\${ARGS[@]}" } } + // Kill any stalled game processes from a previous launch to prevent file locks + // and "game already running" issues + await killGameProcesses(); + + // Remove AOT cache: generated by official Hytale JRE, incompatible with F2P JRE. + // Client adds -XX:AOTCache when this file exists, causing classloading failures. + const aotCache = path.join(gameLatest, 'Server', 'HytaleServer.aot'); + if (fs.existsSync(aotCache)) { + try { + fs.unlinkSync(aotCache); + console.log('Removed incompatible AOT cache (HytaleServer.aot)'); + } catch (aotErr) { + console.warn('Could not remove AOT cache:', aotErr.message); + } + } + // DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag // This enables runtime auth patching without modifying the server JAR const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar'); diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 465da2f..31a2f25 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -39,6 +39,41 @@ async function isGameRunning() { } } +// Force-kill stalled game processes to release file locks before repair/reinstall. +// Cross-platform: Windows (taskkill/PowerShell), macOS (pkill), Linux (pkill). +async function killGameProcesses() { + const killed = []; + + async function tryKill(command, label) { + try { + await execAsync(command); + killed.push(label); + } catch (_) { /* process not found is expected */ } + } + + if (process.platform === 'win32') { + // Kill client + await tryKill('taskkill /F /IM "HytaleClient.exe" /T', 'HytaleClient.exe'); + // Kill java.exe instances running HytaleServer.jar via PowerShell + // (Get-CimInstance replaces deprecated wmic, works on Windows 10+) + await tryKill( + 'powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'java.exe\'\\" | Where-Object { $_.CommandLine -like \'*HytaleServer*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"', + 'java.exe(HytaleServer)' + ); + } else { + // macOS and Linux + await tryKill('pkill -9 -f HytaleClient', 'HytaleClient'); + await tryKill('pkill -9 -f HytaleServer', 'HytaleServer'); + } + + if (killed.length > 0) { + console.log(`[GameManager] Force-killed stalled processes: ${killed.join(', ')}`); + // Wait for OS to release file handles + await new Promise(resolve => setTimeout(resolve, 2000)); + } + return killed; +} + // Helper function to safely remove directory with retry logic async function safeRemoveDirectory(dirPath, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { @@ -50,8 +85,13 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) { return; // Success, exit the loop } catch (error) { console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`); - + if (attempt < maxRetries) { + // On EPERM/EBUSY, try killing stalled game processes that hold file locks + if (attempt === 1 && (error.code === 'EPERM' || error.code === 'EBUSY')) { + console.log('Permission error detected, killing stalled game processes...'); + await killGameProcesses(); + } // Wait before retrying (exponential backoff) const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); console.log(`Waiting ${delay}ms before retry...`); @@ -833,11 +873,14 @@ async function repairGame(progressCallback, branchOverride = null) { progressCallback('Removing old game files...', 30, null, null, null); } - // Check if game is running before attempting to delete files + // Kill stalled game processes before attempting to delete files const gameRunning = await isGameRunning(); if (gameRunning) { - console.warn('[RepairGame] Game appears to be running. This may cause permission errors during repair.'); - console.log('[RepairGame] Please close the game before repairing, or wait for the repair to complete.'); + console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...'); + if (progressCallback) { + progressCallback('Stopping stalled game processes...', 20, null, null, null); + } + await killGameProcesses(); } // Delete Game and Cache Directory with retry logic @@ -964,5 +1007,6 @@ module.exports = { installGame, uninstallGame, checkExistingGameInstallation, - repairGame + repairGame, + killGameProcesses }; diff --git a/package.json b/package.json index d5f286e..2211450 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.3.8", + "version": "2.3.9", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://git.sanhost.net/sanasol/hytale-f2p", "main": "main.js",