mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Many updater fixes
This commit is contained in:
parent
2242646e08
commit
3a3c14aab7
5 changed files with 290 additions and 184 deletions
7
receivers/electron/package-lock.json
generated
7
receivers/electron/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vscode/sudo-prompt": "^9.3.1",
|
||||
"bufferutil": "^4.0.8",
|
||||
"dashjs": "^4.7.4",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
|
@ -3064,6 +3065,12 @@
|
|||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/sudo-prompt": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz",
|
||||
"integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vscode/sudo-prompt": "^9.3.1",
|
||||
"bufferutil": "^4.0.8",
|
||||
"dashjs": "^4.7.4",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
|
|
|
@ -12,8 +12,9 @@ BASE_DOWNLOAD_URL = BUCKET_NAME.replace('-', '.')
|
|||
EXCLUDED_DELTA_VERSIONS = ["1.0.14"]
|
||||
|
||||
# Version tracking for migration support
|
||||
RELEASES_JSON_VERSION = '1'
|
||||
RELEASES_JSON_COMPAT_VERSION = '1'
|
||||
RELEASES_JSON_FILE_VERSION = 1
|
||||
RELEASES_JSON_MAJOR_VERSION = '1'
|
||||
RELEASES_JSON = f'releases_v{RELEASES_JSON_MAJOR_VERSION}.json'
|
||||
|
||||
# Customizable CI parameters
|
||||
CACHE_VERSION_AMOUNT = int(os.environ.get('CACHE_VERSION_AMOUNT', default="-1"))
|
||||
|
@ -31,7 +32,7 @@ def ensure_files_exist(dirs, files):
|
|||
|
||||
def copy_artifacts_to_local_cache():
|
||||
version = None
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', 'releases.json') , 'r') as file:
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', RELEASES_JSON) , 'r') as file:
|
||||
releases = json.load(file)
|
||||
version = ArtifactVersion(releases['currentVersion'], 'stable', None)
|
||||
|
||||
|
@ -60,9 +61,9 @@ def sync_local_cache():
|
|||
rel_path = os.path.relpath(os.path.join(root, filename), LOCAL_CACHE_DIR)
|
||||
version = os.path.relpath(rel_path, 'electron/').split('/')[0]
|
||||
|
||||
if version in s3.get_versions() or filename == 'releases.json':
|
||||
if version in s3.get_versions() or filename == RELEASES_JSON:
|
||||
local_files.append(rel_path)
|
||||
elif filename != 'releases.json':
|
||||
elif filename != RELEASES_JSON:
|
||||
print(f'Purging file from local cache: {rel_path}')
|
||||
os.remove(os.path.join(root, filename))
|
||||
|
||||
|
@ -85,7 +86,7 @@ def upload_local_cache():
|
|||
local_files.append(rel_path)
|
||||
|
||||
for file_path in local_files:
|
||||
if file_path not in map(lambda x: x['Key'], s3.get_bucket_files()) or os.path.basename(file_path) == 'releases.json':
|
||||
if file_path not in map(lambda x: x['Key'], s3.get_bucket_files()) or os.path.basename(file_path) == RELEASES_JSON:
|
||||
s3.upload_file(os.path.join(LOCAL_CACHE_DIR, file_path), file_path)
|
||||
|
||||
# TODO: WIP
|
||||
|
@ -93,7 +94,7 @@ def generate_delta_updates(artifact_version):
|
|||
delta_info = {}
|
||||
|
||||
releases = None
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', 'releases.json') , 'r') as file:
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', RELEASES_JSON) , 'r') as file:
|
||||
releases = json.load(file)
|
||||
|
||||
# Get sha digest from base version for integrity validation
|
||||
|
@ -156,9 +157,9 @@ def generate_delta_updates(artifact_version):
|
|||
return delta_info
|
||||
|
||||
def generate_releases_json(artifact_version, delta_info):
|
||||
print('Generating releases.json...')
|
||||
print(f'Generating {RELEASES_JSON}...')
|
||||
releases = None
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', 'releases.json') , 'r') as file:
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', RELEASES_JSON) , 'r') as file:
|
||||
releases = json.load(file)
|
||||
|
||||
current_version = releases.get('currentVersion', '0.0.0')
|
||||
|
@ -212,13 +213,12 @@ def generate_releases_json(artifact_version, delta_info):
|
|||
releases['currentVersion'] = current_version
|
||||
|
||||
releases['previousVersions'] = s3.get_versions(full=True)
|
||||
releases['fileVersion'] = RELEASES_JSON_VERSION
|
||||
releases['fileCompatVersion'] = RELEASES_JSON_COMPAT_VERSION
|
||||
releases['fileVersion'] = RELEASES_JSON_FILE_VERSION
|
||||
releases['allVersions'] = all_versions
|
||||
releases['channelCurrentVersions'] = channel_current_versions
|
||||
releases['currentReleases'] = current_releases
|
||||
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', 'releases.json') , 'w') as file:
|
||||
with open(os.path.join(LOCAL_CACHE_DIR, 'electron', RELEASES_JSON) , 'w') as file:
|
||||
json.dump(releases, file, indent=4)
|
||||
|
||||
def generate_previous_releases_page():
|
||||
|
@ -236,7 +236,7 @@ ensure_files_exist(dirs=[
|
|||
os.path.join(LOCAL_CACHE_DIR, 'electron')
|
||||
],
|
||||
files=[
|
||||
os.path.join('electron', 'releases.json')
|
||||
os.path.join('electron', RELEASES_JSON)
|
||||
])
|
||||
artifact_version = copy_artifacts_to_local_cache()
|
||||
sync_local_cache()
|
||||
|
|
|
@ -53,39 +53,41 @@ export default class Main {
|
|||
{
|
||||
label: 'Check for updates',
|
||||
click: async () => {
|
||||
try {
|
||||
if (await Updater.update()) {
|
||||
const restartPrompt = await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Update ready',
|
||||
message: 'Update downloaded, restart now to apply the changes.',
|
||||
buttons: ['Restart'],
|
||||
defaultId: 0
|
||||
});
|
||||
if (!Updater.isDownloading) {
|
||||
try {
|
||||
if (await Updater.update()) {
|
||||
const restartPrompt = await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Update ready',
|
||||
message: 'Update downloaded, restart now to apply the changes.',
|
||||
buttons: ['Restart'],
|
||||
defaultId: 0
|
||||
});
|
||||
|
||||
// Restart the app if the user clicks the 'Restart' button
|
||||
if (restartPrompt.response === 0) {
|
||||
await Updater.processUpdate();
|
||||
// Restart the app if the user clicks the 'Restart' button
|
||||
if (restartPrompt.response === 0) {
|
||||
Updater.restart();
|
||||
}
|
||||
} else {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Already up-to-date',
|
||||
message: 'The application is already on the latest version.',
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
}
|
||||
} else {
|
||||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: 'Already up-to-date',
|
||||
message: 'The application is already on the latest version.',
|
||||
type: 'error',
|
||||
title: 'Failed to update',
|
||||
message: err,
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: 'Failed to update',
|
||||
message: err,
|
||||
buttons: ['OK'],
|
||||
defaultId: 0
|
||||
});
|
||||
|
||||
Main.logger.error('Failed to update:', err);
|
||||
Main.logger.error('Failed to update:', err);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -355,38 +357,45 @@ export default class Main {
|
|||
}
|
||||
|
||||
static async main(app: Electron.App) {
|
||||
Main.application = app;
|
||||
const fileLogType = Updater.isUpdating() ? 'fileSync' : 'file';
|
||||
try {
|
||||
Main.application = app;
|
||||
const isUpdating = Updater.isUpdating();
|
||||
const fileLogType = (isUpdating && !Updater.updateApplied) ? 'fileSync' : 'file';
|
||||
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
out: { type: 'stdout' },
|
||||
log: { type: fileLogType, filename: path.join(app.getPath('logs'), 'fcast-receiver.log'), flags: 'w' },
|
||||
},
|
||||
categories: {
|
||||
default: { appenders: ['out', 'log'], level: 'info' },
|
||||
},
|
||||
});
|
||||
Main.logger = log4js.getLogger();
|
||||
Main.logger.info(`Starting application: ${app.name} (${app.getVersion()}) | ${app.getAppPath()}`);
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
out: { type: 'stdout' },
|
||||
log: { type: fileLogType, filename: path.join(app.getPath('logs'), 'fcast-receiver.log'), flags: 'a', maxLogSize: '10M' },
|
||||
},
|
||||
categories: {
|
||||
default: { appenders: ['out', 'log'], level: 'info' },
|
||||
},
|
||||
});
|
||||
Main.logger = log4js.getLogger();
|
||||
Main.logger.info(`Starting application: ${app.name} (${app.getVersion()} - ${Updater.getChannelVersion()}) | ${app.getAppPath()}`);
|
||||
|
||||
if (Updater.isUpdating()) {
|
||||
await Updater.processUpdate();
|
||||
if (isUpdating) {
|
||||
await Updater.processUpdate();
|
||||
}
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({
|
||||
'boolean-negation': false
|
||||
})
|
||||
.options({
|
||||
'no-main-window': { type: 'boolean', default: false, desc: "Start minimized to tray" },
|
||||
'fullscreen': { type: 'boolean', default: false, desc: "Start application in fullscreen" }
|
||||
})
|
||||
.parseSync();
|
||||
|
||||
Main.startFullscreen = argv.fullscreen;
|
||||
Main.shouldOpenMainWindow = !argv.noMainWindow;
|
||||
Main.application.on('ready', Main.onReady);
|
||||
Main.application.on('window-all-closed', () => { });
|
||||
}
|
||||
catch (err) {
|
||||
Main.logger.error(`Error starting application: ${err}`);
|
||||
app.exit();
|
||||
}
|
||||
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({
|
||||
'boolean-negation': false
|
||||
})
|
||||
.options({
|
||||
'no-main-window': { type: 'boolean', default: false, desc: "Start minimized to tray" },
|
||||
'fullscreen': { type: 'boolean', default: false, desc: "Start application in fullscreen" }
|
||||
})
|
||||
.parseSync();
|
||||
|
||||
Main.startFullscreen = argv.fullscreen;
|
||||
Main.shouldOpenMainWindow = !argv.noMainWindow;
|
||||
Main.application.on('ready', Main.onReady);
|
||||
Main.application.on('window-all-closed', () => { });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import * as crypto from 'crypto';
|
|||
import * as log4js from "log4js";
|
||||
import { app } from 'electron';
|
||||
import { Store } from './Store';
|
||||
import sudo from 'sudo-prompt';
|
||||
const extract = require('extract-zip');
|
||||
const logger = log4js.getLogger();
|
||||
|
||||
enum UpdateState {
|
||||
Copy,
|
||||
Cleanup,
|
||||
Copy = 'copy',
|
||||
Cleanup = 'cleanup',
|
||||
Error = 'error',
|
||||
};
|
||||
|
||||
interface ReleaseInfo {
|
||||
|
@ -27,6 +29,7 @@ interface ReleaseInfo {
|
|||
];
|
||||
channelCurrentVersions: [string: number];
|
||||
allVersions: [string];
|
||||
fileVersion: string;
|
||||
}
|
||||
|
||||
interface UpdateInfo {
|
||||
|
@ -34,14 +37,22 @@ interface UpdateInfo {
|
|||
installPath: string;
|
||||
tempPath: string;
|
||||
currentVersion: string;
|
||||
downloadFile: string;
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class Updater {
|
||||
private static readonly supportedReleasesJsonVersion = '1';
|
||||
|
||||
private static appPath: string = app.getAppPath();
|
||||
private static installPath: string = path.join(Updater.appPath, '../../');
|
||||
private static updateDataPath: string = path.join(app.getPath('userData'), 'updater');
|
||||
private static updateMetadataPath = path.join(Updater.updateDataPath, './update.json');
|
||||
private static baseUrl: string = 'https://dl.fcast.org/electron';
|
||||
private static channelVersion: string = null;
|
||||
|
||||
public static isDownloading: boolean = false;
|
||||
public static updateApplied: boolean = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private static async fetchJSON(url: string): Promise<any> {
|
||||
|
@ -81,149 +92,227 @@ export class Updater {
|
|||
});
|
||||
}
|
||||
|
||||
private static getDownloadFile(version: string) {
|
||||
let target: string = process.platform; // linux
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
target = 'windows';
|
||||
}
|
||||
else if (process.platform === 'darwin') {
|
||||
target = 'macOS';
|
||||
private static async applyUpdate(src: string, dst: string) {
|
||||
// Sanity removal protection check (especially under admin)
|
||||
if (!dst.includes('fcast-receiver')) {
|
||||
throw `Aborting update applying due to possible malformed path: ${dst}`;
|
||||
}
|
||||
|
||||
return `fcast-receiver-${version}-${target}-${process.arch}.zip`;
|
||||
try {
|
||||
fs.accessSync(dst, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy/remove operations
|
||||
process.noAsar = true
|
||||
fs.rmSync(dst, { recursive: true, force: true });
|
||||
fs.cpSync(src, dst, { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code === 'EACCES') {
|
||||
logger.info('Update requires admin privileges. Escalating...');
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const shell = process.platform === 'win32' ? 'powershell' : '';
|
||||
const command = `${shell} rm -rf ${dst}; ${shell} cp -rf ${src} ${dst}`
|
||||
|
||||
sudo.exec(command, { name: 'FCast Receiver' }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error(error);
|
||||
logger.warn(`stdout: ${stdout}`);
|
||||
logger.warn(`stderr: ${stderr}`);
|
||||
reject('User did not authorize the operation...');
|
||||
}
|
||||
|
||||
logger.info('stdout', stdout);
|
||||
logger.info('stderr', stderr);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static isUpdating() {
|
||||
return fs.existsSync(Updater.updateMetadataPath);
|
||||
public static restart() {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
|
||||
const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`;
|
||||
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
|
||||
const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName);
|
||||
|
||||
app.relaunch({ execPath: updateBinPath });
|
||||
app.exit();
|
||||
}
|
||||
|
||||
public static isUpdating(): boolean {
|
||||
try {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
|
||||
Updater.updateApplied = updateInfo.updateState === 'cleanup' ? true : false;
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static getChannelVersion(): string {
|
||||
if (Updater.channelVersion === null) {
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
Updater.channelVersion = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
}
|
||||
|
||||
return Updater.channelVersion;
|
||||
}
|
||||
|
||||
public static async processUpdate(): Promise<void> {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf-8'));
|
||||
const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`;
|
||||
try {
|
||||
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
|
||||
const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`;
|
||||
|
||||
switch (updateInfo.updateState) {
|
||||
case UpdateState.Copy: {
|
||||
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
|
||||
switch (updateInfo.updateState) {
|
||||
case UpdateState.Copy: {
|
||||
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
|
||||
|
||||
if (Updater.installPath === updateInfo.installPath) {
|
||||
logger.info('Update in progress. Restarting application to perform update...')
|
||||
const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName);
|
||||
try {
|
||||
logger.info('Updater process started...');
|
||||
const src = path.join(updateInfo.tempPath, extractionDir);
|
||||
logger.info(`Copying files from update directory ${src} to install directory ${updateInfo.installPath}`);
|
||||
|
||||
log4js.shutdown();
|
||||
app.relaunch({ execPath: updateBinPath });
|
||||
app.exit();
|
||||
Updater.applyUpdate(src, updateInfo.installPath);
|
||||
updateInfo.updateState = UpdateState.Cleanup;
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
|
||||
const installBinPath = path.join(updateInfo.installPath, binaryName);
|
||||
log4js.shutdown();
|
||||
app.relaunch({ execPath: installBinPath });
|
||||
app.exit();
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while applying update...');
|
||||
logger.error(err);
|
||||
|
||||
updateInfo.updateState = UpdateState.Error;
|
||||
updateInfo.error = JSON.stringify(err);
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
log4js.shutdown();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Updater process started...');
|
||||
const src = path.join(updateInfo.tempPath, extractionDir);
|
||||
logger.info(`Copying files from update directory ${src} to install directory ${updateInfo.installPath}`);
|
||||
case UpdateState.Cleanup: {
|
||||
try {
|
||||
logger.info('Performing update cleanup...')
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy
|
||||
process.noAsar = true
|
||||
logger.info(`rm dir ${path.join(Updater.updateDataPath, extractionDir)}`)
|
||||
fs.rmSync(path.join(Updater.updateDataPath, extractionDir), { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy
|
||||
process.noAsar = true
|
||||
fs.cpSync(src, updateInfo.installPath, { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
fs.rmSync(path.join(Updater.updateDataPath, updateInfo.downloadFile));
|
||||
fs.rmSync(Updater.updateMetadataPath);
|
||||
|
||||
updateInfo.updateState = UpdateState.Cleanup;
|
||||
await fs.promises.writeFile(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
// Removing the install directory causes an 'ENOENT: no such file or directory, uv_cwd' when calling process.cwd()
|
||||
// Need to fix the working directory to the update directory that overwritten the install directory
|
||||
process.chdir(Updater.installPath);
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while performing update cleanup...');
|
||||
logger.error(err);
|
||||
|
||||
const installBinPath = path.join(updateInfo.installPath, binaryName);
|
||||
log4js.shutdown();
|
||||
app.relaunch({ execPath: installBinPath });
|
||||
app.exit();
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while applying update...');
|
||||
logger.error(err);
|
||||
log4js.shutdown();
|
||||
app.exit();
|
||||
updateInfo.updateState = UpdateState.Error;
|
||||
updateInfo.error = JSON.stringify(err);
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case UpdateState.Cleanup: {
|
||||
try {
|
||||
logger.info('Performing update cleanup...')
|
||||
// Electron runtime sees .asar file as directory and causes errors during copy
|
||||
process.noAsar = true
|
||||
fs.rmSync(path.join(Updater.updateDataPath, extractionDir), { recursive: true, force: true });
|
||||
process.noAsar = false
|
||||
|
||||
fs.rmSync(path.join(Updater.updateDataPath, Updater.getDownloadFile(updateInfo.currentVersion)));
|
||||
fs.rmSync(Updater.updateMetadataPath);
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('Error while performing update cleanup...');
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
log4js.shutdown();
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
|
||||
return;
|
||||
case UpdateState.Error:
|
||||
logger.warn(`Update operation did not complete successfully: ${updateInfo.error}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.warn(`Error reading update metadata file, ignoring pending update: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
public static async update(): Promise<boolean> {
|
||||
logger.info('Updater invoked');
|
||||
if (!fs.existsSync(Updater.updateDataPath)) {
|
||||
try {
|
||||
fs.accessSync(Updater.updateDataPath, fs.constants.F_OK);
|
||||
}
|
||||
catch (err) {
|
||||
logger.info(`Directory does not exist: ${err}`);
|
||||
fs.mkdirSync(Updater.updateDataPath);
|
||||
}
|
||||
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf-8'));
|
||||
const releases = await Updater.fetchJSON(`${Updater.baseUrl}/releases.json`.toString()) as ReleaseInfo;
|
||||
const localPackage = JSON.parse(fs.readFileSync(path.join(Updater.appPath, './package.json'), 'utf8'));
|
||||
try {
|
||||
const releases = await Updater.fetchJSON(`${Updater.baseUrl}/releases_v${Updater.supportedReleasesJsonVersion}.json`.toString()) as ReleaseInfo;
|
||||
|
||||
let updaterSettings = Store.get('updater');
|
||||
if (updaterSettings === null) {
|
||||
updaterSettings = {
|
||||
'channel': localPackage.channel,
|
||||
let updaterSettings = Store.get('updater');
|
||||
if (updaterSettings === null) {
|
||||
updaterSettings = {
|
||||
'channel': localPackage.channel,
|
||||
}
|
||||
|
||||
Store.set('updater', updaterSettings);
|
||||
}
|
||||
|
||||
Store.set('updater', updaterSettings);
|
||||
const localChannelVersion: number = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
const currentChannelVersion: number = releases.channelCurrentVersions[localPackage.channel] ? releases.channelCurrentVersions[localPackage.channel] : 0
|
||||
logger.info('Update check', { channel: localPackage.channel, channel_version: localChannelVersion, localVersion: localPackage.version,
|
||||
currentVersion: releases.currentVersion, currentChannelVersion: currentChannelVersion });
|
||||
|
||||
if (localPackage.version !== releases.currentVersion || (localPackage.channel !== 'stable' && localChannelVersion < currentChannelVersion)) {
|
||||
const channel = localPackage.version !== releases.currentVersion ? 'stable' : localPackage.channel;
|
||||
const fileInfo = releases.currentReleases[channel][process.platform][process.arch]
|
||||
const file = fileInfo.url.toString().split('/').pop();
|
||||
|
||||
const destination = path.join(Updater.updateDataPath, file);
|
||||
logger.info(`Downloading '${fileInfo.url}' to '${destination}'.`);
|
||||
Updater.isDownloading = true;
|
||||
await Updater.downloadFile(fileInfo.url.toString(), destination);
|
||||
|
||||
const downloadedFile = await fs.promises.readFile(destination);
|
||||
const hash = crypto.createHash('sha256').end(downloadedFile).digest('hex');
|
||||
if (fileInfo.sha256Digest !== hash) {
|
||||
const message = 'Update failed integrity check. Please try checking for updates again or downloading the update manually.';
|
||||
logger.error(`Update failed integrity check. Expected hash: ${fileInfo.sha256Digest}, actual hash: ${hash}`);
|
||||
throw message;
|
||||
}
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during extraction
|
||||
logger.info('Extracting update...');
|
||||
process.noAsar = true;
|
||||
await extract(destination, { dir: path.dirname(destination) });
|
||||
process.noAsar = false;
|
||||
|
||||
logger.info('Extraction complete.');
|
||||
const updateInfo: UpdateInfo = {
|
||||
updateState: UpdateState.Copy,
|
||||
installPath: Updater.installPath,
|
||||
tempPath: path.dirname(destination),
|
||||
currentVersion: releases.currentVersion,
|
||||
downloadFile: file,
|
||||
};
|
||||
|
||||
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
logger.info('Written update metadata.');
|
||||
Updater.isDownloading = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const localChannelVersion: number = localPackage.channelVersion ? localPackage.channelVersion : 0
|
||||
const currentChannelVersion: number = releases.channelCurrentVersions[localPackage.channel] ? releases.channelCurrentVersions[localPackage.channel] : 0
|
||||
logger.info('Update check', { channel: localPackage.channel, localVersion: localPackage.version, currentVersion: releases.currentVersion });
|
||||
|
||||
if (localPackage.version !== releases.currentVersion || (localPackage.channel !== 'stable' && localChannelVersion < currentChannelVersion)) {
|
||||
const channel = localPackage.version !== releases.currentVersion ? 'stable' : localPackage.channel;
|
||||
const file = Updater.getDownloadFile(releases.currentVersion);
|
||||
const fileInfo = releases.currentReleases[channel][process.platform][process.arch]
|
||||
|
||||
const destination = path.join(Updater.updateDataPath, file);
|
||||
logger.info(`Downloading '${fileInfo.url}' to '${destination}'.`);
|
||||
await Updater.downloadFile(fileInfo.url.toString(), destination);
|
||||
|
||||
const downloadedFile = await fs.promises.readFile(destination);
|
||||
const hash = crypto.createHash('sha256').end(downloadedFile).digest('hex');
|
||||
if (fileInfo.sha256Digest !== hash) {
|
||||
const message = 'Update failed integrity check. Please try checking for updates again or downloading the update manually.';
|
||||
logger.error(`Update failed integrity check. Expected hash: ${fileInfo.sha256Digest}, actual hash: ${hash}`);
|
||||
throw message;
|
||||
}
|
||||
|
||||
// Electron runtime sees .asar file as directory and causes errors during extraction
|
||||
logger.info('Extracting update...');
|
||||
process.noAsar = true
|
||||
await extract(destination, { dir: path.dirname(destination) });
|
||||
process.noAsar = false
|
||||
|
||||
logger.info('Extraction complete.');
|
||||
const updateInfo: UpdateInfo = {
|
||||
updateState: UpdateState.Copy,
|
||||
installPath: Updater.installPath,
|
||||
tempPath: path.dirname(destination),
|
||||
currentVersion: releases.currentVersion,
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(Updater.updateMetadataPath, JSON.stringify(updateInfo));
|
||||
logger.info('Written update metadata.');
|
||||
return true;
|
||||
catch (err) {
|
||||
Updater.isDownloading = false;
|
||||
process.noAsar = false;
|
||||
logger.error(`Failed to check for updates: ${err}`);
|
||||
throw 'Failed to check for updates. Please try again later or visit https://fcast.org for updates.';
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue