mirror of
https://github.com/araxiaonline/wow-client-patcher.git
synced 2026-06-13 03:12:24 -04:00
Added mutex to fix manifest corruption, added update notifier
This commit is contained in:
@@ -10,6 +10,7 @@ const Channels = {
|
||||
DOWNLOAD_BATCH_START: 'download-batch-start',
|
||||
DOWNLOAD_BATCH_END: 'download-batch-end',
|
||||
DOWNLOAD_BATCH_DATA: 'download-batch-data',
|
||||
GET_UPDATE: 'get-update',
|
||||
} as const;
|
||||
|
||||
const PLATFORM = {
|
||||
|
||||
@@ -5,6 +5,8 @@ import path, { join } from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import AdmZip from 'adm-zip';
|
||||
import log from 'electron-log';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
import Downloader from './Downloader';
|
||||
import { santizeETag } from '../util';
|
||||
|
||||
@@ -69,6 +71,7 @@ export default class FileManager {
|
||||
private manifestFile: string;
|
||||
private downloader: Downloader;
|
||||
private remoteVersions: Versions = [];
|
||||
private manifestMutex: Mutex = new Mutex();
|
||||
|
||||
constructor(basePath: string) {
|
||||
// This sets up all local paths for the file manager
|
||||
@@ -502,21 +505,27 @@ export default class FileManager {
|
||||
*/
|
||||
async GetManifest(): Promise<Manifest> {
|
||||
let manifest: Manifest;
|
||||
if (!fs.existsSync(this.manifestFile)) {
|
||||
manifest = {
|
||||
Version: 'v0',
|
||||
LastUpdate: new Date().toISOString(),
|
||||
Files: {},
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
} else {
|
||||
manifest = JSON.parse(
|
||||
await fs.promises.readFile(this.manifestFile, 'utf8')
|
||||
) as Manifest;
|
||||
const release = await this.manifestMutex.acquire();
|
||||
try {
|
||||
if (!fs.existsSync(this.manifestFile)) {
|
||||
manifest = {
|
||||
Version: 'v0',
|
||||
LastUpdate: new Date().toISOString(),
|
||||
Files: {},
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
} else {
|
||||
manifest = JSON.parse(
|
||||
await fs.promises.readFile(this.manifestFile, 'utf8')
|
||||
) as Manifest;
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
return manifest;
|
||||
@@ -539,12 +548,19 @@ export default class FileManager {
|
||||
*/
|
||||
async UpdateManifest(filename: string, tag: string): Promise<void> {
|
||||
const manifest = await this.GetManifest();
|
||||
manifest.Files[filename] = santizeETag(tag);
|
||||
manifest.LastUpdate = new Date().toISOString();
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
|
||||
const release = await this.manifestMutex.acquire();
|
||||
try {
|
||||
manifest.Files[filename] = santizeETag(tag);
|
||||
manifest.LastUpdate = new Date().toISOString();
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -553,12 +569,18 @@ export default class FileManager {
|
||||
async UpdateLocalVersion() {
|
||||
const manifest = await this.GetManifest();
|
||||
const remoteVersion = await this.GetRemoteVersion();
|
||||
manifest.Version = remoteVersion[0].version;
|
||||
manifest.LastUpdate = new Date().toISOString();
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
|
||||
const release = await this.manifestMutex.acquire();
|
||||
try {
|
||||
manifest.Version = remoteVersion[0].version;
|
||||
manifest.LastUpdate = new Date().toISOString();
|
||||
await fs.promises.writeFile(
|
||||
this.manifestFile,
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,26 +593,31 @@ export default class FileManager {
|
||||
// we need a new instances to make sure we pass the correct event handler.
|
||||
const downloader = this.DownloaderInstance();
|
||||
const aioETag = await this.downloader.getETag(AIORemotePath);
|
||||
const patchSETag = await this.downloader.getETag('patches/patch-S.MPQ');
|
||||
|
||||
const downloader2 = this.DownloaderInstance();
|
||||
|
||||
downloader.on('end', () => {
|
||||
// update the manifest file with the new install need some more error handling here
|
||||
this.UpdateManifest(AIORemotePath, aioETag);
|
||||
this.UpdateManifest('patches/patch-S.MPQ', patchSETag)
|
||||
|
||||
downloader2.downloadFile('patches/patch-S.MPQ', 'Data/patch-S.MPQ');
|
||||
setTimeout(() => {
|
||||
const zip = new AdmZip(path.join(this.basePath, AIOLocalPath));
|
||||
zip.extractAllTo(path.join(this.basePath, 'Interface/AddOns'), true);
|
||||
}, 250);
|
||||
});
|
||||
|
||||
downloader2.on('end', async () => {
|
||||
const patchSETag = await this.downloader.getETag('patches/patch-S.MPQ');
|
||||
this.UpdateManifest('patches/patch-S.MPQ', patchSETag)
|
||||
});
|
||||
|
||||
|
||||
if (!(await downloader.downloadFile(AIORemotePath, AIOLocalPath))) {
|
||||
log.error('Failed to download AIO');
|
||||
}
|
||||
|
||||
await downloader.downloadFile('patches/patch-S.MPQ', 'Data/patch-S.MPQ');
|
||||
|
||||
return downloader;
|
||||
return downloader2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,17 +23,11 @@ import { Channels } from '../constants';
|
||||
import MenuBuilder from './menu';
|
||||
import { resolveHtmlPath } from './util';
|
||||
import FileManager from './libs/FileManager';
|
||||
import AutoUpdate from './libs/AutoUpdate';
|
||||
import packjson = require('../../package.json');
|
||||
|
||||
const showdown = require('showdown');
|
||||
|
||||
// class AppUpdater {
|
||||
// constructor() {
|
||||
// log.transports.file.level = 'info';
|
||||
// autoUpdater.logger = log;
|
||||
// autoUpdater.checkForUpdatesAndNotify();
|
||||
// }
|
||||
// }
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
@@ -94,23 +88,8 @@ const createWindow = async () => {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
// Remove this if your app does not use auto updates
|
||||
// eslint-disable-next-line
|
||||
// new AppUpdater();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners...
|
||||
*/
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
@@ -133,6 +112,7 @@ app
|
||||
// loads the application path where the exe is run, this is needed to lookup information about the installed patches.
|
||||
let appPath: string;
|
||||
let fileManager: FileManager;
|
||||
const autoUpdater = new AutoUpdate();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
if(process.env.PORTABLE_EXECUTABLE_DIR) {
|
||||
@@ -147,6 +127,8 @@ if (process.platform === 'win32') {
|
||||
appPath = '/System/Applications/Chess.app';
|
||||
fileManager = new FileManager(path.join(__dirname, '../../dist/')); // will handle macs even though we will never be building for one.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This will return all the information of the file system installed in the directory.
|
||||
*/
|
||||
@@ -159,8 +141,11 @@ if (process.platform === 'win32') {
|
||||
const newHtml = converter.makeHtml(news);
|
||||
const remoteInfo = await fileManager.GetRemoteVersion();
|
||||
const localInfo = await fileManager.GetVersion();
|
||||
const latestAppVersion = await autoUpdater.getLatestVersion();
|
||||
|
||||
const appInfo = {
|
||||
AppVersion: `v${packjson.version}`,
|
||||
LatestAppVersion: latestAppVersion,
|
||||
Version: localInfo.Version,
|
||||
LastUpdate: localInfo.LastUpdate,
|
||||
ExecPath: process.env.PORTABLE_EXECUTABLE_DIR,
|
||||
@@ -172,6 +157,8 @@ if (process.platform === 'win32') {
|
||||
LatestNews: newHtml,
|
||||
RemoteVersion: remoteInfo[0]?.version,
|
||||
};
|
||||
|
||||
console.log(appInfo);
|
||||
return appInfo;
|
||||
});
|
||||
|
||||
@@ -183,7 +170,10 @@ if (process.platform === 'win32') {
|
||||
|
||||
})();
|
||||
|
||||
|
||||
ipcMain.on(Channels.GET_UPDATE, async (event, arg) => {
|
||||
const latestAppVersion = await autoUpdater.getLatestVersion();
|
||||
shell.openExternal(`https://github.com/araxiaonline/wow-client-patcher/releases/tag/${latestAppVersion}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* We have to redirect events from the main process to the render process as they are fired this allows the
|
||||
|
||||
@@ -138,8 +138,11 @@ const IPCApi: LauncherServer.Api = {
|
||||
|
||||
InstallUpdates: async (callbacks: DownloadCallbacks) => {
|
||||
handleInstall('InstallUpdates', callbacks);
|
||||
}
|
||||
},
|
||||
|
||||
GetUpdate: async () => {
|
||||
ipcRenderer.send(Channels.GET_UPDATE);
|
||||
}
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('api', IPCApi);
|
||||
|
||||
@@ -27,6 +27,7 @@ import CardActionArea from '@mui/material/CardActionArea';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
// App Components
|
||||
import VersionText from './VersionText';
|
||||
@@ -88,18 +89,16 @@ function WoWClientPatcher() {
|
||||
}
|
||||
|
||||
const installStore = async () => {
|
||||
if(appInfo?.AIOInstalled) {
|
||||
if(appInfo?.AIOInstalled || isDownloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.api.InstallStore({
|
||||
data: (data) => {
|
||||
setIsDownloading(true);
|
||||
setDownloadProgress(Math.floor(data.percentage));
|
||||
},
|
||||
end: async ({totalBytes, file}) => {
|
||||
setIsDownloading(false);
|
||||
setDownloadProgress(0);
|
||||
|
||||
setTimeout(() => {
|
||||
updateAppInfo();
|
||||
@@ -111,18 +110,20 @@ function WoWClientPatcher() {
|
||||
const batchCallbacks: DownloadCallbacks = {
|
||||
batchStart: (data) => {
|
||||
setIsDownloading(true);
|
||||
// console.log('start:', data);
|
||||
},
|
||||
batchData: throttle((data: any) => {
|
||||
setIsDownloading(true);
|
||||
// console.log('progress:', data);
|
||||
setDownloadProgress(Math.floor(data.percentage));
|
||||
}, 300),
|
||||
}, 500),
|
||||
batchEnd: (data) => {
|
||||
setTimeout(() => {
|
||||
setIsDownloading(false);
|
||||
|
||||
setDownloadProgress(0);
|
||||
updateAppInfo();
|
||||
}, 300);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,6 +154,9 @@ function WoWClientPatcher() {
|
||||
window.api.InstallUpdates(batchCallbacks);
|
||||
}
|
||||
|
||||
const getUpdate = () => {
|
||||
window.api.GetUpdate();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -485,6 +489,18 @@ function WoWClientPatcher() {
|
||||
versionStamp={versionStamp}
|
||||
updater={installUpdates}
|
||||
/>
|
||||
<br/>
|
||||
<Box sx={{ justifyContent: 'center', textAlign: 'center', justifyItems: 'center'}}>
|
||||
{ appInfo?.AppVersion !== appInfo?.LatestAppVersion &&
|
||||
<Alert onClick={getUpdate} sx={{
|
||||
width: '40%',
|
||||
left: '30%',
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.70)',
|
||||
color: 'rgba(11,207,247, 1.0)',
|
||||
cursor: 'pointer',
|
||||
}} severity="info">A newer version of this launcher can be download by clicking <b>here</b></Alert> }
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
16
src/typings/index.d.ts
vendored
16
src/typings/index.d.ts
vendored
@@ -22,6 +22,16 @@ declare namespace WowLauncher {
|
||||
* Information about the application.
|
||||
*/
|
||||
interface AppInfo {
|
||||
/**
|
||||
* The current version of the application. It is pulled from the package.json file.
|
||||
*/
|
||||
AppVersion: string;
|
||||
|
||||
/**
|
||||
* Latest App Version from Github releases
|
||||
*/
|
||||
LatestAppVersion: string;
|
||||
|
||||
/**
|
||||
* The current version of the application.
|
||||
*/
|
||||
@@ -158,6 +168,12 @@ declare namespace LauncherServer {
|
||||
*/
|
||||
InstallUpdates: (callbacks: any) => void;
|
||||
|
||||
/**
|
||||
* Returns the latest version of the client from the remote server.
|
||||
* @returns Link to the latest binary of the file
|
||||
*/
|
||||
GetUpdate: () => void;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user