v2.3.9: Fix stalled processes and AOT cache crash

- Auto-kill stalled HytaleClient/java processes before launch and repair
  (cross-platform: Windows taskkill+PowerShell, macOS/Linux pkill)
- Remove HytaleServer.aot before launch to prevent incompatible AOT cache
  causing fastutil ClassNotFoundException on singleplayer
- safeRemoveDirectory auto-kills processes on EPERM/EBUSY before retry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-02-24 12:52:07 +01:00
parent a649bf1fcc
commit e14d56ef48
3 changed files with 67 additions and 6 deletions

View File

@@ -27,6 +27,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager');
const { syncModsForCurrentProfile } = require('./modManager'); const { syncModsForCurrentProfile } = require('./modManager');
const { getUserDataPath } = require('../utils/userDataMigration'); const { getUserDataPath } = require('../utils/userDataMigration');
const { syncServerList } = require('../utils/serverListSync'); const { syncServerList } = require('../utils/serverListSync');
const { killGameProcesses } = require('./gameManager');
// Client patcher for custom auth server (sanasol.ws) // Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null; 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 // DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
// This enables runtime auth patching without modifying the server JAR // This enables runtime auth patching without modifying the server JAR
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar'); const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');

View File

@@ -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 // Helper function to safely remove directory with retry logic
async function safeRemoveDirectory(dirPath, maxRetries = 3) { async function safeRemoveDirectory(dirPath, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) { for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -52,6 +87,11 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`); console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
if (attempt < maxRetries) { 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) // Wait before retrying (exponential backoff)
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
console.log(`Waiting ${delay}ms before retry...`); 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); 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(); const gameRunning = await isGameRunning();
if (gameRunning) { if (gameRunning) {
console.warn('[RepairGame] Game appears to be running. This may cause permission errors during repair.'); console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...');
console.log('[RepairGame] Please close the game before repairing, or wait for the repair to complete.'); if (progressCallback) {
progressCallback('Stopping stalled game processes...', 20, null, null, null);
}
await killGameProcesses();
} }
// Delete Game and Cache Directory with retry logic // Delete Game and Cache Directory with retry logic
@@ -964,5 +1007,6 @@ module.exports = {
installGame, installGame,
uninstallGame, uninstallGame,
checkExistingGameInstallation, checkExistingGameInstallation,
repairGame repairGame,
killGameProcesses
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "hytale-f2p-launcher", "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", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p", "homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
"main": "main.js", "main": "main.js",