mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 12:51:47 -03:00
380 lines
15 KiB
JavaScript
380 lines
15 KiB
JavaScript
const { autoUpdater } = require('electron-updater');
|
|
const { app } = require('electron');
|
|
const logger = require('./logger');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
class AppUpdater {
|
|
constructor(mainWindow) {
|
|
this.mainWindow = mainWindow;
|
|
this.autoUpdateAvailable = true; // Track if auto-update is possible
|
|
this.updateAvailable = false; // Track if an update was detected
|
|
this.updateVersion = null; // Store the available update version
|
|
this.setupAutoUpdater();
|
|
}
|
|
|
|
setupAutoUpdater() {
|
|
// Enable dev mode for testing (reads dev-app-update.yml)
|
|
// Only enable in development, not in production builds
|
|
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
|
autoUpdater.forceDevUpdateConfig = true;
|
|
console.log('Dev update mode enabled - using dev-app-update.yml');
|
|
}
|
|
|
|
// Configure logger for electron-updater
|
|
// Create a compatible logger interface
|
|
autoUpdater.logger = {
|
|
info: (...args) => logger.info(...args),
|
|
warn: (...args) => logger.warn(...args),
|
|
error: (...args) => logger.error(...args),
|
|
debug: (...args) => logger.log(...args)
|
|
};
|
|
|
|
// Auto download updates
|
|
autoUpdater.autoDownload = true;
|
|
// Auto install on quit (after download)
|
|
autoUpdater.autoInstallOnAppQuit = true;
|
|
|
|
// Event handlers
|
|
autoUpdater.on('checking-for-update', () => {
|
|
console.log('Checking for updates...');
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-checking');
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-available', (info) => {
|
|
console.log('Update available:', info.version);
|
|
const currentVersion = app.getVersion();
|
|
const newVersion = info.version;
|
|
|
|
// Only proceed if the new version is actually different from current
|
|
if (newVersion === currentVersion) {
|
|
console.log('Update version matches current version, ignoring update-available event');
|
|
return;
|
|
}
|
|
|
|
this.updateAvailable = true;
|
|
this.updateVersion = newVersion;
|
|
this.autoUpdateAvailable = true; // Reset flag when new update is available
|
|
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-available', {
|
|
version: newVersion,
|
|
newVersion: newVersion,
|
|
currentVersion: currentVersion,
|
|
releaseName: info.releaseName,
|
|
releaseNotes: info.releaseNotes
|
|
});
|
|
// Also send to the old popup handler for compatibility
|
|
this.mainWindow.webContents.send('show-update-popup', {
|
|
currentVersion: currentVersion,
|
|
newVersion: newVersion,
|
|
version: newVersion
|
|
});
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-not-available', (info) => {
|
|
console.log('Update not available. Current version is latest.');
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-not-available', {
|
|
version: info.version
|
|
});
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('error', (err) => {
|
|
console.error('Error in auto-updater:', err);
|
|
|
|
// Check if this is a network error (not critical, don't show UI)
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
|
errorMessage.includes('network') ||
|
|
errorMessage.includes('connection') ||
|
|
errorMessage.includes('timeout') ||
|
|
errorMessage.includes('enotfound');
|
|
|
|
if (isNetworkError) {
|
|
console.warn('Network error in auto-updater - will retry later. Not showing error UI.');
|
|
return; // Don't show error UI for network issues
|
|
}
|
|
|
|
// Handle SHA512 checksum mismatch - this can happen during updates, just retry
|
|
const isChecksumError = err.code === 'ERR_CHECKSUM_MISMATCH' ||
|
|
errorMessage.includes('sha512') ||
|
|
errorMessage.includes('checksum') ||
|
|
errorMessage.includes('mismatch');
|
|
|
|
if (isChecksumError) {
|
|
console.warn('SHA512 checksum mismatch detected - clearing cache and will retry automatically. This is normal during updates.');
|
|
// Clear the update cache and let it re-download
|
|
this.clearUpdateCache();
|
|
|
|
// Don't show error UI - just log and let it retry automatically on next check
|
|
return;
|
|
}
|
|
|
|
// Determine if this is a critical error that prevents auto-update
|
|
const isCriticalError = this.isCriticalUpdateError(err);
|
|
|
|
if (isCriticalError) {
|
|
this.autoUpdateAvailable = false;
|
|
console.warn('Auto-update failed. Manual download required.');
|
|
}
|
|
|
|
// Handle missing metadata files (platform-specific builds)
|
|
if (err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND') {
|
|
const platform = process.platform === 'darwin' ? 'macOS' :
|
|
process.platform === 'win32' ? 'Windows' : 'Linux';
|
|
const missingFile = process.platform === 'darwin' ? 'latest-mac.yml' :
|
|
process.platform === 'win32' ? 'latest.yml' : 'latest-linux.yml';
|
|
console.warn(`${platform} update metadata file (${missingFile}) not found in release.`);
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-error', {
|
|
message: `Update metadata file for ${platform} not found in release. Please download manually.`,
|
|
code: err.code,
|
|
requiresManualDownload: true,
|
|
updateVersion: this.updateVersion,
|
|
isMissingMetadata: true
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Linux-specific: Handle installation permission errors
|
|
if (process.platform === 'linux') {
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
const errorStack = err.stack?.toLowerCase() || '';
|
|
const isInstallError = errorMessage.includes('pkexec') ||
|
|
errorMessage.includes('gksudo') ||
|
|
errorMessage.includes('kdesudo') ||
|
|
errorMessage.includes('setuid root') ||
|
|
errorMessage.includes('exited with code 127') ||
|
|
errorStack.includes('pacmanupdater') ||
|
|
errorStack.includes('doinstall') ||
|
|
errorMessage.includes('installation failed');
|
|
|
|
if (isInstallError) {
|
|
console.warn('Linux installation error: Package installation requires root privileges. Manual installation required.');
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-error', {
|
|
message: 'Auto-installation requires root privileges. Please download and install the update manually.',
|
|
code: err.code || 'ERR_LINUX_INSTALL_PERMISSION',
|
|
isLinuxInstallError: true,
|
|
requiresManualDownload: true,
|
|
updateVersion: this.updateVersion
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// macOS-specific: Handle unsigned app errors gracefully
|
|
if (process.platform === 'darwin' && err.code === 2) {
|
|
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-error', {
|
|
message: 'Please download manually from GitHub.',
|
|
code: err.code,
|
|
isMacSigningError: true,
|
|
requiresManualDownload: true,
|
|
updateVersion: this.updateVersion
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-error', {
|
|
message: err.message,
|
|
code: err.code,
|
|
requiresManualDownload: isCriticalError,
|
|
updateVersion: this.updateVersion
|
|
});
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('download-progress', (progressObj) => {
|
|
const message = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`;
|
|
console.log(message);
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-download-progress', {
|
|
percent: progressObj.percent,
|
|
bytesPerSecond: progressObj.bytesPerSecond,
|
|
transferred: progressObj.transferred,
|
|
total: progressObj.total
|
|
});
|
|
}
|
|
});
|
|
|
|
autoUpdater.on('update-downloaded', (info) => {
|
|
console.log('Update downloaded:', info.version);
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
this.mainWindow.webContents.send('update-downloaded', {
|
|
version: info.version,
|
|
releaseName: info.releaseName,
|
|
releaseNotes: info.releaseNotes
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
checkForUpdatesAndNotify() {
|
|
// Check for updates and notify if available
|
|
autoUpdater.checkForUpdatesAndNotify().catch(err => {
|
|
console.error('Failed to check for updates:', err);
|
|
|
|
// Network errors are not critical - just log and continue
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
|
errorMessage.includes('network') ||
|
|
errorMessage.includes('connection') ||
|
|
errorMessage.includes('timeout') ||
|
|
errorMessage.includes('enotfound');
|
|
|
|
if (isNetworkError) {
|
|
console.warn('Network error checking for updates - will retry later. This is not critical.');
|
|
return; // Don't show error UI for network issues
|
|
}
|
|
|
|
const isCritical = this.isCriticalUpdateError(err);
|
|
if (this.mainWindow && !this.mainWindow.isDestroyed() && isCritical) {
|
|
this.mainWindow.webContents.send('update-error', {
|
|
message: err.message || 'Failed to check for updates',
|
|
code: err.code,
|
|
requiresManualDownload: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
checkForUpdates() {
|
|
// Manual check for updates (returns promise)
|
|
return autoUpdater.checkForUpdates().catch(err => {
|
|
console.error('Failed to check for updates:', err);
|
|
|
|
// Network errors are not critical - just return no update available
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
|
|
errorMessage.includes('network') ||
|
|
errorMessage.includes('connection') ||
|
|
errorMessage.includes('timeout') ||
|
|
errorMessage.includes('enotfound');
|
|
|
|
if (isNetworkError) {
|
|
console.warn('Network error - update check unavailable');
|
|
return { updateInfo: null }; // Return empty result for network errors
|
|
}
|
|
|
|
const isCritical = this.isCriticalUpdateError(err);
|
|
if (isCritical) {
|
|
this.autoUpdateAvailable = false;
|
|
}
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
quitAndInstall() {
|
|
// Quit and install the update
|
|
autoUpdater.quitAndInstall(false, true);
|
|
}
|
|
|
|
getUpdateInfo() {
|
|
return {
|
|
currentVersion: app.getVersion(),
|
|
updateAvailable: false
|
|
};
|
|
}
|
|
|
|
clearUpdateCache() {
|
|
try {
|
|
// Get the cache directory based on platform
|
|
const cacheDir = process.platform === 'darwin'
|
|
? path.join(os.homedir(), 'Library', 'Caches', `${app.getName()}-updater`)
|
|
: process.platform === 'win32'
|
|
? path.join(os.homedir(), 'AppData', 'Local', `${app.getName()}-updater`)
|
|
: path.join(os.homedir(), '.cache', `${app.getName()}-updater`);
|
|
|
|
if (fs.existsSync(cacheDir)) {
|
|
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
console.log('Update cache cleared successfully');
|
|
} else {
|
|
console.log('Update cache directory does not exist');
|
|
}
|
|
} catch (cacheError) {
|
|
console.warn('Could not clear update cache:', cacheError.message);
|
|
}
|
|
}
|
|
|
|
isCriticalUpdateError(err) {
|
|
// Check for errors that prevent auto-update
|
|
const errorMessage = err.message?.toLowerCase() || '';
|
|
const errorCode = err.code;
|
|
|
|
// Missing update metadata files (platform-specific)
|
|
if (errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' ||
|
|
errorMessage.includes('cannot find latest') ||
|
|
errorMessage.includes('latest-linux.yml') ||
|
|
errorMessage.includes('latest-mac.yml') ||
|
|
errorMessage.includes('latest.yml')) {
|
|
return true;
|
|
}
|
|
|
|
// macOS code signing errors
|
|
if (process.platform === 'darwin' && (errorCode === 2 || errorMessage.includes('shipit'))) {
|
|
return true;
|
|
}
|
|
|
|
// Download failures
|
|
if (errorMessage.includes('download') && errorMessage.includes('fail')) {
|
|
return true;
|
|
}
|
|
|
|
// Network errors that prevent download (but we handle these separately as non-critical)
|
|
// Installation errors
|
|
if (errorMessage.includes('install') && errorMessage.includes('fail')) {
|
|
return true;
|
|
}
|
|
|
|
// Permission errors
|
|
if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
|
|
return true;
|
|
}
|
|
|
|
// Linux installation errors (pkexec, sudo issues)
|
|
if (process.platform === 'linux' && (
|
|
errorMessage.includes('pkexec') ||
|
|
errorMessage.includes('setuid root') ||
|
|
errorMessage.includes('exited with code 127') ||
|
|
errorMessage.includes('gksudo') ||
|
|
errorMessage.includes('kdesudo'))) {
|
|
return true;
|
|
}
|
|
|
|
// File system errors (but not "not found" for metadata files - handled above)
|
|
if (errorMessage.includes('enoent') || errorMessage.includes('cannot find')) {
|
|
// Only if it's not about metadata files
|
|
if (!errorMessage.includes('latest') && !errorMessage.includes('.yml')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Generic critical error codes (but not checksum errors - those are handled separately)
|
|
if (errorCode && (errorCode >= 100 ||
|
|
errorCode === 'ERR_UPDATER_INVALID_RELEASE_FEED' ||
|
|
errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')) {
|
|
// Don't treat checksum errors as critical - they're handled separately
|
|
if (errorCode === 'ERR_CHECKSUM_MISMATCH') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
module.exports = AppUpdater;
|