diff --git a/receivers/electron/package-lock.json b/receivers/electron/package-lock.json index 2ef4fc4..9fcd105 100644 --- a/receivers/electron/package-lock.json +++ b/receivers/electron/package-lock.json @@ -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", diff --git a/receivers/electron/package.json b/receivers/electron/package.json index e46da80..e17904a 100644 --- a/receivers/electron/package.json +++ b/receivers/electron/package.json @@ -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", diff --git a/receivers/electron/scripts/deploy/deploy.py b/receivers/electron/scripts/deploy/deploy.py index 32c1a65..2aaa3cd 100644 --- a/receivers/electron/scripts/deploy/deploy.py +++ b/receivers/electron/scripts/deploy/deploy.py @@ -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() diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index e5d86d9..4d1a583 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -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', () => { }); } } diff --git a/receivers/electron/src/Updater.ts b/receivers/electron/src/Updater.ts index e9266de..49c55a8 100644 --- a/receivers/electron/src/Updater.ts +++ b/receivers/electron/src/Updater.ts @@ -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 { @@ -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((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 { - 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 { 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;