Handle error case for auto updater.

This commit is contained in:
Franco Montenegro 2022-06-28 20:00:24 -03:00
parent 8a7d89c857
commit d2648c9f62
8 changed files with 213 additions and 143 deletions

View file

@ -36,26 +36,21 @@ try {
autoUpdater.autoDownload = !upgradeDisabled; autoUpdater.autoDownload = !upgradeDisabled;
autoUpdater.allowPrerelease = false; autoUpdater.allowPrerelease = false;
// This is set to true if an auto update has been downloaded through the Electron const UPDATE_STATE_INIT = 0;
// auto-update system and is ready to install. If the user declined an update earlier, const UPDATE_STATE_CHECKING = 1;
// it will still install on shutdown. const UPDATE_STATE_UPDATES_FOUND = 2;
let autoUpdateDownloaded = false; const UPDATE_STATE_NO_UPDATES_FOUND = 3;
const UPDATE_STATE_DOWNLOADING = 4;
const UPDATE_STATE_DOWNLOADED = 5;
let updateState = UPDATE_STATE_INIT;
let updateDownloadItem;
const isAutoUpdateSupported = ['win32', 'darwin'].includes(process.platform) || !!process.env.APPIMAGE;
// This is used to keep track of whether we are showing the special dialog // This is used to keep track of whether we are showing the special dialog
// that we show on Windows after you decline an upgrade and close the app later. // that we show on Windows after you decline an upgrade and close the app later.
let showingAutoUpdateCloseAlert = false; let showingAutoUpdateCloseAlert = false;
// This is used to prevent downloading updates multiple times when
// using the auto updater API.
// As read in the documentation:
// "Calling autoUpdater.checkForUpdates() twice will download the update two times."
// https://www.electronjs.org/docs/latest/api/auto-updater#autoupdatercheckforupdates
let keepCheckingForUpdates = true;
let downloadUpgradeInitiated = false;
let downloadUpgradeItem;
// Keep a global reference, if you don't, they will be closed automatically when the JavaScript // Keep a global reference, if you don't, they will be closed automatically when the JavaScript
// object is garbage collected. // object is garbage collected.
let rendererWindow; let rendererWindow;
@ -243,7 +238,8 @@ app.on('activate', () => {
app.on('will-quit', event => { app.on('will-quit', event => {
if ( if (
process.platform === 'win32' && process.platform === 'win32' &&
autoUpdateDownloaded && updateState === UPDATE_STATE_DOWNLOADED &&
isAutoUpdateSupported &&
!appState.autoUpdateAccepted && !appState.autoUpdateAccepted &&
!showingAutoUpdateCloseAlert !showingAutoUpdateCloseAlert
) { ) {
@ -327,124 +323,6 @@ ipcMain.on('get-disk-space', async (event) => {
} }
}); });
ipcMain.on('cancel-download-upgrade', () => {
if (downloadUpgradeItem) {
// Cancel the download and execute the onCancel
// callback set in the options.
downloadUpgradeItem.cancel();
}
});
ipcMain.on('download-upgrade', async (event, params) => {
// Prevent downloading multiple times.
if (downloadUpgradeInitiated || downloadUpgradeItem) {
return;
}
const { url, options } = params;
const dir = fs.mkdtempSync(app.getPath('temp') + path.sep);
downloadUpgradeInitiated = true;
// Grab the download item's handler to allow
// cancelling the operation if required.
options.onStarted = function(downloadItem) {
downloadUpgradeItem = downloadItem;
};
options.onCancel = function() {
downloadUpgradeItem = undefined;
downloadUpgradeInitiated = false;
};
options.onProgress = function(p) {
rendererWindow.webContents.send('download-progress-update', p);
};
options.directory = dir;
options.onCompleted = function(c) {
downloadUpgradeInitiated = false;
downloadUpgradeItem = undefined;
rendererWindow.webContents.send('download-update-complete', c);
};
const win = BrowserWindow.getFocusedWindow();
await download(win, url, options).catch(e => console.log('e', e));
});
ipcMain.on('upgrade', (event, installerPath) => {
// what to do if no shutdown in a long time?
console.log('Update downloaded to', installerPath);
console.log('The app will close and you will be prompted to install the latest version of LBRY.');
console.log('After the install is complete, please reopen the app.');
// Prevent .deb package from opening with archive manager (Ubuntu >= 20)
if (process.platform === 'linux' && !process.env.APPIMAGE) {
sudo.exec(`dpkg -i ${installerPath}`, { name: app.name }, (err, stdout, stderr) => {
if (err || stderr) {
rendererWindow.webContents.send('upgrade-installing-error');
return;
}
// Re-launch the application when the installation finishes.
app.relaunch();
app.quit();
});
return;
}
app.on('quit', () => {
console.log('Launching upgrade installer at', installerPath);
// This gets triggered called after *all* other quit-related events, so
// we'll only get here if we're fully prepared and quitting for real.
shell.openPath(installerPath);
});
app.quit();
});
ipcMain.on('check-for-updates', (event, autoDownload) => {
// Prevent downloading the same update multiple times.
if (!keepCheckingForUpdates) {
return;
}
keepCheckingForUpdates = false;
autoUpdater.autoDownload = autoDownload;
autoUpdater.checkForUpdates();
});
autoUpdater.on('update-downloaded', () => {
autoUpdateDownloaded = true;
// If this download was trigger by
// autoUpdateAccepted it means, the user
// wants to install the new update but
// needed to downloaded the files first.
if (appState.autoUpdateAccepted) {
autoUpdater.quitAndInstall();
}
});
autoUpdater.on('update-not-available', () => {
keepCheckingForUpdates = true;
});
ipcMain.on('autoUpdateAccepted', () => {
appState.autoUpdateAccepted = true;
// quitAndInstall can only be called if the
// update has been downloaded. Since the user
// can disable auto updates, we have to make
// sure it has been downloaded first.
if (autoUpdateDownloaded) {
autoUpdater.quitAndInstall();
return;
}
// If the update hasn't been downloaded,
// start downloading it. After it's done, the
// event 'update-downloaded' will be triggered,
// where we will be able to resume the
// update installation.
autoUpdater.downloadUpdate();
});
ipcMain.on('version-info-requested', () => { ipcMain.on('version-info-requested', () => {
function formatRc(ver) { function formatRc(ver) {
// Adds dash if needed to make RC suffix SemVer friendly // Adds dash if needed to make RC suffix SemVer friendly
@ -537,3 +415,147 @@ process.on('uncaughtException', error => {
if (daemon) daemon.quit(); if (daemon) daemon.quit();
app.exit(1); app.exit(1);
}); });
// Auto updater
// autoUpdater.on('download-progress', (p) => {
// rendererWindow.webContents.send('download-progress-update', p);
// });
autoUpdater.on('update-downloaded', () => {
updateState = UPDATE_STATE_DOWNLOADED;
// If this download was trigger by
// autoUpdateAccepted it means, the user
// wants to install the new update but
// needed to downloaded the files first.
if (appState.autoUpdateAccepted) {
autoUpdater.quitAndInstall();
}
});
autoUpdater.on('update-not-available', () => {
updateState = UPDATE_STATE_NO_UPDATES_FOUND;
});
autoUpdater.on('error', () => {
updateState = UPDATE_STATE_INIT;
});
// Manual (.deb) update
ipcMain.on('cancel-download-upgrade', () => {
if (updateDownloadItem) {
// Cancel the download and execute the onCancel
// callback set in the options.
updateDownloadItem.cancel();
}
});
ipcMain.on('download-upgrade', (event, params) => {
if (updateState !== UPDATE_STATE_UPDATES_FOUND) {
return;
}
if (isAutoUpdateSupported) {
updateState = UPDATE_STATE_DOWNLOADING;
autoUpdater.downloadUpdate();
return;
}
const { url, options } = params;
const dir = fs.mkdtempSync(app.getPath('temp') + path.sep);
updateState = UPDATE_STATE_DOWNLOADING;
// Grab the download item's handler to allow
// cancelling the operation if required.
options.onStarted = function(downloadItem) {
updateDownloadItem = downloadItem;
};
options.onCancel = function() {
updateState = UPDATE_STATE_UPDATES_FOUND;
updateDownloadItem = undefined;
};
options.onProgress = function(p) {
rendererWindow.webContents.send('download-progress-update', p);
};
options.onCompleted = function(c) {
updateState = UPDATE_STATE_DOWNLOADED;
updateDownloadItem = undefined;
rendererWindow.webContents.send('download-update-complete', c);
};
options.directory = dir;
const win = BrowserWindow.getFocusedWindow();
download(win, url, options).catch(e => {
updateState = UPDATE_STATE_UPDATES_FOUND;
console.log('e', e);
});
});
// Update behavior
ipcMain.on('autoUpdateAccepted', () => {
appState.autoUpdateAccepted = true;
// quitAndInstall can only be called if the
// update has been downloaded. Since the user
// can disable auto updates, we have to make
// sure it has been downloaded first.
if (updateState === UPDATE_STATE_DOWNLOADED) {
autoUpdater.quitAndInstall();
return;
}
// If the update hasn't been downloaded,
// start downloading it. After it's done, the
// event 'update-downloaded' will be triggered,
// where we will be able to resume the
// update installation.
updateState = UPDATE_STATE_DOWNLOADING;
autoUpdater.downloadUpdate();
});
ipcMain.on('check-for-updates', (event, autoDownload) => {
if (![UPDATE_STATE_INIT, UPDATE_STATE_NO_UPDATES_FOUND].includes(updateState)) {
return;
}
updateState = UPDATE_STATE_CHECKING;
// If autoDownload is true, checkForUpdates will begin the
// download automatically.
if (autoDownload) {
updateState = UPDATE_STATE_DOWNLOADING;
}
autoUpdater.autoDownload = autoDownload;
autoUpdater.checkForUpdates();
});
ipcMain.on('upgrade', (event, installerPath) => {
// what to do if no shutdown in a long time?
console.log('Update downloaded to', installerPath);
console.log('The app will close and you will be prompted to install the latest version of LBRY.');
console.log('After the install is complete, please reopen the app.');
// Prevent .deb package from opening with archive manager (Ubuntu >= 20)
if (process.platform === 'linux' && !process.env.APPIMAGE) {
sudo.exec(`dpkg -i ${installerPath}`, { name: app.name }, (err, stdout, stderr) => {
if (err || stderr) {
rendererWindow.webContents.send('upgrade-installing-error');
return;
}
// Re-launch the application when the installation finishes.
app.relaunch();
app.quit();
});
return;
}
app.on('quit', () => {
console.log('Launching upgrade installer at', installerPath);
// This gets triggered called after *all* other quit-related events, so
// we'll only get here if we're fully prepared and quitting for real.
shell.openPath(installerPath);
});
app.quit();
});

View file

@ -57,6 +57,8 @@ export const UPDATE_REMOTE_VERSION = 'UPDATE_REMOTE_VERSION';
export const SKIP_UPGRADE = 'SKIP_UPGRADE'; export const SKIP_UPGRADE = 'SKIP_UPGRADE';
export const START_UPGRADE = 'START_UPGRADE'; export const START_UPGRADE = 'START_UPGRADE';
export const AUTO_UPDATE_DECLINED = 'AUTO_UPDATE_DECLINED'; export const AUTO_UPDATE_DECLINED = 'AUTO_UPDATE_DECLINED';
export const AUTO_UPDATE_RESET = 'AUTO_UPDATE_RESET';
export const AUTO_UPDATE_FAILED = 'AUTO_UPDATE_FAILED';
export const AUTO_UPDATE_DOWNLOADED = 'AUTO_UPDATE_DOWNLOADED'; export const AUTO_UPDATE_DOWNLOADED = 'AUTO_UPDATE_DOWNLOADED';
export const CLEAR_UPGRADE_TIMER = 'CLEAR_UPGRADE_TIMER'; export const CLEAR_UPGRADE_TIMER = 'CLEAR_UPGRADE_TIMER';

View file

@ -22,6 +22,8 @@ import {
doUpdateDownloadProgress, doUpdateDownloadProgress,
doNotifyUpdateAvailable, doNotifyUpdateAvailable,
doShowUpgradeInstallationError, doShowUpgradeInstallationError,
doAutoUpdateReset,
doAutoUpdateFail,
} from 'redux/actions/app'; } from 'redux/actions/app';
import { isURIValid } from 'util/lbryURI'; import { isURIValid } from 'util/lbryURI';
import { setSearchApi } from 'redux/actions/search'; import { setSearchApi } from 'redux/actions/search';
@ -129,10 +131,23 @@ ipcRenderer.on('open-uri-requested', (event, url, newSession) => {
handleError(); handleError();
}); });
autoUpdater.on('checking-for-update', () => {
app.store.dispatch(doAutoUpdateReset());
});
autoUpdater.on('update-available', (e) => { autoUpdater.on('update-available', (e) => {
app.store.dispatch(doAutoUpdateReset());
app.store.dispatch(doNotifyUpdateAvailable(e)); app.store.dispatch(doNotifyUpdateAvailable(e));
}); });
autoUpdater.on('update-downloaded', () => {
app.store.dispatch(doAutoUpdateReset());
});
autoUpdater.on('error', () => {
app.store.dispatch(doAutoUpdateFail());
});
ipcRenderer.on('upgrade-installing-error', () => { ipcRenderer.on('upgrade-installing-error', () => {
app.store.dispatch(doShowUpgradeInstallationError()); app.store.dispatch(doShowUpgradeInstallationError());
}); });

View file

@ -1,13 +1,15 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doAutoUpdateDeclined, doHideModal } from 'redux/actions/app'; import { doAutoUpdateDeclined, doHideModal } from 'redux/actions/app';
import { selectAutoUpdateFailed } from 'redux/selectors/app';
import ModalAutoUpdateDownloaded from './view'; import ModalAutoUpdateDownloaded from './view';
const perform = dispatch => ({ const select = (state, props) => ({
errorWhileUpdating: selectAutoUpdateFailed(state),
});
const perform = (dispatch) => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
declineAutoUpdate: () => dispatch(doAutoUpdateDeclined()), declineAutoUpdate: () => dispatch(doAutoUpdateDeclined()),
}); });
export default connect( export default connect(select, perform)(ModalAutoUpdateDownloaded);
null,
perform
)(ModalAutoUpdateDownloaded);

View file

@ -7,13 +7,15 @@ import { Modal } from 'modal/modal';
import LastReleaseChanges from 'component/lastReleaseChanges'; import LastReleaseChanges from 'component/lastReleaseChanges';
type Props = { type Props = {
closeModal: any => any, closeModal: (any) => any,
declineAutoUpdate: () => any, declineAutoUpdate: () => any,
errorWhileUpdating: boolean,
}; };
const ModalAutoUpdateDownloaded = (props: Props) => { const ModalAutoUpdateDownloaded = (props: Props) => {
const { closeModal, declineAutoUpdate } = props; const { closeModal, declineAutoUpdate, errorWhileUpdating } = props;
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const isDownloading = disabled && !errorWhileUpdating;
const handleConfirm = () => { const handleConfirm = () => {
setDisabled(true); setDisabled(true);
@ -31,13 +33,14 @@ const ModalAutoUpdateDownloaded = (props: Props) => {
type="confirm" type="confirm"
contentLabel={__('Upgrade Downloaded')} contentLabel={__('Upgrade Downloaded')}
title={__('LBRY leveled up')} title={__('LBRY leveled up')}
confirmButtonLabel={__('Upgrade Now')} confirmButtonLabel={isDownloading ? __('Downloading...') : __('Upgrade Now')}
abortButtonLabel={__('Not Now')} abortButtonLabel={__('Not Now')}
confirmButtonDisabled={disabled} confirmButtonDisabled={isDownloading}
onConfirmed={handleConfirm} onConfirmed={handleConfirm}
onAborted={handleAbort} onAborted={handleAbort}
> >
<LastReleaseChanges /> <LastReleaseChanges />
{errorWhileUpdating && <p>__('There was an error while updating. Please try again.')</p>}
</Modal> </Modal>
); );
}; };

View file

@ -220,6 +220,18 @@ export function doNotifyUpdateAvailable(e) {
}; };
} }
export function doAutoUpdateReset() {
return {
type: ACTIONS.AUTO_UPDATE_RESET,
};
}
export function doAutoUpdateFail() {
return {
type: ACTIONS.AUTO_UPDATE_FAILED,
};
}
/* /*
Initiate a timer that will check for an app upgrade every 10 minutes. Initiate a timer that will check for an app upgrade every 10 minutes.
*/ */

View file

@ -27,6 +27,7 @@ export type AppState = {
badgeNumber: number, badgeNumber: number,
volume: number, volume: number,
autoUpdateDeclined: boolean, autoUpdateDeclined: boolean,
autoUpdateFailed: boolean,
modalsAllowed: boolean, modalsAllowed: boolean,
downloadProgress: ?number, downloadProgress: ?number,
upgradeDownloading: ?boolean, upgradeDownloading: ?boolean,
@ -64,6 +65,7 @@ const defaultState: AppState = {
muted: false, muted: false,
autoUpdateDownloaded: false, autoUpdateDownloaded: false,
autoUpdateDeclined: false, autoUpdateDeclined: false,
autoUpdateFailed: false,
modalsAllowed: true, modalsAllowed: true,
hasClickedComment: false, hasClickedComment: false,
downloadProgress: undefined, downloadProgress: undefined,
@ -152,6 +154,16 @@ reducers[ACTIONS.AUTO_UPDATE_DECLINED] = (state) =>
autoUpdateDeclined: true, autoUpdateDeclined: true,
}); });
reducers[ACTIONS.AUTO_UPDATE_RESET] = (state) =>
Object.assign({}, state, {
autoUpdateFailed: false,
});
reducers[ACTIONS.AUTO_UPDATE_FAILED] = (state) =>
Object.assign({}, state, {
autoUpdateFailed: true,
});
reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) => reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) =>
Object.assign({}, state, { Object.assign({}, state, {
downloadPath: action.data.path, downloadPath: action.data.path,

View file

@ -54,6 +54,8 @@ export const selectAutoUpdateDownloaded = createSelector(selectState, (state) =>
export const selectAutoUpdateDeclined = createSelector(selectState, (state) => state.autoUpdateDeclined); export const selectAutoUpdateDeclined = createSelector(selectState, (state) => state.autoUpdateDeclined);
export const selectAutoUpdateFailed = createSelector(selectState, (state) => state.autoUpdateFailed);
export const selectIsUpdateModalDisplayed = createSelector(selectState, (state) => { export const selectIsUpdateModalDisplayed = createSelector(selectState, (state) => {
return [MODALS.AUTO_UPDATE_DOWNLOADED, MODALS.UPGRADE, MODALS.DOWNLOADING].includes(state.modal); return [MODALS.AUTO_UPDATE_DOWNLOADED, MODALS.UPGRADE, MODALS.DOWNLOADING].includes(state.modal);
}); });