import * as fs from 'fs' ; import * as path from 'path' ; import * as os from 'os' ; import ServicePaths from '../services/service.paths' ; import Logger from '../tools/env.logger' ; import ServicePackage from './service.package' ; import ServiceElectron , { Subscription , IPCMessages } from './service.electron' ; import ServiceProduction from './service.production' ; import GitHubClient from '../tools/env.github.client' ; import ServiceSettings from './service.settings' ; import { IReleaseAsset , IReleaseData } from '../tools/env.github.client' ; import { getPlatform , EPlatforms } from '../tools/env.os' ; import { IService } from '../interfaces/interface.service' ; import { StandardBoolean } from '../controllers/settings/settings.standard.boolean' ; import { IApplication , EExitCodes } from '../interfaces/interface.app' ; import { sequences } from '../tools/sequences' ; import { Entry , ESettingType } from '../../../common/settings/field.store' ; const CHooks = { alias : '<alias>' , version : '<version>' , platform : '<platform>' , } ; const CReleaseNameAliases = [ 'logviewer' , 'chipmunk' ] ; const CAssetFilePattern = `${ CHooks . alias }@${ CHooks . version }-${ CHooks . platform }-portable.tgz` ; const CSettings = { repo : 'chipmunk' , } ; class description class ServiceUpdate implements IService { private _logger : Logger = new Logger ( 'ServiceUpdate' ) ; private _target : string | undefined ; private _tgzfile : string | undefined ; private _subscription : { [ key ] : Subscription ; } = { } ; private _app : IApplication | undefined ; private _settings : { updates : StandardBoolean ; } = { updates : new StandardBoolean ( { key : 'ApplicationUpdates' , name : 'Automatically update chipmunk' , desc : 'Update chipmunk automatically if update is available' , path : 'general.updates' , type : ESettingType . standard } , 'ApplicationUpdates' ) , } ; returns public init ( app ) { return new Promise ( ( resolve , reject ) => { if ( app === undefined ) { return reject ( new Error ( `Instance of main process is required.` ) ) ; } this . _app = app ; Promise . all ( [ ServiceElectron . IPC . subscribe ( ServiceElectron . IPCMessages . RenderState , this . _onRenderState . bind ( this ) ) . then ( ( subscription ) => { this . _subscription . RenderState = subscription ; } ) , ServiceElectron . IPC . subscribe ( ServiceElectron . IPCMessages . UpdateRequest , this . _onUpdateRequest . bind ( this ) ) . then ( ( subscription ) => { this . _subscription . UpdateRequest = subscription ; } ) , ] ) . catch ( ( error ) => { this . _logger . warn ( `Fail to make subscriptions to due error: ${ error . message }` ) ; } ) . finally ( ( ) => { this . _setSettings ( ) . catch ( ( settingErr ) => { this . _logger . warn ( `Fail to register settings due error: ${ settingErr . message }` ) ; } ) . finally ( ( ) => { resolve ( ) ; } ) ; } ) ; } ) ; } public destroy ( ) { return new Promise ( ( resolve ) => { resolve ( ) ; } ) ; } public getName ( ) { return 'ServiceUpdate' ; } private _setSettings ( ) { return new Promise ( ( resolve , reject ) => { sequences ( [ ServiceSettings . register . bind ( ServiceSettings , new Entry ( { key : 'general' , name : 'General' , desc : 'General setting of chipmunk' , path : '' , type : ESettingType . standard } ) ) , ServiceSettings . register . bind ( ServiceSettings , new Entry ( { key : 'updates' , name : 'Updates' , desc : 'Configure application update workflow' , path : 'general' , type : ESettingType . standard } ) ) , ] ) . then ( ( ) => { Promise . all ( [ ServiceSettings . register ( this . _settings . updates ) . catch ( ( regErr ) => { this . _logger . error ( regErr . message ) ; } ) , ] ) . then ( ( ) => { resolve ( ) ; } ) . catch ( ( error ) => { reject ( error ) ; } ) ; } ) . catch ( ( structErr ) => { reject ( structErr ) ; } ) ; } ) ; } private _check ( ) { if ( ! this . _settings . updates . get ( ) ) { return ; } if ( ! ServiceProduction . isProduction ( ) ) { this . _logger . debug ( `Updates would not check becuase developing mode is ON` ) ; return ; } GitHubClient . getReleases ( { repo : CSettings . repo } ) . then ( ( releases ) => { const current = ServicePackage . get ( ) . version ; if ( typeof current !== 'string' || current . trim ( ) === '' ) { return this . _logger . warn ( `Fail to detect version of current app.` ) ; } let latest = current ; let info ; releases . forEach ( ( release ) => { if ( this . _isVersionNewer ( latest , release . name ) ) { latest = release . name ; info = release ; } } ) ; if ( info === undefined ) { this . _logger . debug ( `Current version "${ current }" is newest, no update needed.` ) ; return ; } this . _logger . debug ( `New version is released...: ${ info . name }` ) ; const targets = this . _getAssetFileName ( latest ) ; this . _logger . debug ( `Asset file names:...: ${ info . name }` ) ; if ( targets instanceof Error ) { return this . _logger . warn ( `Fail to get targets due error: ${ targets . message }` ) ; } let compressedFile ; info . assets . forEach ( ( asset ) => { if ( targets . indexOf ( asset . name ) !== - 1 ) { compressedFile = asset . name ; this . _logger . debug ( `package to download:...: ${ compressedFile }` ) ; } } ) ; if ( compressedFile === undefined ) { return this . _logger . warn ( `Fail to find archive-file with release for current platform.` ) ; } this . _target = latest ; const file = path . resolve ( ServicePaths . getDownloads ( ) , compressedFile ) ; if ( fs . existsSync ( file ) ) { this . _logger . debug ( `File was already downloaded "${ file }". latest: ${ latest }.` ) ; this . _tgzfile = file ; this . _notify ( latest ) ; } else { this . _logger . debug ( `Found new version "${ latest }". Starting downloading: ${ compressedFile }.` ) ; GitHubClient . download ( { repo : CSettings . repo , } , { version : latest , name : compressedFile , dest : ServicePaths . getDownloads ( ) , } ) . then ( ( _tgzfile ) => { this . _tgzfile = _tgzfile ; this . _notify ( latest ) ; this . _logger . debug ( `File ${ compressedFile } is downloaded into: ${ _tgzfile }.` ) ; } ) . catch ( ( downloadError ) => { this . _logger . error ( `Fail to download "${ compressedFile }" due error: ${ downloadError . message }` ) ; } ) ; } } ) . catch ( ( gettingReleasesError ) => { this . _logger . warn ( `Fail to get releases list due error: ${ gettingReleasesError . message }` ) ; } ) ; } private _notify ( version ) { ServiceElectron . IPC . send ( new ServiceElectron . IPCMessages . Notification ( { caption : `Update` , message : `New version of chipmunk "${ version }" is available.` , type : ServiceElectron . IPCMessages . Notification . Types . info , session : '*' , actions : [ { type : ServiceElectron . IPCMessages . ENotificationActionType . ipc , value : 'UpdateRequest' , caption : 'Update' , } , ] , } ) ) . catch ( ( error ) => { this . _logger . warn ( `Fail send Notification due error: ${ error . message }` ) ; } ) ; } private _getAssetFileName ( version ) { const platform = getPlatform ( ) ; if ( platform === EPlatforms . undefined ) { return new Error ( `Fail to detect supported platform for (${ os . platform ( ) }).` ) ; } return CReleaseNameAliases . map ( ( alias ) => { const pattern = CAssetFilePattern ; return pattern . replace ( CHooks . alias , alias ) . replace ( CHooks . version , version ) . replace ( CHooks . platform , platform ) ; } ) ; } private _versplit ( version ) { return version . split ( '.' ) . map ( ( part ) => { return parseInt ( part , 10 ) ; } ) . filter ( ( value ) => { return isNaN ( value ) ? false : isFinite ( value ) ; } ) ; } private _isVersionNewer ( current , target ) { const cParts = this . _versplit ( current ) ; const tParts = this . _versplit ( target ) ; if ( cParts . length !== 3 || tParts . length !== 3 ) { return false ; } const diff = cParts . map ( ( xxx , i ) => { return tParts [ i ] - xxx ; } ) ; if ( diff [ 0 ] > 0 ) { return true ; } if ( diff [ 0 ] === 0 && diff [ 1 ] > 0 ) { return true ; } if ( diff [ 0 ] === 0 && diff [ 1 ] === 0 && diff [ 2 ] > 0 ) { return true ; } return false ; } private _onRenderState ( message ) { if ( ( message as IPCMessages . RenderState ) . state !== IPCMessages . ERenderState . ready ) { return ; } this . _check ( ) ; } private _getLauncherFile ( ) { return new Promise ( ( resolve , reject ) => { const updater = path . resolve ( ServicePaths . getRoot ( ) , `apps/${ os . platform ( ) === 'win32' ? 'updater.exe' : 'updater' }` ) ; if ( ! fs . existsSync ( updater ) ) { return reject ( new Error ( `Fail to find an updater in package "${ updater }".` ) ) ; } const existed = path . resolve ( ServicePaths . getApps ( ) , ( os . platform ( ) === 'win32' ? 'updater.exe' : 'updater' ) ) ; if ( fs . existsSync ( existed ) ) { try { this . _logger . debug ( `Found existed updater "${ existed }". It will be removed.` ) ; fs . unlinkSync ( existed ) ; } catch ( e ) { return reject ( e ) ; } } fs . copyFile ( updater , existed , ( error ) => { if ( error ) { return reject ( error ) ; } this . _logger . debug ( `Updater "${ existed }" is delivered.` ) ; resolve ( existed ) ; } ) ; } ) ; } private _onUpdateRequest ( message ) { if ( this . _tgzfile === undefined ) { return ; } this . _getLauncherFile ( ) . then ( ( updater ) => { this . _update ( updater ) ; } ) . catch ( ( gettingLauncherErr ) => { this . _logger . error ( `Fail to get updater due error: ${ gettingLauncherErr . message }` ) ; } ) ; } private _update ( updater ) { if ( this . _tgzfile === undefined || this . _app === undefined ) { return ; } this . _app ?. destroy ( EExitCodes . update ) . catch ( ( error ) => { this . _logger . warn ( `Fail destroy app due error: ${ error . message }` ) ; } ) ; } } export default ( new ServiceUpdate ( ) ) ;