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

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