diff --git a/electron/index.js b/electron/index.js index 9671a6eb2..fda80eac3 100644 --- a/electron/index.js +++ b/electron/index.js @@ -36,26 +36,21 @@ try { autoUpdater.autoDownload = !upgradeDisabled; autoUpdater.allowPrerelease = false; -// This is set to true if an auto update has been downloaded through the Electron -// auto-update system and is ready to install. If the user declined an update earlier, -// it will still install on shutdown. -let autoUpdateDownloaded = false; +const UPDATE_STATE_INIT = 0; +const UPDATE_STATE_CHECKING = 1; +const UPDATE_STATE_UPDATES_FOUND = 2; +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 // that we show on Windows after you decline an upgrade and close the app later. 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 // object is garbage collected. let rendererWindow; @@ -243,7 +238,8 @@ app.on('activate', () => { app.on('will-quit', event => { if ( process.platform === 'win32' && - autoUpdateDownloaded && + updateState === UPDATE_STATE_DOWNLOADED && + isAutoUpdateSupported && !appState.autoUpdateAccepted && !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', () => { function formatRc(ver) { // Adds dash if needed to make RC suffix SemVer friendly @@ -537,3 +415,147 @@ process.on('uncaughtException', error => { if (daemon) daemon.quit(); 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(); +}); \ No newline at end of file diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 37954c7e2..470f44c7f 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -57,6 +57,8 @@ export const UPDATE_REMOTE_VERSION = 'UPDATE_REMOTE_VERSION'; export const SKIP_UPGRADE = 'SKIP_UPGRADE'; export const START_UPGRADE = 'START_UPGRADE'; 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 CLEAR_UPGRADE_TIMER = 'CLEAR_UPGRADE_TIMER'; diff --git a/ui/index.jsx b/ui/index.jsx index b31b4e418..00b72e6df 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -22,6 +22,8 @@ import { doUpdateDownloadProgress, doNotifyUpdateAvailable, doShowUpgradeInstallationError, + doAutoUpdateReset, + doAutoUpdateFail, } from 'redux/actions/app'; import { isURIValid } from 'util/lbryURI'; import { setSearchApi } from 'redux/actions/search'; @@ -129,10 +131,23 @@ ipcRenderer.on('open-uri-requested', (event, url, newSession) => { handleError(); }); +autoUpdater.on('checking-for-update', () => { + app.store.dispatch(doAutoUpdateReset()); +}); + autoUpdater.on('update-available', (e) => { + app.store.dispatch(doAutoUpdateReset()); 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', () => { app.store.dispatch(doShowUpgradeInstallationError()); }); diff --git a/ui/modal/modalAutoUpdateDownloaded/index.js b/ui/modal/modalAutoUpdateDownloaded/index.js index afe8e8fa4..2b98538dd 100644 --- a/ui/modal/modalAutoUpdateDownloaded/index.js +++ b/ui/modal/modalAutoUpdateDownloaded/index.js @@ -1,13 +1,15 @@ import { connect } from 'react-redux'; import { doAutoUpdateDeclined, doHideModal } from 'redux/actions/app'; +import { selectAutoUpdateFailed } from 'redux/selectors/app'; import ModalAutoUpdateDownloaded from './view'; -const perform = dispatch => ({ +const select = (state, props) => ({ + errorWhileUpdating: selectAutoUpdateFailed(state), +}); + +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), declineAutoUpdate: () => dispatch(doAutoUpdateDeclined()), }); -export default connect( - null, - perform -)(ModalAutoUpdateDownloaded); +export default connect(select, perform)(ModalAutoUpdateDownloaded); diff --git a/ui/modal/modalAutoUpdateDownloaded/view.jsx b/ui/modal/modalAutoUpdateDownloaded/view.jsx index aecb049b4..7110cfb94 100644 --- a/ui/modal/modalAutoUpdateDownloaded/view.jsx +++ b/ui/modal/modalAutoUpdateDownloaded/view.jsx @@ -7,13 +7,15 @@ import { Modal } from 'modal/modal'; import LastReleaseChanges from 'component/lastReleaseChanges'; type Props = { - closeModal: any => any, + closeModal: (any) => any, declineAutoUpdate: () => any, + errorWhileUpdating: boolean, }; const ModalAutoUpdateDownloaded = (props: Props) => { - const { closeModal, declineAutoUpdate } = props; + const { closeModal, declineAutoUpdate, errorWhileUpdating } = props; const [disabled, setDisabled] = useState(false); + const isDownloading = disabled && !errorWhileUpdating; const handleConfirm = () => { setDisabled(true); @@ -31,13 +33,14 @@ const ModalAutoUpdateDownloaded = (props: Props) => { type="confirm" contentLabel={__('Upgrade Downloaded')} title={__('LBRY leveled up')} - confirmButtonLabel={__('Upgrade Now')} + confirmButtonLabel={isDownloading ? __('Downloading...') : __('Upgrade Now')} abortButtonLabel={__('Not Now')} - confirmButtonDisabled={disabled} + confirmButtonDisabled={isDownloading} onConfirmed={handleConfirm} onAborted={handleAbort} > + {errorWhileUpdating &&

__('There was an error while updating. Please try again.')

} ); }; diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index f8916d36e..f4aaa2c46 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -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. */ diff --git a/ui/redux/reducers/app.js b/ui/redux/reducers/app.js index 5426e56b8..41ec971b5 100644 --- a/ui/redux/reducers/app.js +++ b/ui/redux/reducers/app.js @@ -27,6 +27,7 @@ export type AppState = { badgeNumber: number, volume: number, autoUpdateDeclined: boolean, + autoUpdateFailed: boolean, modalsAllowed: boolean, downloadProgress: ?number, upgradeDownloading: ?boolean, @@ -64,6 +65,7 @@ const defaultState: AppState = { muted: false, autoUpdateDownloaded: false, autoUpdateDeclined: false, + autoUpdateFailed: false, modalsAllowed: true, hasClickedComment: false, downloadProgress: undefined, @@ -152,6 +154,16 @@ reducers[ACTIONS.AUTO_UPDATE_DECLINED] = (state) => 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) => Object.assign({}, state, { downloadPath: action.data.path, diff --git a/ui/redux/selectors/app.js b/ui/redux/selectors/app.js index 920267b62..8e1cefeb9 100644 --- a/ui/redux/selectors/app.js +++ b/ui/redux/selectors/app.js @@ -54,6 +54,8 @@ export const selectAutoUpdateDownloaded = createSelector(selectState, (state) => export const selectAutoUpdateDeclined = createSelector(selectState, (state) => state.autoUpdateDeclined); +export const selectAutoUpdateFailed = createSelector(selectState, (state) => state.autoUpdateFailed); + export const selectIsUpdateModalDisplayed = createSelector(selectState, (state) => { return [MODALS.AUTO_UPDATE_DOWNLOADED, MODALS.UPGRADE, MODALS.DOWNLOADING].includes(state.modal); });