mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 06:41:47 -03:00
feat: auto-resume download process & auto-retry if disconnected (#143)
This commit is contained in:
@@ -602,7 +602,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
||||
<div id="progressOverlay" class="progress-overlay" style="display: none;">
|
||||
<div class="progress-content">
|
||||
<div class="progress-info">
|
||||
<span id="progressText" data-i18n="progress.initializing">Initializing...</span>
|
||||
@@ -615,6 +615,15 @@
|
||||
<span id="progressSpeed"></span>
|
||||
<span id="progressSize"></span>
|
||||
</div>
|
||||
<div id="progressErrorContainer" class="progress-error-container" style="display: none;">
|
||||
<div id="progressErrorMessage" class="progress-error-message"></div>
|
||||
<div class="progress-retry-section">
|
||||
<span id="progressRetryInfo" class="progress-retry-info"></span>
|
||||
<button id="progressRetryBtn" class="progress-retry-btn" style="display: none;">
|
||||
🔄 Retry Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
273
GUI/js/ui.js
273
GUI/js/ui.js
@@ -6,6 +6,22 @@ let progressText;
|
||||
let progressPercent;
|
||||
let progressSpeed;
|
||||
let progressSize;
|
||||
let progressErrorContainer;
|
||||
let progressErrorMessage;
|
||||
let progressRetryInfo;
|
||||
let progressRetryBtn;
|
||||
|
||||
// Download retry state
|
||||
let currentDownloadState = {
|
||||
isDownloading: false,
|
||||
canRetry: false,
|
||||
retryData: null,
|
||||
lastError: null,
|
||||
errorType: null,
|
||||
branch: null,
|
||||
fileName: null,
|
||||
cacheDir: null
|
||||
};
|
||||
|
||||
function showPage(pageId) {
|
||||
const pages = document.querySelectorAll('.page');
|
||||
@@ -144,6 +160,12 @@ function hideProgress() {
|
||||
}
|
||||
|
||||
function updateProgress(data) {
|
||||
// Handle retry state
|
||||
if (data.retryState) {
|
||||
currentDownloadState.retryData = data.retryState;
|
||||
updateRetryState(data.retryState);
|
||||
}
|
||||
|
||||
if (data.message && progressText) {
|
||||
progressText.textContent = data.message;
|
||||
}
|
||||
@@ -162,6 +184,82 @@ function updateProgress(data) {
|
||||
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
|
||||
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
|
||||
}
|
||||
|
||||
// Handle error states with enhanced categorization
|
||||
// Don't show error during automatic retries - let the retry message display instead
|
||||
if ((data.error || (data.message && data.message.includes('failed'))) &&
|
||||
!(data.retryState && data.retryState.isAutomaticRetry)) {
|
||||
const errorType = categorizeError(data.message);
|
||||
showDownloadError(data.message, data.canRetry, errorType);
|
||||
} else if (data.percent === 100) {
|
||||
hideDownloadError();
|
||||
} else if (data.retryState && data.retryState.isAutomaticRetry) {
|
||||
// Hide any existing error during automatic retries
|
||||
hideDownloadError();
|
||||
}
|
||||
}
|
||||
|
||||
function updateRetryState(retryState) {
|
||||
if (!progressRetryInfo) return;
|
||||
|
||||
if (retryState.isAutomaticRetry && retryState.automaticStallRetries > 0) {
|
||||
// Show automatic stall retry count
|
||||
progressRetryInfo.textContent = `Auto-retry ${retryState.automaticStallRetries}/3`;
|
||||
progressRetryInfo.style.display = 'block';
|
||||
progressRetryInfo.style.background = 'rgba(255, 193, 7, 0.2)'; // Light orange background for auto-retries
|
||||
progressRetryInfo.style.color = '#ff9800'; // Orange text for auto-retries
|
||||
} else if (retryState.attempts > 1) {
|
||||
// Show manual retry count
|
||||
progressRetryInfo.textContent = `Attempt ${retryState.attempts}/${retryState.maxRetries}`;
|
||||
progressRetryInfo.style.display = 'block';
|
||||
progressRetryInfo.style.background = ''; // Reset background
|
||||
progressRetryInfo.style.color = ''; // Reset color
|
||||
} else {
|
||||
progressRetryInfo.style.display = 'none';
|
||||
progressRetryInfo.style.background = ''; // Reset background
|
||||
progressRetryInfo.style.color = ''; // Reset color
|
||||
}
|
||||
}
|
||||
|
||||
function showDownloadError(errorMessage, canRetry = true, errorType = 'general') {
|
||||
if (!progressErrorContainer || !progressErrorMessage || !progressRetryBtn) return;
|
||||
|
||||
currentDownloadState.lastError = errorMessage;
|
||||
currentDownloadState.canRetry = canRetry;
|
||||
currentDownloadState.errorType = errorType;
|
||||
|
||||
// Update retry context if available
|
||||
if (data && data.retryData) {
|
||||
currentDownloadState.branch = data.retryData.branch;
|
||||
currentDownloadState.fileName = data.retryData.fileName;
|
||||
currentDownloadState.cacheDir = data.retryData.cacheDir;
|
||||
}
|
||||
|
||||
// User-friendly error messages
|
||||
const userMessage = getErrorMessage(errorMessage, errorType);
|
||||
progressErrorMessage.textContent = userMessage;
|
||||
progressErrorContainer.style.display = 'block';
|
||||
progressRetryBtn.style.display = canRetry ? 'block' : 'none';
|
||||
|
||||
// Add visual indicators based on error type
|
||||
progressErrorContainer.className = `progress-error-container error-${errorType}`;
|
||||
|
||||
if (progressOverlay) {
|
||||
progressOverlay.classList.add('error-state');
|
||||
}
|
||||
}
|
||||
|
||||
function hideDownloadError() {
|
||||
if (!progressErrorContainer) return;
|
||||
|
||||
progressErrorContainer.style.display = 'none';
|
||||
currentDownloadState.canRetry = false;
|
||||
currentDownloadState.lastError = null;
|
||||
currentDownloadState.errorType = null;
|
||||
|
||||
if (progressOverlay) {
|
||||
progressOverlay.classList.remove('error-state');
|
||||
}
|
||||
}
|
||||
|
||||
function setupAnimations() {
|
||||
@@ -478,10 +576,17 @@ function setupUI() {
|
||||
progressPercent = document.getElementById('progressPercent');
|
||||
progressSpeed = document.getElementById('progressSpeed');
|
||||
progressSize = document.getElementById('progressSize');
|
||||
progressErrorContainer = document.getElementById('progressErrorContainer');
|
||||
progressErrorMessage = document.getElementById('progressErrorMessage');
|
||||
progressRetryInfo = document.getElementById('progressRetryInfo');
|
||||
progressRetryBtn = document.getElementById('progressRetryBtn');
|
||||
|
||||
// Setup draggable progress bar
|
||||
setupProgressDrag();
|
||||
|
||||
// Setup retry button
|
||||
setupRetryButton();
|
||||
|
||||
lockPlayButton(true);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -648,6 +753,174 @@ function toggleMaximize() {
|
||||
}
|
||||
}
|
||||
|
||||
// Error categorization and user-friendly messages
|
||||
function categorizeError(message) {
|
||||
const msg = message.toLowerCase();
|
||||
|
||||
if (msg.includes('network') || msg.includes('connection') || msg.includes('offline')) {
|
||||
return 'network';
|
||||
} else if (msg.includes('stalled') || msg.includes('timeout')) {
|
||||
return 'stall';
|
||||
} else if (msg.includes('file') || msg.includes('disk')) {
|
||||
return 'file';
|
||||
} else if (msg.includes('permission') || msg.includes('access')) {
|
||||
return 'permission';
|
||||
} else if (msg.includes('server') || msg.includes('5')) {
|
||||
return 'server';
|
||||
} else if (msg.includes('corrupted') || msg.includes('pwr file') || msg.includes('unexpected eof')) {
|
||||
return 'corruption';
|
||||
} else if (msg.includes('butler') || msg.includes('patch installation')) {
|
||||
return 'butler';
|
||||
} else if (msg.includes('space') || msg.includes('full') || msg.includes('device full')) {
|
||||
return 'space';
|
||||
} else if (msg.includes('conflict') || msg.includes('already exists')) {
|
||||
return 'conflict';
|
||||
} else {
|
||||
return 'general';
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(technicalMessage, errorType) {
|
||||
// Technical errors go to console, user gets friendly messages
|
||||
console.error(`Download error [${errorType}]:`, technicalMessage);
|
||||
|
||||
switch (errorType) {
|
||||
case 'network':
|
||||
return 'Network connection lost. Please check your internet connection and retry.';
|
||||
case 'stall':
|
||||
return 'Download stalled due to slow connection. Please retry.';
|
||||
case 'file':
|
||||
return 'Unable to save file. Check disk space and permissions. Please retry.';
|
||||
case 'permission':
|
||||
return 'Permission denied. Check if launcher has write access. Please retry.';
|
||||
case 'server':
|
||||
return 'Server error. Please wait a moment and retry.';
|
||||
case 'corruption':
|
||||
return 'Corrupted PWR file detected. File deleted and will retry.';
|
||||
case 'butler':
|
||||
return 'Patch installation failed. Please retry.';
|
||||
case 'space':
|
||||
return 'Insufficient disk space. Free up space and retry.';
|
||||
case 'conflict':
|
||||
return 'Installation directory conflict. Please retry.';
|
||||
default:
|
||||
return 'Download failed. Please retry.';
|
||||
}
|
||||
}
|
||||
|
||||
// Connection quality indicator (simplified)
|
||||
function updateConnectionQuality(quality) {
|
||||
if (!progressSize) return;
|
||||
|
||||
const qualityColors = {
|
||||
'Good': '#10b981',
|
||||
'Fair': '#fbbf24',
|
||||
'Poor': '#f87171'
|
||||
};
|
||||
|
||||
const color = qualityColors[quality] || '#6b7280';
|
||||
progressSize.style.color = color;
|
||||
|
||||
// Add subtle quality indicator
|
||||
if (progressSize.dataset.quality !== quality) {
|
||||
progressSize.dataset.quality = quality;
|
||||
progressSize.style.transition = 'color 0.5s ease';
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced retry button setup
|
||||
function setupRetryButton() {
|
||||
if (!progressRetryBtn) return;
|
||||
|
||||
progressRetryBtn.addEventListener('click', async () => {
|
||||
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable retry button during retry
|
||||
progressRetryBtn.disabled = true;
|
||||
progressRetryBtn.textContent = '🔄 Retrying...';
|
||||
progressRetryBtn.classList.add('retrying');
|
||||
currentDownloadState.isDownloading = true;
|
||||
|
||||
try {
|
||||
// Hide error state during retry
|
||||
hideDownloadError();
|
||||
|
||||
// Reset retry info styling for manual retries
|
||||
if (progressRetryInfo) {
|
||||
progressRetryInfo.style.background = '';
|
||||
progressRetryInfo.style.color = '';
|
||||
}
|
||||
|
||||
// Update progress text with context-aware message
|
||||
if (progressText) {
|
||||
const contextMessage = getRetryContextMessage();
|
||||
progressText.textContent = contextMessage;
|
||||
}
|
||||
|
||||
// Ensure retry data exists, create defaults if null
|
||||
if (!currentDownloadState.retryData) {
|
||||
currentDownloadState.retryData = {
|
||||
branch: 'release',
|
||||
fileName: '4.pwr'
|
||||
};
|
||||
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
|
||||
}
|
||||
|
||||
// Send retry request to backend
|
||||
if (window.electronAPI && window.electronAPI.retryDownload) {
|
||||
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Retry failed');
|
||||
}
|
||||
} else {
|
||||
// Fallback for development/testing
|
||||
console.warn('electronAPI.retryDownload not available, simulating retry...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
throw new Error('Retry API not available');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Retry failed:', error);
|
||||
const errorType = categorizeError(error.message);
|
||||
showDownloadError(`Retry failed: ${error.message}`, true, errorType);
|
||||
|
||||
// Reset retry button
|
||||
progressRetryBtn.disabled = false;
|
||||
progressRetryBtn.textContent = '🔄 Retry Download';
|
||||
progressRetryBtn.classList.remove('retrying');
|
||||
currentDownloadState.isDownloading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getRetryContextMessage() {
|
||||
const errorType = currentDownloadState.errorType;
|
||||
|
||||
switch (errorType) {
|
||||
case 'network':
|
||||
return 'Reconnecting and retrying download...';
|
||||
case 'stall':
|
||||
return 'Resuming stalled download...';
|
||||
case 'server':
|
||||
return 'Waiting for server and retrying...';
|
||||
case 'corruption':
|
||||
return 'Re-downloading corrupted PWR file...';
|
||||
case 'butler':
|
||||
return 'Re-attempting patch installation...';
|
||||
case 'space':
|
||||
return 'Retrying after clearing disk space...';
|
||||
case 'permission':
|
||||
return 'Retrying with corrected permissions...';
|
||||
case 'conflict':
|
||||
return 'Retrying after resolving conflicts...';
|
||||
default:
|
||||
return 'Initiating retry download...';
|
||||
}
|
||||
}
|
||||
|
||||
// Make toggleMaximize globally available
|
||||
window.toggleMaximize = toggleMaximize;
|
||||
|
||||
|
||||
231
GUI/style.css
231
GUI/style.css
@@ -1780,6 +1780,237 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress Error and Retry Styles */
|
||||
.progress-error-container {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(239, 68, 68, 0.3);
|
||||
animation: errorSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes errorSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-error-message {
|
||||
color: #f87171;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.progress-retry-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-retry-info {
|
||||
color: #fbbf24;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-retry-btn {
|
||||
background: linear-gradient(135deg, #dc2626, #ef4444);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.progress-retry-btn:hover {
|
||||
background: linear-gradient(135deg, #b91c1c, #dc2626);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.progress-retry-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.progress-retry-btn:disabled {
|
||||
background: linear-gradient(135deg, #4b5563, #6b7280);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Progress overlay error state */
|
||||
.progress-overlay.error-state {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.5),
|
||||
0 0 30px rgba(239, 68, 68, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.progress-overlay.error-state #progressBarFill {
|
||||
background: linear-gradient(90deg, #dc2626, #ef4444);
|
||||
animation: errorPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes errorPulse {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error type specific styling */
|
||||
.progress-error-container.error-network {
|
||||
border-top-color: rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-network .progress-error-message {
|
||||
color: #60a5fa;
|
||||
text-shadow: 0 0 8px rgba(96, 165, 250, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-stall {
|
||||
border-top-color: rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-stall .progress-error-message {
|
||||
color: #fbbf24;
|
||||
text-shadow: 0 0 8px rgba(251, 191, 36, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-file {
|
||||
border-top-color: rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-file .progress-error-message {
|
||||
color: #f87171;
|
||||
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-permission {
|
||||
border-top-color: rgba(168, 85, 247, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-permission .progress-error-message {
|
||||
color: #a855f7;
|
||||
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-server {
|
||||
border-top-color: rgba(236, 72, 153, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-server .progress-error-message {
|
||||
color: #ec4899;
|
||||
text-shadow: 0 0 8px rgba(236, 72, 153, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-corruption {
|
||||
border-top-color: rgba(220, 38, 38, 0.8);
|
||||
}
|
||||
|
||||
.progress-error-container.error-corruption .progress-error-message {
|
||||
color: #dc2626;
|
||||
text-shadow: 0 0 8px rgba(220, 38, 38, 0.6);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-error-container.error-butler {
|
||||
border-top-color: rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-butler .progress-error-message {
|
||||
color: #f59e0b;
|
||||
text-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-space {
|
||||
border-top-color: rgba(168, 85, 247, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-space .progress-error-message {
|
||||
color: #a855f7;
|
||||
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
|
||||
.progress-error-container.error-conflict {
|
||||
border-top-color: rgba(6, 182, 212, 0.5);
|
||||
}
|
||||
|
||||
.progress-error-container.error-conflict .progress-error-message {
|
||||
color: #06b6d4;
|
||||
text-shadow: 0 0 8px rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
/* Connection quality indicators */
|
||||
.progress-details {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-details #progressSize {
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Enhanced retry button states */
|
||||
.progress-retry-btn.retrying {
|
||||
background: linear-gradient(135deg, #059669, #10b981);
|
||||
animation: retryingPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes retryingPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Network status indicator (optional future enhancement) */
|
||||
.network-status {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 6px rgba(16, 185, 129, 0.6);
|
||||
}
|
||||
|
||||
.network-status.poor {
|
||||
background: #f87171;
|
||||
box-shadow: 0 0 6px rgba(248, 113, 113, 0.6);
|
||||
}
|
||||
|
||||
.network-status.fair {
|
||||
background: #fbbf24;
|
||||
box-shadow: 0 0 6px rgba(251, 191, 36, 0.6);
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
|
||||
@@ -3,15 +3,15 @@ const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { downloadFile } = require('../utils/fileManager');
|
||||
const { getLatestClientVersion } = require('../services/versionManager');
|
||||
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
||||
const { installButler } = require('./butlerManager');
|
||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||
const userDataBackup = require('../utils/userDataBackup');
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) {
|
||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||
const osName = getOS();
|
||||
const arch = getArch();
|
||||
|
||||
@@ -20,42 +20,142 @@ async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallb
|
||||
}
|
||||
|
||||
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
|
||||
|
||||
const dest = path.join(cacheDir, `${branch}_${fileName}`);
|
||||
|
||||
// Check if file exists and validate it
|
||||
if (fs.existsSync(dest)) {
|
||||
if (fs.existsSync(dest) && !manualRetry) {
|
||||
console.log('PWR file found in cache:', dest);
|
||||
|
||||
// Validate file size (PWR files should be > 1MB)
|
||||
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads)
|
||||
const stats = fs.statSync(dest);
|
||||
if (stats.size < 1024 * 1024) {
|
||||
console.log('Cached PWR file seems corrupted (too small), re-downloading...');
|
||||
fs.unlinkSync(dest);
|
||||
} else {
|
||||
return dest;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is under 1.5 GB (incomplete download)
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
if (sizeInMB < 1500) {
|
||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Fetching PWR patch file:', url);
|
||||
await downloadFile(url, dest, progressCallback);
|
||||
|
||||
// Validate downloaded file
|
||||
try {
|
||||
if (manualRetry) {
|
||||
await retryDownload(url, dest, progressCallback);
|
||||
} else {
|
||||
await downloadFile(url, dest, progressCallback);
|
||||
}
|
||||
} catch (error) {
|
||||
// Check for automatic stall retry conditions (only for stall errors, not manual retries)
|
||||
if (!manualRetry &&
|
||||
error.message &&
|
||||
error.message.includes('stalled') &&
|
||||
error.canRetry !== false && // Explicitly check it's not false
|
||||
(!error.retryState || error.retryState.automaticStallRetries < MAX_AUTOMATIC_STALL_RETRIES)) {
|
||||
|
||||
console.log(`[PWR] Automatic stall retry triggered (${(error.retryState && error.retryState.automaticStallRetries || 0) + 1}/${MAX_AUTOMATIC_STALL_RETRIES})`);
|
||||
|
||||
try {
|
||||
await retryStalledDownload(url, dest, progressCallback, error);
|
||||
console.log('[PWR] Automatic stall retry successful');
|
||||
|
||||
// After successful automatic retry, continue with normal flow - the file should be valid now
|
||||
const retryStats = fs.statSync(dest);
|
||||
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
if (!validatePWRFile(dest)) {
|
||||
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
||||
fs.unlinkSync(dest);
|
||||
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
||||
}
|
||||
|
||||
|
||||
} catch (retryError) {
|
||||
console.error('[PWR] Automatic stall retry failed:', retryError.message);
|
||||
|
||||
// Create enhanced error with updated retry state
|
||||
const enhancedError = new Error(`PWR download failed after automatic retries: ${retryError.message}`);
|
||||
enhancedError.originalError = retryError;
|
||||
enhancedError.retryState = retryError.retryState || error.retryState || null;
|
||||
enhancedError.canRetry = true; // Still allow manual retry
|
||||
enhancedError.pwrUrl = url;
|
||||
enhancedError.pwrDest = dest;
|
||||
enhancedError.branch = branch;
|
||||
enhancedError.fileName = fileName;
|
||||
enhancedError.cacheDir = cacheDir;
|
||||
enhancedError.automaticRetriesExhausted = true;
|
||||
throw enhancedError;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced error handling for retry UI (non-stall errors or exhausted automatic retries)
|
||||
const enhancedError = new Error(`PWR download failed: ${error.message}`);
|
||||
enhancedError.originalError = error;
|
||||
enhancedError.retryState = error.retryState || null;
|
||||
enhancedError.canRetry = error.isConnectionLost ? false : (error.canRetry !== false); // Don't allow retry for connection lost
|
||||
enhancedError.pwrUrl = url;
|
||||
enhancedError.pwrDest = dest;
|
||||
enhancedError.branch = branch;
|
||||
enhancedError.fileName = fileName;
|
||||
enhancedError.cacheDir = cacheDir;
|
||||
enhancedError.isConnectionLost = error.isConnectionLost || false;
|
||||
|
||||
console.log(`[PWR] Error handling:`, {
|
||||
message: enhancedError.message,
|
||||
isConnectionLost: enhancedError.isConnectionLost,
|
||||
canRetry: enhancedError.canRetry,
|
||||
retryState: enhancedError.retryState
|
||||
});
|
||||
|
||||
throw enhancedError;
|
||||
}
|
||||
|
||||
// Enhanced PWR file validation
|
||||
const stats = fs.statSync(dest);
|
||||
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
if (stats.size < 1024 * 1024) {
|
||||
if (!validatePWRFile(dest)) {
|
||||
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
||||
fs.unlinkSync(dest);
|
||||
throw new Error('Downloaded PWR file is corrupted (file too small)');
|
||||
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
||||
}
|
||||
|
||||
console.log('PWR saved to:', dest);
|
||||
console.log(`[PWR Validation] PWR file validation passed: ${dest}`);
|
||||
|
||||
return dest;
|
||||
}
|
||||
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR) {
|
||||
// Manual retry function for PWR downloads
|
||||
async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = CACHE_DIR) {
|
||||
console.log('Initiating manual PWR retry...');
|
||||
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
||||
}
|
||||
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
||||
console.log(`[Butler] Starting PWR application with:`);
|
||||
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
||||
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
||||
console.log(`[Butler] - Game dir: ${gameDir}`);
|
||||
console.log(`[Butler] - Branch: ${branch}`);
|
||||
console.log(`[Butler] - Cache dir: ${cacheDir}`);
|
||||
|
||||
// Validate PWR file exists and get diagnostic info
|
||||
if (!pwrFile || typeof pwrFile !== 'string' || !fs.existsSync(pwrFile)) {
|
||||
throw new Error(`PWR file not found: ${pwrFile || 'undefined'}. Please retry download.`);
|
||||
}
|
||||
|
||||
const pwrStats = fs.statSync(pwrFile);
|
||||
console.log(`[Butler] PWR file size: ${(pwrStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`[Butler] PWR file exists: ${fs.existsSync(pwrFile)}`);
|
||||
|
||||
const butlerPath = await installButler(toolsDir);
|
||||
console.log(`[Butler] Butler path: ${butlerPath}`);
|
||||
console.log(`[Butler] Butler executable: ${fs.existsSync(butlerPath)}`);
|
||||
|
||||
const gameLatest = gameDir;
|
||||
const stagingDir = path.join(gameLatest, 'staging-temp');
|
||||
|
||||
@@ -66,12 +166,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(gameLatest)) {
|
||||
fs.mkdirSync(gameLatest, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(stagingDir)) {
|
||||
fs.mkdirSync(stagingDir, { recursive: true });
|
||||
}
|
||||
// Validate and prepare directories
|
||||
validateGameDirectory(gameLatest, stagingDir);
|
||||
|
||||
console.log(`[Butler] Game directory validated: ${gameLatest}`);
|
||||
console.log(`[Butler] Staging directory validated: ${stagingDir}`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Installing game patch...', null, null, null, null);
|
||||
@@ -95,6 +194,8 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
||||
gameLatest
|
||||
];
|
||||
|
||||
console.log(`[Butler] Executing command: ${butlerPath} ${args.join(' ')}`);
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = execFile(butlerPath, args, {
|
||||
@@ -102,32 +203,97 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
||||
timeout: 600000
|
||||
}, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('Butler stderr:', stderr);
|
||||
console.error('Butler stdout:', stdout);
|
||||
console.error('[Butler] stderr:', stderr);
|
||||
console.error('[Butler] stdout:', stdout);
|
||||
console.error('[Butler] error code:', error.code);
|
||||
console.error('[Butler] error signal:', error.signal);
|
||||
|
||||
// Check for EOF error (corrupted PWR file)
|
||||
if (stderr && stderr.includes('unexpected EOF')) {
|
||||
// Delete corrupted PWR file
|
||||
console.log('Corrupted PWR file detected, deleting:', pwrFile);
|
||||
// Enhanced error pattern detection
|
||||
const errorPatterns = {
|
||||
'unexpected EOF': {
|
||||
message: 'Corrupted PWR file detected and deleted. Please try launching the game again.',
|
||||
shouldDeletePWR: true
|
||||
},
|
||||
'permission denied': {
|
||||
message: 'Permission denied. Check file permissions and try again.',
|
||||
shouldDeletePWR: false
|
||||
},
|
||||
'no space left': {
|
||||
message: 'Insufficient disk space. Free up space and try again.',
|
||||
shouldDeletePWR: false
|
||||
},
|
||||
'device full': {
|
||||
message: 'Insufficient disk space. Free up space and try again.',
|
||||
shouldDeletePWR: false
|
||||
},
|
||||
'already exists': {
|
||||
message: 'Installation directory conflict. Clean directories and retry.',
|
||||
shouldDeletePWR: false
|
||||
},
|
||||
'network error': {
|
||||
message: 'Network error during patch installation. Please retry.',
|
||||
shouldDeletePWR: false
|
||||
},
|
||||
'connection refused': {
|
||||
message: 'Connection refused. Check network and retry.',
|
||||
shouldDeletePWR: false
|
||||
}
|
||||
};
|
||||
|
||||
let enhancedMessage = `Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`;
|
||||
let shouldDeletePWR = false;
|
||||
|
||||
// Check error patterns
|
||||
const errorText = (stderr + ' ' + error.message).toLowerCase();
|
||||
for (const [pattern, config] of Object.entries(errorPatterns)) {
|
||||
if (errorText.includes(pattern)) {
|
||||
enhancedMessage = config.message;
|
||||
shouldDeletePWR = config.shouldDeletePWR;
|
||||
console.log(`[Butler] Pattern matched: ${pattern}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete corrupted PWR file if needed
|
||||
if (shouldDeletePWR) {
|
||||
try {
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
console.log('Corrupted PWR file deleted. Please try again to re-download.');
|
||||
console.log('[Butler] Corrupted PWR file deleted:', pwrFile);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.error('Failed to delete corrupted PWR file:', delErr);
|
||||
console.error('[Butler] Failed to delete corrupted PWR file:', delErr);
|
||||
}
|
||||
reject(new Error(`Corrupted PWR file detected and deleted. Please try launching the game again.`));
|
||||
} else {
|
||||
reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`));
|
||||
}
|
||||
|
||||
// Enhanced error with retry context
|
||||
const enhancedError = new Error(enhancedMessage);
|
||||
enhancedError.canRetry = true;
|
||||
enhancedError.branch = branch;
|
||||
enhancedError.fileName = path.basename(pwrFile);
|
||||
enhancedError.cacheDir = cacheDir;
|
||||
enhancedError.butlerError = true;
|
||||
enhancedError.errorCode = error.code;
|
||||
enhancedError.stderr = stderr;
|
||||
enhancedError.stdout = stdout;
|
||||
|
||||
console.log('[Butler] Enhanced error created with retry context');
|
||||
reject(enhancedError);
|
||||
} else {
|
||||
console.log('[Butler] Patch installation completed successfully');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
console.error('[Butler] Exception during Butler execution:', error);
|
||||
const enhancedError = new Error(`Butler execution failed: ${error.message}`);
|
||||
enhancedError.canRetry = true;
|
||||
enhancedError.branch = branch;
|
||||
enhancedError.fileName = path.basename(pwrFile);
|
||||
enhancedError.cacheDir = cacheDir;
|
||||
enhancedError.butlerError = true;
|
||||
throw enhancedError;
|
||||
}
|
||||
|
||||
if (fs.existsSync(stagingDir)) {
|
||||
@@ -191,7 +357,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
progressCallback('Extracting new files...', 60, null, null, null);
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
|
||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Replacing game files...', 80, null, null, null);
|
||||
@@ -369,8 +535,28 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
console.log(`Installing game files for branch: ${branch}...`);
|
||||
|
||||
const latestVersion = await getLatestClientVersion(branch);
|
||||
const pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
|
||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
||||
let pwrFile;
|
||||
try {
|
||||
pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
|
||||
|
||||
// If downloadPWR returns false, it means the file doesn't exist or is invalid
|
||||
// We should retry the download with a manual retry flag
|
||||
if (!pwrFile) {
|
||||
console.log('[Install] PWR file not found or invalid, attempting retry...');
|
||||
pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir);
|
||||
}
|
||||
|
||||
// Double-check we have a valid file path
|
||||
if (!pwrFile || typeof pwrFile !== 'string') {
|
||||
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
|
||||
}
|
||||
|
||||
} catch (downloadError) {
|
||||
console.error('[Install] PWR download failed:', downloadError.message);
|
||||
throw downloadError; // Re-throw to be handled by the main installGame error handler
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
|
||||
|
||||
// Save the installed version and branch to config
|
||||
saveVersionClient(latestVersion);
|
||||
@@ -546,8 +732,79 @@ async function repairGame(progressCallback, branchOverride = null) {
|
||||
return { success: true, repaired: true };
|
||||
}
|
||||
|
||||
// Directory validation and cleanup function
|
||||
function validateGameDirectory(gameDir, stagingDir) {
|
||||
try {
|
||||
// Ensure game directory exists and is writable
|
||||
if (!fs.existsSync(gameDir)) {
|
||||
fs.mkdirSync(gameDir, { recursive: true });
|
||||
console.log(`[Butler] Created game directory: ${gameDir}`);
|
||||
}
|
||||
|
||||
// Test write permissions
|
||||
const testFile = path.join(gameDir, '.permission_test');
|
||||
fs.writeFileSync(testFile, 'test');
|
||||
fs.unlinkSync(testFile);
|
||||
console.log(`[Butler] Game directory is writable: ${gameDir}`);
|
||||
|
||||
// Clean and ensure staging directory
|
||||
if (fs.existsSync(stagingDir)) {
|
||||
console.log(`[Butler] Cleaning existing staging directory: ${stagingDir}`);
|
||||
fs.rmSync(stagingDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(stagingDir, { recursive: true });
|
||||
console.log(`[Butler] Created clean staging directory: ${stagingDir}`);
|
||||
|
||||
// Check disk space (basic check)
|
||||
const freeSpace = fs.statSync(gameDir);
|
||||
console.log(`[Butler] Directory validation completed successfully`);
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Directory validation failed: ${error.message}. Please check permissions and disk space.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced PWR file validation
|
||||
function validatePWRFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
|
||||
if (stats.size < 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is under 1.5 GB (incomplete download)
|
||||
if (sizeInMB < 1500) {
|
||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic file header validation (PWR files should have specific headers)
|
||||
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
|
||||
if (buffer.length < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common PWR magic bytes or patterns
|
||||
// This is a basic check - could be enhanced with actual PWR format specification
|
||||
const header = buffer.toString('hex', 0, 10);
|
||||
console.log(`[PWR Validation] File header: ${header}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[PWR Validation] Error:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadPWR,
|
||||
retryPWRDownload,
|
||||
applyPWR,
|
||||
updateGameFiles,
|
||||
isGameInstalled,
|
||||
|
||||
@@ -2,16 +2,116 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
||||
// Automatic stall retry constants
|
||||
const MAX_AUTOMATIC_STALL_RETRIES = 3;
|
||||
const AUTOMATIC_STALL_RETRY_DELAY = 3000; // 3 seconds in milliseconds
|
||||
|
||||
// Network monitoring utilities using Node.js built-in methods
|
||||
function checkNetworkConnection() {
|
||||
return new Promise((resolve) => {
|
||||
const { lookup } = require('dns');
|
||||
const http = require('http');
|
||||
|
||||
// Try DNS lookup first (faster) - using callback version
|
||||
lookup('8.8.8.8', (err) => {
|
||||
if (err) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try HTTP request to confirm internet connectivity
|
||||
const req = http.get('http://www.google.com', { timeout: 3000 }, (res) => {
|
||||
resolve(true);
|
||||
res.destroy();
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
|
||||
let lastError = null;
|
||||
let retryState = {
|
||||
attempts: 0,
|
||||
maxRetries: maxRetries,
|
||||
canRetry: true,
|
||||
lastError: null,
|
||||
automaticStallRetries: 0,
|
||||
isAutomaticRetry: false
|
||||
};
|
||||
let downloadStalled = false;
|
||||
let streamCompleted = false;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
retryState.attempts = attempt + 1;
|
||||
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
||||
|
||||
if (attempt > 0 && progressCallback) {
|
||||
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
|
||||
// Exponential backoff with jitter
|
||||
const baseDelay = 2000;
|
||||
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
||||
const jitter = Math.random() * 1000;
|
||||
const delay = Math.min(exponentialDelay + jitter, 30000);
|
||||
|
||||
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null, retryState);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
// Create AbortController for proper stream control
|
||||
const controller = new AbortController();
|
||||
let hasReceivedData = false;
|
||||
|
||||
// Smart overall timeout - only trigger if no progress for extended period
|
||||
const overallTimeout = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastProgress = now - lastProgressTime;
|
||||
|
||||
// Only timeout if no data received for 5 minutes (300 seconds)
|
||||
if (timeSinceLastProgress > 300000 && hasReceivedData) {
|
||||
console.log('Download stalled for 5 minutes, aborting...');
|
||||
console.log(`Download had progress before stall: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
|
||||
controller.abort();
|
||||
}
|
||||
}, 60000); // Check every minute
|
||||
|
||||
// Check if we can resume existing download
|
||||
let startByte = 0;
|
||||
if (fs.existsSync(dest)) {
|
||||
const existingStats = fs.statSync(dest);
|
||||
|
||||
// Only resume if file exists and is substantial (> 1MB)
|
||||
if (existingStats.size > 1024 * 1024) {
|
||||
startByte = existingStats.size;
|
||||
console.log(`Resuming download from byte ${startByte} (${(existingStats.size / 1024 / 1024).toFixed(2)} MB already downloaded)`);
|
||||
} else {
|
||||
// File too small, start fresh
|
||||
fs.unlinkSync(dest);
|
||||
console.log('Existing file too small, starting fresh download');
|
||||
}
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': 'https://launcher.hytale.com/',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
// Add Range header for resume capability
|
||||
if (startByte > 0) {
|
||||
headers['Range'] = `bytes=${startByte}-`;
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
@@ -19,16 +119,12 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
||||
url: url,
|
||||
responseType: 'stream',
|
||||
timeout: 60000, // 60 secondes timeout
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': 'https://launcher.hytale.com/',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
signal: controller.signal,
|
||||
headers: headers,
|
||||
// Configuration Axios pour la robustesse réseau
|
||||
validateStatus: function (status) {
|
||||
return status >= 200 && status < 300;
|
||||
// Accept both 200 (full download) and 206 (partial content for resume)
|
||||
return (status >= 200 && status < 300) || status === 206;
|
||||
},
|
||||
// Retry configuration
|
||||
maxRedirects: 5,
|
||||
@@ -36,80 +132,214 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
||||
family: 4 // Force IPv4
|
||||
});
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloaded = 0;
|
||||
const contentLength = response.headers['content-length'];
|
||||
const totalSize = contentLength ? parseInt(contentLength, 10) + startByte : 0; // Adjust for resume
|
||||
let downloaded = startByte; // Start with existing bytes
|
||||
let lastProgressTime = Date.now();
|
||||
const startTime = Date.now();
|
||||
|
||||
// Nettoyer le fichier de destination s'il existe
|
||||
if (fs.existsSync(dest)) {
|
||||
fs.unlinkSync(dest);
|
||||
// Check network status before attempting download
|
||||
try {
|
||||
const isNetworkOnline = await checkNetworkConnection();
|
||||
if (!isNetworkOnline) {
|
||||
throw new Error('Network connection unavailable. Please check your connection and retry.');
|
||||
}
|
||||
} catch (networkError) {
|
||||
console.error('[Network] Network check failed, proceeding anyway:', networkError.message);
|
||||
// Continue with download attempt - network check failure shouldn't block
|
||||
}
|
||||
|
||||
const writer = fs.createWriteStream(dest);
|
||||
let downloadStalled = false;
|
||||
const writer = fs.createWriteStream(dest, {
|
||||
flags: startByte > 0 ? 'a' : 'w', // 'a' for append (resume), 'w' for write (fresh)
|
||||
start: startByte > 0 ? startByte : 0
|
||||
});
|
||||
let streamError = null;
|
||||
let stalledTimeout = null;
|
||||
|
||||
// Reset state for this attempt
|
||||
downloadStalled = false;
|
||||
streamCompleted = false;
|
||||
|
||||
// Enhanced stream event handling
|
||||
response.data.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
const now = Date.now();
|
||||
hasReceivedData = true; // Mark that we've received data
|
||||
|
||||
// Reset stalled timer on data received
|
||||
// Reset simple stall timer on data received
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
|
||||
// Set new stalled timer (30 seconds without data = stalled)
|
||||
stalledTimeout = setTimeout(() => {
|
||||
// Set new stall timer (30 seconds without data = stalled)
|
||||
stalledTimeout = setTimeout(async () => {
|
||||
console.log('Download stalled - checking network connectivity...');
|
||||
|
||||
// Check if network is actually available before retrying
|
||||
try {
|
||||
const isNetworkOnline = await checkNetworkConnection();
|
||||
if (!isNetworkOnline) {
|
||||
console.log('Network connection lost - stopping download and showing error');
|
||||
downloadStalled = true;
|
||||
streamError = new Error('Network connection lost. Please check your internet connection and retry.');
|
||||
streamError.isConnectionLost = true;
|
||||
streamError.canRetry = false;
|
||||
controller.abort();
|
||||
writer.destroy();
|
||||
response.data.destroy();
|
||||
// Immediately reject the promise to prevent hanging
|
||||
setTimeout(() => promiseReject(streamError), 100);
|
||||
return;
|
||||
}
|
||||
} catch (networkError) {
|
||||
console.error('Network check failed during stall detection:', networkError.message);
|
||||
}
|
||||
|
||||
console.log('Network available - download stalled due to slow connection, aborting for retry...');
|
||||
downloadStalled = true;
|
||||
streamError = new Error('Download stalled due to slow network connection. Please retry.');
|
||||
controller.abort();
|
||||
writer.destroy();
|
||||
response.data.destroy();
|
||||
// Immediately reject the promise to prevent hanging
|
||||
setTimeout(() => promiseReject(streamError), 100);
|
||||
}, 30000);
|
||||
|
||||
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
||||
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
||||
const elapsed = (now - startTime) / 1000;
|
||||
const speed = elapsed > 0 ? downloaded / elapsed : 0;
|
||||
progressCallback(null, percent, speed, downloaded, totalSize);
|
||||
|
||||
progressCallback(null, percent, speed, downloaded, totalSize, retryState);
|
||||
lastProgressTime = now;
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced stream error handling
|
||||
response.data.on('error', (error) => {
|
||||
// Ignore errors if it was intentionally cancelled or already handled
|
||||
if (downloadStalled || streamCompleted || controller.signal.aborted) {
|
||||
console.log(`Ignoring stream error after cancellation: ${error.code || error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!streamError) {
|
||||
streamError = new Error(`Stream error: ${error.code || error.message}. Please retry.`);
|
||||
// Check for connection lost indicators
|
||||
if (error.code === 'ERR_NETWORK_CHANGED' ||
|
||||
error.code === 'ERR_INTERNET_DISCONNECTED' ||
|
||||
error.code === 'ERR_CONNECTION_LOST') {
|
||||
streamError.isConnectionLost = true;
|
||||
streamError.canRetry = false;
|
||||
}
|
||||
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
|
||||
}
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
}
|
||||
writer.destroy();
|
||||
});
|
||||
|
||||
response.data.on('close', () => {
|
||||
// Only treat as error if not already handled by cancellation and writer didn't complete
|
||||
if (!streamError && !streamCompleted && !downloadStalled && !controller.signal.aborted) {
|
||||
// Check if writer actually completed but stream close came first
|
||||
setTimeout(() => {
|
||||
if (!streamCompleted) {
|
||||
streamError = new Error('Stream closed unexpectedly. Please retry.');
|
||||
console.log('Stream closed unexpectedly on attempt', attempt + 1);
|
||||
}
|
||||
}, 500); // Small delay to check if writer completes
|
||||
}
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('abort', () => {
|
||||
// Only treat as error if not already handled by stall detection
|
||||
if (!streamError && !streamCompleted && !downloadStalled) {
|
||||
streamError = new Error('Download aborted due to network issue. Please retry.');
|
||||
console.log('Stream aborted on attempt', attempt + 1);
|
||||
}
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
let promiseReject = null;
|
||||
await new Promise((resolve, reject) => {
|
||||
// Store promise reject function for immediate use by stall timeout
|
||||
promiseReject = reject;
|
||||
writer.on('finish', () => {
|
||||
streamCompleted = true;
|
||||
console.log(`Writer finished on attempt ${attempt + 1}, downloaded: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
// Clear ALL timeouts to prevent them from firing after completion
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
console.log('Cleared stall timeout after writer finished');
|
||||
}
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
console.log('Cleared overall timeout after writer finished');
|
||||
}
|
||||
|
||||
// Download is successful if writer finished - regardless of stream state
|
||||
if (!downloadStalled) {
|
||||
console.log(`Download completed successfully on attempt ${attempt + 1}`);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Download stalled'));
|
||||
// Don't reject here if we already rejected due to network loss - prevents duplicate rejection
|
||||
console.log('Writer finished after stall detection, ignoring...');
|
||||
}
|
||||
});
|
||||
|
||||
writer.on('error', (error) => {
|
||||
// Ignore write errors if stream was intentionally cancelled
|
||||
if (downloadStalled || controller.signal.aborted) {
|
||||
console.log(`Ignoring writer error after cancellation: ${error.code || error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!streamError) {
|
||||
streamError = new Error(`File write error: ${error.code || error.message}. Please retry.`);
|
||||
console.error(`Writer error on attempt ${attempt + 1}:`, error.code || error.message);
|
||||
}
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
reject(error);
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
}
|
||||
reject(streamError);
|
||||
});
|
||||
|
||||
response.data.on('error', (error) => {
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
// Handle case where stream ends without finishing writer
|
||||
response.data.on('end', () => {
|
||||
if (!streamCompleted && !downloadStalled && !streamError) {
|
||||
// Give a small delay for writer to finish - this is normal behavior
|
||||
setTimeout(() => {
|
||||
if (!streamCompleted) {
|
||||
console.log('Stream ended but writer not finished - waiting longer...');
|
||||
// Give more time for writer to finish - this might be slow disk I/O
|
||||
setTimeout(() => {
|
||||
if (!streamCompleted) {
|
||||
streamError = new Error('Download incomplete. Please retry.');
|
||||
reject(streamError);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,35 +348,97 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
||||
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
retryState.lastError = error;
|
||||
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
||||
console.error(`Error details:`, {
|
||||
isConnectionLost: error.isConnectionLost,
|
||||
canRetry: error.canRetry,
|
||||
message: error.message,
|
||||
downloadStalled: downloadStalled,
|
||||
streamCompleted: streamCompleted
|
||||
});
|
||||
|
||||
// Nettoyer le fichier partiel en cas d'erreur
|
||||
// Check if download actually completed successfully despite the error
|
||||
if (fs.existsSync(dest)) {
|
||||
const stats = fs.statSync(dest);
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
console.log(`File size after error: ${sizeInMB.toFixed(2)} MB`);
|
||||
|
||||
// If file is substantial size (> 1.5GB), treat as success and break
|
||||
if (sizeInMB >= 1500) {
|
||||
console.log('File appears to be complete despite error, treating as success');
|
||||
return; // Exit the retry loop successfully
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced file cleanup with validation
|
||||
if (fs.existsSync(dest)) {
|
||||
try {
|
||||
// Check if file is corrupted (small or invalid) or if error is non-resumable
|
||||
const partialStats = fs.statSync(dest);
|
||||
const isResumableError = error.message && (
|
||||
error.message.includes('stalled') ||
|
||||
error.message.includes('timeout') ||
|
||||
error.message.includes('network') ||
|
||||
error.message.includes('aborted')
|
||||
);
|
||||
|
||||
// Check if download appears to be complete (close to expected PWR size)
|
||||
const isPossiblyComplete = partialStats.size >= 1500 * 1024 * 1024; // >= 1.5GB
|
||||
|
||||
if (partialStats.size < 1024 * 1024 || (!isResumableError && !isPossiblyComplete)) {
|
||||
// Delete if file is too small OR error is non-resumable AND not possibly complete
|
||||
console.log(`[Cleanup] Removing PWR file (${!isResumableError && !isPossiblyComplete ? 'non-resumable error' : 'too small'}): ${(partialStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
fs.unlinkSync(dest);
|
||||
} else {
|
||||
// Keep the file for resume on resumable errors or if possibly complete
|
||||
console.log(`[Resume] Keeping PWR file (${isPossiblyComplete ? 'possibly complete' : 'for resume'}): ${(partialStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Could not cleanup partial file:', cleanupError.message);
|
||||
console.warn('Could not handle partial file:', cleanupError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si c'est une erreur réseau que l'on peut retry
|
||||
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
|
||||
// Expanded retryable error codes for better network detection
|
||||
const retryableErrors = [
|
||||
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
|
||||
'ESOCKETTIMEDOUT', 'EPROTO', 'ENETDOWN', 'EHOSTUNREACH',
|
||||
'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET',
|
||||
'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED'
|
||||
];
|
||||
|
||||
const isRetryable = retryableErrors.includes(error.code) ||
|
||||
error.message.includes('timeout') ||
|
||||
error.message.includes('stalled') ||
|
||||
error.message.includes('aborted') ||
|
||||
error.message.includes('network') ||
|
||||
error.message.includes('connection') ||
|
||||
error.message.includes('Please retry') ||
|
||||
error.message.includes('corrupted') ||
|
||||
error.message.includes('invalid') ||
|
||||
(error.response && error.response.status >= 500);
|
||||
|
||||
if (!isRetryable || attempt === maxRetries - 1) {
|
||||
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
||||
// Respect error's canRetry property if set
|
||||
const canRetry = (error.canRetry === false) ? false : isRetryable;
|
||||
|
||||
if (!canRetry || attempt === maxRetries - 1) {
|
||||
retryState.canRetry = false;
|
||||
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
|
||||
console.log(`Retryable error detected, will retry...`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
||||
// Enhanced error with retry state and user-friendly message
|
||||
const detailedError = lastError?.code || lastError?.message || 'Unknown error';
|
||||
const errorMessage = `Download failed after ${maxRetries} attempts. Last error: ${detailedError}. Please retry`;
|
||||
const enhancedError = new Error(errorMessage);
|
||||
enhancedError.retryState = retryState;
|
||||
enhancedError.lastError = lastError;
|
||||
enhancedError.detailedError = detailedError;
|
||||
throw enhancedError;
|
||||
}
|
||||
|
||||
function findHomePageUIPath(gameLatest) {
|
||||
@@ -205,8 +497,64 @@ function findLogoPath(gameLatest) {
|
||||
return searchDirectory(gameLatest);
|
||||
}
|
||||
|
||||
// Automatic stall retry function for network stalls
|
||||
async function retryStalledDownload(url, dest, progressCallback, previousError = null) {
|
||||
console.log('Automatic stall retry initiated for:', url);
|
||||
|
||||
// Wait before retry to allow network recovery
|
||||
console.log(`Waiting ${AUTOMATIC_STALL_RETRY_DELAY/1000} seconds before automatic retry...`);
|
||||
await new Promise(resolve => setTimeout(resolve, AUTOMATIC_STALL_RETRY_DELAY));
|
||||
|
||||
try {
|
||||
// Create new retryState for automatic retry
|
||||
const automaticRetryState = {
|
||||
attempts: 1,
|
||||
maxRetries: 1,
|
||||
canRetry: true,
|
||||
lastError: null,
|
||||
automaticStallRetries: (previousError && previousError.retryState) ? previousError.retryState.automaticStallRetries + 1 : 1,
|
||||
isAutomaticRetry: true
|
||||
};
|
||||
|
||||
// Update progress callback with automatic retry info
|
||||
if (progressCallback) {
|
||||
progressCallback(
|
||||
`Automatic stall retry ${automaticRetryState.automaticStallRetries}/${MAX_AUTOMATIC_STALL_RETRIES}...`,
|
||||
null, null, null, null, automaticRetryState
|
||||
);
|
||||
}
|
||||
|
||||
await downloadFile(url, dest, progressCallback, 1);
|
||||
console.log('Automatic stall retry successful');
|
||||
} catch (error) {
|
||||
console.error('Automatic stall retry failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual retry function for user-initiated retries
|
||||
async function retryDownload(url, dest, progressCallback, previousError = null) {
|
||||
console.log('Manual retry initiated for:', url);
|
||||
|
||||
// If we have a previous error with retry state, continue from there
|
||||
let additionalRetries = 3; // Allow 3 additional manual retries
|
||||
if (previousError && previousError.retryState) {
|
||||
additionalRetries = Math.max(2, 5 - previousError.retryState.attempts);
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadFile(url, dest, progressCallback, additionalRetries);
|
||||
console.log('Manual retry successful');
|
||||
} catch (error) {
|
||||
console.error('Manual retry failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadFile,
|
||||
retryDownload,
|
||||
retryStalledDownload,
|
||||
findHomePageUIPath,
|
||||
findLogoPath
|
||||
};
|
||||
|
||||
156
main.js
156
main.js
@@ -255,9 +255,9 @@ app.whenReady().then(async () => {
|
||||
mainWindow.webContents.send('lock-play-button', true);
|
||||
}
|
||||
|
||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
|
||||
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total, retryState });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -348,14 +348,15 @@ app.on('window-all-closed', () => {
|
||||
|
||||
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
||||
try {
|
||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
};
|
||||
mainWindow.webContents.send('progress-update', data);
|
||||
}
|
||||
@@ -398,14 +399,15 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
||||
mainWindow.webContents.send('installation-start');
|
||||
}
|
||||
|
||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
};
|
||||
mainWindow.webContents.send('progress-update', data);
|
||||
}
|
||||
@@ -418,17 +420,77 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
||||
mainWindow.webContents.send('installation-end');
|
||||
}
|
||||
|
||||
return result;
|
||||
// Ensure we always return a result for the IPC handler
|
||||
const successResponse = result || { success: true };
|
||||
console.log('[Main] Returning success response for install-game:', successResponse);
|
||||
return successResponse;
|
||||
} catch (error) {
|
||||
console.error('Install error:', error);
|
||||
const errorMessage = error.message || error.toString();
|
||||
|
||||
// Enhanced error data extraction for both download and Butler errors
|
||||
let errorData = {
|
||||
message: errorMessage,
|
||||
error: true,
|
||||
canRetry: true,
|
||||
retryData: null
|
||||
};
|
||||
|
||||
// Handle Butler-specific errors
|
||||
if (error.butlerError) {
|
||||
console.log('[Main] Processing Butler error with retry context');
|
||||
errorData.retryData = {
|
||||
branch: error.branch || 'release',
|
||||
fileName: error.fileName || '4.pwr',
|
||||
cacheDir: error.cacheDir
|
||||
};
|
||||
errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true;
|
||||
|
||||
// Add Butler-specific error details
|
||||
if (error.stderr) {
|
||||
console.error('[Main] Butler stderr:', error.stderr);
|
||||
}
|
||||
if (error.stdout) {
|
||||
console.log('[Main] Butler stdout:', error.stdout);
|
||||
}
|
||||
if (error.errorCode) {
|
||||
console.log('[Main] Butler error code:', error.errorCode);
|
||||
}
|
||||
}
|
||||
// Handle PWR download errors
|
||||
else if (error.branch && error.fileName) {
|
||||
console.log('[Main] Processing PWR download error with retry context');
|
||||
errorData.retryData = {
|
||||
branch: error.branch,
|
||||
fileName: error.fileName,
|
||||
cacheDir: error.cacheDir
|
||||
};
|
||||
errorData.canRetry = error.canRetry !== undefined ? error.canRetry : true;
|
||||
}
|
||||
// Default fallback for other errors
|
||||
else {
|
||||
console.log('[Main] Processing generic error, creating default retry data');
|
||||
errorData.retryData = {
|
||||
branch: 'release',
|
||||
fileName: '4.pwr'
|
||||
};
|
||||
}
|
||||
|
||||
// Send enhanced error info for retry UI
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
console.log('[Main] Sending error data to renderer:', errorData);
|
||||
mainWindow.webContents.send('progress-update', errorData);
|
||||
}
|
||||
|
||||
// Signal installation end on error too
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('installation-end');
|
||||
}
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
// Always return a proper response to prevent timeout
|
||||
const errorResponse = { success: false, error: errorMessage };
|
||||
console.log('[Main] Returning error response for install-game:', errorResponse);
|
||||
return errorResponse;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -519,14 +581,15 @@ ipcMain.handle('select-install-path', async () => {
|
||||
|
||||
ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
|
||||
try {
|
||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
};
|
||||
mainWindow.webContents.send('first-launch-progress', data);
|
||||
}
|
||||
@@ -575,14 +638,15 @@ ipcMain.handle('uninstall-game', async () => {
|
||||
|
||||
ipcMain.handle('repair-game', async () => {
|
||||
try {
|
||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
};
|
||||
mainWindow.webContents.send('progress-update', data);
|
||||
}
|
||||
@@ -592,7 +656,72 @@ ipcMain.handle('repair-game', async () => {
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Repair error:', error);
|
||||
return { success: false, error: error.message };
|
||||
const errorMessage = error.message || error.toString();
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('retry-download', async (event, retryData) => {
|
||||
try {
|
||||
console.log('[IPC] retry-download called with data:', retryData);
|
||||
|
||||
// Handle null retry data gracefully
|
||||
if (!retryData || !retryData.branch || !retryData.fileName) {
|
||||
console.log('[IPC] Invalid retry data, using defaults');
|
||||
retryData = {
|
||||
branch: 'release',
|
||||
fileName: '4.pwr'
|
||||
};
|
||||
}
|
||||
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
};
|
||||
mainWindow.webContents.send('progress-update', data);
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PWR download info from retryData
|
||||
const branch = retryData.branch;
|
||||
const fileName = retryData.fileName;
|
||||
const cacheDir = retryData.cacheDir;
|
||||
|
||||
console.log(`[IPC] Retrying PWR download: branch=${branch}, fileName=${fileName}`);
|
||||
|
||||
// Perform the retry with enhanced context
|
||||
await retryPWRDownload(branch, fileName, progressCallback, cacheDir);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Retry download error:', error);
|
||||
const errorMessage = error.message || error.toString();
|
||||
|
||||
// Send error update to frontend with context
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const data = {
|
||||
message: errorMessage,
|
||||
error: true,
|
||||
canRetry: true,
|
||||
retryData: {
|
||||
branch: retryData?.branch || 'release',
|
||||
fileName: retryData?.fileName || '4.pwr',
|
||||
cacheDir: retryData?.cacheDir
|
||||
}
|
||||
};
|
||||
mainWindow.webContents.send('progress-update', data);
|
||||
}
|
||||
|
||||
// Always return a proper response to prevent timeout
|
||||
const errorResponse = { success: false, error: errorMessage };
|
||||
console.log('[Main] Returning error response for retry-download:', errorResponse);
|
||||
return errorResponse;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -711,6 +840,7 @@ ipcMain.handle('load-settings', async () => {
|
||||
});
|
||||
|
||||
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
|
||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||
const os = require('os');
|
||||
|
||||
ipcMain.handle('get-local-app-data', async () => {
|
||||
|
||||
10
preload.js
10
preload.js
@@ -28,6 +28,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
||||
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
|
||||
repairGame: () => ipcRenderer.invoke('repair-game'),
|
||||
retryDownload: (retryData) => ipcRenderer.invoke('retry-download', retryData),
|
||||
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
|
||||
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
|
||||
@@ -44,7 +45,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
||||
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
||||
onProgressUpdate: (callback) => {
|
||||
ipcRenderer.on('progress-update', (event, data) => callback(data));
|
||||
ipcRenderer.on('progress-update', (event, data) => {
|
||||
// Ensure data includes retry state if available
|
||||
if (data && typeof data === 'object') {
|
||||
callback(data);
|
||||
} else {
|
||||
callback(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
onProgressComplete: (callback) => {
|
||||
ipcRenderer.on('progress-complete', () => callback());
|
||||
|
||||
Reference in New Issue
Block a user