Release v2.1.1: Fix EPERM cross-platform error (#183)

* fix: resolve cross-platform EPERM permissions errors

modManager.js:
- Switch from hardcoded 'junction' to dynamic symlink type based on OS (fixing Linux EPERM).
- Add retry logic for directory removal to handle file locking race conditions.
- Improve broken symlink detection during profile sync.

gameManager.js:
- Implement retry loop (3 attempts) for game directory removal in updateGameFiles to prevent EBUSY/EPERM errors on Windows.

paths.js:
- Prevent fs.mkdirSync failure in getModsPath by pre-checking for broken symbolic links.

* fix: missing pacman builds

* prepare release for 2.1.1

minor fix for EPERM error permission

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

* fix: add pathexists for paths.js to check symlink

* fix: isbrokenlink should be true to remove the symlink
This commit is contained in:
Fazri Gading
2026-01-26 12:29:14 +08:00
committed by GitHub
parent 7a9a67d8e8
commit c4a32ce1e0
7 changed files with 82 additions and 21 deletions

View File

@@ -35,6 +35,7 @@ jobs:
dist/*.AppImage.blockmap dist/*.AppImage.blockmap
dist/*.deb dist/*.deb
dist/*.rpm dist/*.rpm
dist/*.pacman
dist/*.pkg.tar.zst dist/*.pkg.tar.zst
dist/latest-linux.yml dist/latest-linux.yml

View File

@@ -1,12 +1,13 @@
<div align="center"> <div align="center">
<header> <header>
<h1>🎮 Hytale F2P Launcher | Cross-Platform Multiplayer 🖥️</h1> <h1>🎮 Hytale F2P Launcher 🚀</h1>
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3> <h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p> <p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
</header> </header>
![Version](https://img.shields.io/badge/Version-2.1.0-green?style=for-the-badge) ![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
@@ -17,10 +18,10 @@
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️ ### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑 #### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
<p> <p>
If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> 👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
Any support is appreciated and helps keep the project going. Any support is appreciated and helps keep the project going.
</p> </p>
@@ -160,9 +161,15 @@
### 🪟 Windows Prequisites ### 🪟 Windows Prequisites
* **Java JDK 25:** Download via [Adoptium](https://adoptium.net/temurin/releases/?version=25) or [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows) * **
* **Latest Visual Studio Redist:** Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/) * **Java JDK 25:**
* **ENABLE MULTIPLAYER:** // TODO MULTIPLAYER GUIDE; FIREWALL GUIDE AND SUCH * [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
* Download from any vendor if your OS is not Windows with ARM64 architecture.
* **Latest Visual Studio Redist:**
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
### 🐧 Linux Prequisites ### 🐧 Linux Prequisites

View File

@@ -179,8 +179,28 @@ async function getModsPath(customInstallPath = null) {
const profilesPath = path.join(userDataPath, 'Profiles'); const profilesPath = path.join(userDataPath, 'Profiles');
if (!fs.existsSync(modsPath)) { if (!fs.existsSync(modsPath)) {
// Ensure the Mods directory exists // Check for broken symlink to avoid EEXIST/EPERM on mkdir
fs.mkdirSync(modsPath, { recursive: true }); let isBrokenLink = false;
let pathExists = false;
try {
const stats = fs.lstatSync(modsPath);
pathExists = true;
if (stats.isSymbolicLink()) {
// Check if target exists
try {
fs.statSync(modsPath);
} catch {
isBrokenLink = true;
}
}
} catch (e) { /* path doesn't exist at all */ }
if (isBrokenLink) {
fs.unlinkSync(modsPath); // Remove broken symlink
}
if (!pathExists || isBrokenLink) {
fs.mkdirSync(modsPath, { recursive: true });
}
} }
if (!fs.existsSync(disabledModsPath)) { if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true }); fs.mkdirSync(disabledModsPath, { recursive: true });

View File

@@ -365,7 +365,21 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
if (fs.existsSync(gameDir)) { if (fs.existsSync(gameDir)) {
console.log('Removing old game files...'); console.log('Removing old game files...');
fs.rmSync(gameDir, { recursive: true, force: true }); let retries = 3;
while (retries > 0) {
try {
fs.rmSync(gameDir, { recursive: true, force: true });
break;
} catch (err) {
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
retries--;
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
throw err;
}
}
}
} }
fs.renameSync(tempUpdateDir, gameDir); fs.renameSync(tempUpdateDir, gameDir);

View File

@@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
const { getOS } = require('../utils/platformUtils');
const { getModsPath, getProfilesDir } = require('../core/paths'); const { getModsPath, getProfilesDir } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager'); const profileManager = require('./profileManager');
@@ -307,11 +308,16 @@ async function syncModsForCurrentProfile() {
// 2. Symlink / Migration Logic // 2. Symlink / Migration Logic
let needsLink = false; let needsLink = false;
let globalStats = null;
if (fs.existsSync(globalModsPath)) { try {
const stats = fs.lstatSync(globalModsPath); globalStats = fs.lstatSync(globalModsPath);
} catch (e) {
// Path doesn't exist
}
if (stats.isSymbolicLink()) { if (globalStats) {
if (globalStats.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(globalModsPath); const linkTarget = fs.readlinkSync(globalModsPath);
// Normalize paths for comparison // Normalize paths for comparison
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) { if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
@@ -319,7 +325,7 @@ async function syncModsForCurrentProfile() {
fs.unlinkSync(globalModsPath); fs.unlinkSync(globalModsPath);
needsLink = true; needsLink = true;
} }
} else if (stats.isDirectory()) { } else if (globalStats.isDirectory()) {
// MIGRATION: It's a real directory. Move contents to profile. // MIGRATION: It's a real directory. Move contents to profile.
console.log('[ModManager] Migrating global mods folder to profile folder...'); console.log('[ModManager] Migrating global mods folder to profile folder...');
const files = fs.readdirSync(globalModsPath); const files = fs.readdirSync(globalModsPath);
@@ -349,7 +355,20 @@ async function syncModsForCurrentProfile() {
// Remove the directory so we can link it // Remove the directory so we can link it
try { try {
fs.rmSync(globalModsPath, { recursive: true, force: true }); let retries = 3;
while (retries > 0) {
try {
fs.rmSync(globalModsPath, { recursive: true, force: true });
break;
} catch (err) {
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
retries--;
await new Promise(resolve => setTimeout(resolve, 500));
} else {
throw err;
}
}
}
needsLink = true; needsLink = true;
} catch (e) { } catch (e) {
console.error('Failed to remove global mods dir:', e); console.error('Failed to remove global mods dir:', e);
@@ -364,8 +383,8 @@ async function syncModsForCurrentProfile() {
if (needsLink) { if (needsLink) {
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`); console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
try { try {
// 'junction' is key for Windows without admin const symlinkType = getOS() === 'windows' ? 'junction' : 'dir';
fs.symlinkSync(profileModsPath, globalModsPath, 'junction'); fs.symlinkSync(profileModsPath, globalModsPath, symlinkType);
} catch (err) { } catch (err) {
// If we can't create the symlink, try creating the directory first // If we can't create the symlink, try creating the directory first
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.'); console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.1.0", "version": "2.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.1.0", "version": "2.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.1.0", "version": "2.1.1",
"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://github.com/amiayweb/Hytale-F2P", "homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js", "main": "main.js",