/********************************************************************************
 * Copyright (C) 2020 TypeFox, EclipseSource and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import { BrowserWindow , Menu , remote } from '@theia/core/shared/electron' ; import { Command , CommandContribution , CommandRegistry , Emitter , MenuContribution , MenuModelRegistry , MenuPath , MessageService , Progress } from '@theia/core/lib/common' ; import { PreferenceScope , PreferenceService } from '@theia/core/lib/browser/preferences' ; import { TheiaUpdater , TheiaUpdaterClient , UpdaterError } from '../../common/updater/theia-updater' ; import { inject , injectable , postConstruct } from '@theia/core/shared/inversify' ; import { CommonMenus , OpenerService } from '@theia/core/lib/browser' ; import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory' ; import { isOSX } from '@theia/core/lib/common/os' ; import { setInterval , clearInterval } from 'timers' ; import URI from '@theia/core/lib/common/uri' ; export namespace TheiaUpdaterCommands { const category = 'Theia Electron Updater' ; export const CHECK_FOR_UPDATES = { id : 'electron-theia:check-for-updates' , label : 'Check for Updates...' , category } ; export const RESTART_TO_UPDATE = { id : 'electron-theia:restart-to-update' , label : 'Restart to Update' , category } ; } export namespace TheiaUpdaterMenu { export const MENU_PATH = [ ... CommonMenus . FILE_SETTINGS_SUBMENU , '3_settings_submenu_update' ] ; } @ injectable ( ) export class TheiaUpdaterClientImpl implements TheiaUpdaterClient { @ inject ( PreferenceService ) private readonly preferenceService : PreferenceService ; protected readonly onReadyToInstallEmitter = new Emitter < void > ( ) ; readonly onReadyToInstall = this . onReadyToInstallEmitter . event ; protected readonly onUpdateAvailableEmitter = new Emitter < boolean > ( ) ; readonly onUpdateAvailable = this . onUpdateAvailableEmitter . event ; protected readonly onErrorEmitter = new Emitter < UpdaterError > ( ) ; readonly onError = this . onErrorEmitter . event ; notifyReadyToInstall ( ) { this . onReadyToInstallEmitter . fire ( ) ; } updateAvailable ( available , startupCheck ) { if ( startupCheck ) { this . preferenceService . ready . then ( ( ) => { setTimeout ( ( ) => { const reportOnStart = this . preferenceService . get ( 'updates.reportOnStart' , true ) ; if ( reportOnStart ) { this . onUpdateAvailableEmitter . fire ( available ) ; } } , 10000 ) ; } ) ; } else { this . onUpdateAvailableEmitter . fire ( available ) ; } } reportError ( error ) { this . onErrorEmitter . fire ( error ) ; } } @ injectable ( ) export class ElectronMenuUpdater { @ inject ( ElectronMainMenuFactory ) protected readonly factory : ElectronMainMenuFactory ; public update ( ) { this . setMenu ( ) ; } private setMenu ( menu = this . factory . createElectronMenuBar ( ) , electronWindow = remote . getCurrentWindow ( ) ) { if ( isOSX ) { remote . Menu . setApplicationMenu ( menu ) ; } else { electronWindow . setMenu ( menu ) ; } } } @ injectable ( ) export class TheiaUpdaterFrontendContribution implements CommandContribution , MenuContribution { @ inject ( MessageService ) protected readonly messageService : MessageService ; @ inject ( ElectronMenuUpdater ) protected readonly menuUpdater : ElectronMenuUpdater ; @ inject ( TheiaUpdater ) protected readonly updater : TheiaUpdater ; @ inject ( TheiaUpdaterClientImpl ) protected readonly updaterClient : TheiaUpdaterClientImpl ; @ inject ( PreferenceService ) private readonly preferenceService : PreferenceService ; @ inject ( OpenerService ) protected readonly openerService : OpenerService ; protected readyToUpdate = false ; private progress : Progress | undefined ; private intervalId : NodeJS . Timeout | undefined ; @ postConstruct ( ) protected init ( ) { this . updaterClient . onUpdateAvailable ( available => { if ( available ) { this . handleDownloadUpdate ( ) ; } else { this . handleNoUpdate ( ) ; } } ) ; this . updaterClient . onReadyToInstall ( async ( ) => { this . readyToUpdate = true ; this . menuUpdater . update ( ) ; this . handleUpdatesAvailable ( ) ; } ) ; this . updaterClient . onError ( error => this . handleError ( error ) ) ; } registerCommands ( registry ) { registry . registerCommand ( TheiaUpdaterCommands . CHECK_FOR_UPDATES , { execute : async ( ) => { this . updater . checkForUpdates ( ) ; } , isEnabled : ( ) => ! this . readyToUpdate , isVisible : ( ) => ! this . readyToUpdate } ) ; registry . registerCommand ( TheiaUpdaterCommands . RESTART_TO_UPDATE , { execute : ( ) => this . updater . onRestartToUpdateRequested ( ) , isEnabled : ( ) => this . readyToUpdate , isVisible : ( ) => this . readyToUpdate } ) ; } registerMenus ( registry ) { registry . registerMenuAction ( TheiaUpdaterMenu . MENU_PATH , { commandId : TheiaUpdaterCommands . CHECK_FOR_UPDATES . id } ) ; registry . registerMenuAction ( TheiaUpdaterMenu . MENU_PATH , { commandId : TheiaUpdaterCommands . RESTART_TO_UPDATE . id } ) ; } protected async handleDownloadUpdate ( ) { const answer = await this . messageService . info ( 'Updates found, do you want to download the update?' , 'No' , 'Yes' , 'Never' ) ; if ( answer === 'Never' ) { this . preferenceService . set ( 'updates.reportOnStart' , false , PreferenceScope . User ) ; return ; } if ( answer === 'Yes' ) { this . stopProgress ( ) ; this . progress = await this . messageService . showProgress ( { text : 'Blueprint Update' } ) ; let dots = 0 ; this . intervalId = setInterval ( ( ) => { if ( this . progress !== undefined ) { dots = ( dots + 1 ) % 4 ; this . progress . report ( { message : 'Downloading' + '.' . repeat ( dots ) } ) ; } } , 1000 ) ; this . updater . downloadUpdate ( ) ; } } protected async handleNoUpdate ( ) { this . messageService . info ( 'Already using the latest version' ) ; } protected async handleUpdatesAvailable ( ) { if ( this . progress !== undefined ) { this . progress . report ( { work : { done : 1 , total : 1 } } ) ; this . stopProgress ( ) ; } const answer = await this . messageService . info ( 'Ready to update. Do you want to update now? (This will restart the application)' , 'No' , 'Yes' ) ; if ( answer === 'Yes' ) { this . updater . onRestartToUpdateRequested ( ) ; } } protected async handleError ( error ) { this . stopProgress ( ) ; if ( error . errorLogPath ) { const viewLogAction = 'View Error Log' ; const answer = await this . messageService . error ( error . message , viewLogAction ) ; if ( answer === viewLogAction ) { const uri = new URI ( error . errorLogPath ) ; const opener = await this . openerService . getOpener ( uri ) ; opener . open ( uri ) ; } } else { this . messageService . error ( error . message ) ; } } private stopProgress ( ) { if ( this . intervalId !== undefined ) { clearInterval ( this . intervalId ) ; this . intervalId = undefined ; } if ( this . progress !== undefined ) { this . progress . cancel ( ) ; this . progress = undefined ; } } }