Added mutex to fix manifest corruption, added update notifier

This commit is contained in:
Ben
2023-10-05 00:17:01 -05:00
parent d206ab896e
commit 3ee11a9e98
6 changed files with 114 additions and 61 deletions

View File

@@ -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 = {

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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;
}
/**