import { Injectable , NgZone } from '@angular/core' ; import { AppVersion } from '@ionic-native/app-version' ; import { Device } from '@ionic-native/device' ; import { NetworkInterface } from '@ionic-native/network-interface' ; import { Zeroconf } from '@ionic-native/zeroconf' ; import { Alert , AlertController , Events , Platform } from 'ionic-angular' ; import * as ipUtils from 'ip-utils' ; import { Observable , Subject } from 'rxjs' ; import { SemVer } from 'semver' ; import { discoveryResultModel } from '../models/discovery-result' ; import { requestModel , requestModelDeleteScanSessions , requestModelGetVersion , requestModelHelo , requestModelPing , requestModelPutScanSessions } from '../models/request.model' ; import { responseModel , responseModelEnableQuantity , responseModelHelo , responseModelKick , responseModelPopup , responseModelUpdateSettings } from '../models/response.model' ; import { wsEvent } from '../models/ws-event.model' ; import { HelpPage } from '../pages/help/help' ; import { Settings } from '../providers/settings' ; import { ServerModel } from './../models/server.model' ; import { Config } from './config' ; import { LastToastProvider } from './last-toast/last-toast' ; import { ScanSessionsStorage } from './scan-sessions-storage' ; import { Utils } from './utils' ; @ Injectable ( ) export class ServerProvider { public static RECONNECT_INTERVAL = 7000 ; public static EVENT_CODE_CLOSE_NORMAL = 1000 ; public static EVENT_CODE_DO_NOT_ATTEMP_RECCONECTION = 4000 ; public serverVersion : SemVer = null ; private connected = false ; private webSocket : WebSocket ; private _onMessage = new Subject < responseModel > ( ) ; private _onConnect = new Subject < ServerModel > ( ) ; private _onDisconnect = new Subject < any > ( ) ; private wsEventObservable = new Subject < wsEvent > ( ) ; private watchForServersSubject : Subject < discoveryResultModel > ; private reconnecting = false ; private everConnected = false ; private connectionProblemAlert = false ; private serverQueue : ServerModel [ ] = [ ] ; private reconnectInterval = null ; private heartBeatInterval = null ; private pongTimeout = null ; private fallBackTimeout = null ; private popup : Alert = null ; private kickedOut = false ; private lastOnResumeSubscription = null ; constructor ( private settings , private ngZone , private lastToast , private zeroconf , private alertCtrl , public platform , public device , private networkInterface , public events , private appVersion , private scanSessionsStorage , private utils ) { } onMessage ( ) { return this . _onMessage . asObservable ( ) ; } onWsEvent ( ) { return this . wsEventObservable ; } onConnect ( ) { return this . _onConnect . asObservable ( ) ; } onDisconnect ( ) { return this . _onDisconnect . asObservable ( ) ; } async connect ( server , skipQueue = false ) { console . log ( '[S-c] connect(server=' , server , 'skipQueue=' , skipQueue ) ; if ( ! this . webSocket || this . webSocket . readyState != WebSocket . OPEN || this . webSocket . url . indexOf ( server . address ) == - 1 ) { console . log ( '[S-c]: not connected or new server selected, creating a new WS connection...' ) ; await this . wsConnect ( server , skipQueue ) ; } else if ( this . webSocket . readyState == WebSocket . OPEN ) { console . log ( '[S-c]: already connected to a server, no action taken' ) ; this . serverQueue = [ ] ; this . connected = true ; this . wsEventObservable . next ( { name : wsEvent . EVENT_ALREADY_OPEN , ws : this . webSocket } ) ; } } async disconnect ( ) { if ( this . heartBeatInterval ) clearTimeout ( this . heartBeatInterval ) ; if ( this . pongTimeout ) clearTimeout ( this . pongTimeout ) ; await this . wsDisconnect ( false ) ; } isConnected ( ) { return this . connected ; } isReconnecting ( ) { return this . reconnecting ; } private async wsDisconnect ( reconnect = false ) { console . log ( '[S-wd]: wsDisconnect(reconnect=' + reconnect + ')' , this . webSocket ) ; if ( this . webSocket ) { if ( this . everConnected && ! this . reconnecting ) { this . lastToast . present ( await this . utils . text ( 'connectionLostToastTitle' ) ) ; this . connected = false ; this . wsEventObservable . next ( { name : wsEvent . EVENT_ERROR , ws : this . webSocket } ) ; this . _onDisconnect . next ( ) ; } let code = reconnect ? ServerProvider . EVENT_CODE_CLOSE_NORMAL : ServerProvider . EVENT_CODE_DO_NOT_ATTEMP_RECCONECTION ; this . webSocket . close ( code ) ; this . webSocket . onmessage = null ; this . webSocket . onopen = null ; this . webSocket . onerror = null ; this . webSocket . onclose = null ; this . webSocket = null ; } } private isTransitioningState ( ) { return this . webSocket && ( this . webSocket . readyState == WebSocket . CLOSING || this . webSocket . readyState == WebSocket . CONNECTING ) ; } private async wsConnect ( server , skipQueue = false ) { console . log ( '[S-wc]: wsConnect(' + server . address + ')' , new Date ( ) ) ; if ( skipQueue ) { console . log ( '[S-wc]: WS: skipQueue is true, skipping the queue and disconnecting from the old one' ) ; this . serverQueue = [ ] ; this . serverQueue . push ( server ) ; this . reconnecting = true ; } else if ( this . isTransitioningState ( ) ) { if ( ! this . serverQueue . find ( x => x . equals ( server ) ) ) { this . serverQueue . push ( server ) ; } else { } setTimeout ( async ( ) => { if ( this . isTransitioningState ( ) ) { await this . wsDisconnect ( ) ; this . webSocket = null ; } } , 5000 ) ; return ; } await this . wsDisconnect ( ) ; let wsUrl = 'ws://' + server . address + ':' + Config . SERVER_PORT + '/' ; this . webSocket = new WebSocket ( wsUrl ) ; this . webSocket . onmessage = async ( message ) => { let messageData = null ; if ( message . data ) { messageData = JSON . parse ( message . data ) ; } console . log ( 'responseModel action = ' , messageData . action ) ; if ( messageData . action == responseModel . ACTION_HELO ) { if ( this . fallBackTimeout ) clearTimeout ( this . fallBackTimeout ) ; let heloResponse = messageData ; this . appVersion . getVersionNumber ( ) . then ( appVersionString => { let appVersion = new SemVer ( appVersionString ) ; let serverVersion = new SemVer ( heloResponse . version ) ; this . serverVersion = serverVersion ; if ( appVersion . major != serverVersion . major ) { this . showVersionMismatch ( ) ; } } ) ; if ( heloResponse . outputProfiles ) { this . settings . setOutputProfiles ( heloResponse . outputProfiles ) ; } else { this . settings . setQuantityEnabled ( heloResponse . quantityEnabled ) ; } let deletedIds = await this . settings . getUnsyncedDeletedScanSesions ( heloResponse . serverUUID ) ; if ( deletedIds ) { let wsDeleteRequest = new requestModelDeleteScanSessions ( ) . fromObject ( { scanSessionIds : deletedIds } ) ; this . send ( wsDeleteRequest ) ; this . settings . setUnsyncedDeletedScanSesions ( heloResponse . serverUUID , [ ] ) ; } let restoredIds = await this . settings . getUnsyncedRestoredScanSesions ( ) ; if ( restoredIds ) { let wsRestoreRequest = new requestModelPutScanSessions ( ) . fromObject ( { scanSessions : ( await this . scanSessionsStorage . getScanSessions ( ) ) . filter ( x => restoredIds . indexOf ( x . id ) != - 1 ) , sendKeystrokes : false , deviceId : this . device . uuid , } ) ; this . send ( wsRestoreRequest ) ; this . settings . setUnsyncedRestoredScanSesions ( [ ] ) ; } } else if ( messageData . action == responseModel . ACTION_PONG ) { if ( this . pongTimeout ) clearTimeout ( this . pongTimeout ) ; } else if ( messageData . action == responseModel . ACTION_POPUP ) { let responseModelPopup = messageData ; if ( this . popup ) { this . popup . dismiss ( ) ; } this . popup = this . alertCtrl . create ( { title : responseModelPopup . title , message : responseModelPopup . message , buttons : [ await this . utils . text ( 'responseModalDialogOkButton' ) ] } ) ; this . popup . present ( ) ; } else if ( messageData . action == responseModel . ACTION_UPDATE_SETTINGS ) { let responseModelUpdateSettings = messageData ; await this . settings . setOutputProfiles ( responseModelUpdateSettings . outputProfiles ) ; this . events . publish ( responseModel . ACTION_UPDATE_SETTINGS , responseModelUpdateSettings ) ; } else if ( messageData . action == responseModel . ACTION_GET_VERSION ) { console . log ( 'FallBack: old getVersion received, showing version mismatch' ) ; this . showVersionMismatch ( ) ; } else if ( messageData . action == responseModel . ACTION_ENABLE_QUANTITY ) { let responseModelEnableQuantity = messageData ; this . settings . setQuantityEnabled ( responseModelEnableQuantity . enable ) ; } else if ( messageData . action == responseModel . ACTION_KICK ) { let responseModelKick = messageData ; this . kickedOut = true ; if ( responseModelKick . message != '' ) { this . alertCtrl . create ( { title : await this . utils . text ( 'responseModelKickDialogTitle' ) , message : responseModelKick . message , buttons : [ { text : await this . utils . text ( 'responseModelKickDialogCloseButton' ) , role : 'cancel' } ] } ) . present ( ) ; } } this . ngZone . run ( ( ) => { this . _onMessage . next ( messageData ) ; } ) ; } ; this . webSocket . onopen = async ( ) => { this . connectionProblemAlert = false ; this . everConnected = true ; this . settings . setEverConnected ( true ) ; this . serverQueue = [ ] ; if ( this . pongTimeout ) clearTimeout ( this . pongTimeout ) ; console . log ( "[S-wc]: WS: reconnected successfully with " + server . address ) ; this . clearReconnectInterval ( ) ; this . settings . saveServer ( server ) ; this . connected = true ; this . wsEventObservable . next ( { name : 'open' , ws : this . webSocket } ) ; this . _onConnect . next ( server ) ; this . lastToast . present ( await this . utils . text ( 'connectionEstablishedToastTitle' , { "serverName" : server . name } ) ) ; if ( this . heartBeatInterval ) clearInterval ( this . heartBeatInterval ) ; this . heartBeatInterval = setInterval ( ( ) => { let request = new requestModelPing ( ) ; this . send ( request ) ; if ( this . pongTimeout ) clearTimeout ( this . pongTimeout ) ; this . pongTimeout = setTimeout ( async ( ) => { console . log ( '[S-wc]: WS pong not received, closing connection...' ) ; await this . wsDisconnect ( false ) ; await this . scheduleNewWsConnection ( server ) ; } , 1000 * 5 ) ; } , 1000 * 60 ) ; /** Since we are inside onopen it means that we're connected to a server
             * and we can try to reconnect to it up until another onopen event will
             * occour.
             * When the next onopen will occour it'll use a new 'server' variable, and
             * it'll try to reconnect to it on every resume event */ if ( this . lastOnResumeSubscription != null ) { this . lastOnResumeSubscription . unsubscribe ( ) ; this . lastOnResumeSubscription = null ; } this . lastOnResumeSubscription = this . platform . resume . subscribe ( next => { console . log ( 'resume()' ) ; if ( ! this . connected ) { console . log ( 'onResume: not connected -> scheduling new connection immediately' ) ; this . scheduleNewWsConnection ( server ) ; } } ) ; this . settings . getDeviceName ( ) . then ( async ( deviceName ) => { console . log ( '[S-wc] sending HELO to ' + server . address ) ; let request = new requestModelHelo ( ) . fromObject ( { version : await this . appVersion . getVersionNumber ( ) , deviceName : deviceName , deviceId : this . device . uuid , } ) ; this . send ( request ) ; if ( this . fallBackTimeout ) clearTimeout ( this . fallBackTimeout ) ; this . fallBackTimeout = setTimeout ( ( ) => { let request = new requestModelGetVersion ( ) . fromObject ( { } ) ; this . send ( request ) ; } , 5000 ) ; } ) ; } ; this . webSocket . onerror = async ( err ) => { console . log ( '[S]: WS: onerror ' ) ; if ( ! this . reconnecting ) { this . lastToast . present ( await this . utils . text ( 'unableToConnectToastTitle' ) ) ; } this . connected = false ; this . wsEventObservable . next ( { name : wsEvent . EVENT_ERROR , ws : this . webSocket } ) ; this . _onDisconnect . next ( ) ; await this . scheduleNewWsConnection ( server ) ; } ; this . webSocket . onclose = async ( ev ) => { console . log ( '[S]: onclose' ) ; if ( this . everConnected && ! this . reconnecting ) { this . lastToast . present ( await this . utils . text ( 'connectionClosedToastTitle' ) ) ; } if ( ev . code != ServerProvider . EVENT_CODE_DO_NOT_ATTEMP_RECCONECTION ) { await this . scheduleNewWsConnection ( server ) ; } this . connected = false ; this . kickedOut = false ; this . wsEventObservable . next ( { name : wsEvent . EVENT_CLOSE , ws : this . webSocket } ) ; this . _onDisconnect . next ( ) ; } ; } async send ( request ) { if ( this . kickedOut ) { return ; } if ( this . webSocket ) { if ( this . webSocket . readyState == WebSocket . OPEN ) { this . webSocket . send ( JSON . stringify ( request ) ) ; } else if ( ! this . connectionProblemAlert ) { this . connectionProblemAlert = true ; this . alertCtrl . create ( { title : await this . utils . text ( 'connectionProblemDialogTitle' ) , message : await this . utils . text ( 'connectionProblemDialogMessage' ) , buttons : [ { text : await this . utils . text ( 'connectionProblemDialogCloseButton' ) , role : 'cancel' } , { text : await this . utils . text ( 'connectionProblemDialogHelpPageButton' ) , handler : ( ) => { this . events . publish ( 'setPage' , HelpPage ) ; } } ] } ) . present ( ) ; } } else { } } /**
     * Call stopWatchForServers when you don't need to receive updates anymore
     *
     * It is NOT required to call stopWatchForServers() before calling
     * watchForServers. It'll always start a brand new "watch" everytime it's
     * called.
     */ watchForServers ( ) { this . stopWatchForServers ( ) ; console . log ( "[S] watchForServers()" ) ; this . watchForServersSubject = new Subject < discoveryResultModel > ( ) ; if ( ! this . platform . is ( 'cordova' ) ) { let dummyServer = { server : new ServerModel ( '192.168.5.10' , 'Server 1' ) , action : 'added' } ; this . watchForServersSubject . next ( dummyServer ) ; setTimeout ( ( ) => { dummyServer = { server : new ServerModel ( '192.168.6.7' , 'Server 2' ) , action : 'added' } ; this . watchForServersSubject . next ( dummyServer ) ; } , 500 ) ; return ; } Promise . all ( [ this . zeroconf . reInit ( ) , this . networkInterface . getWiFiIPAddress ( ) , this . networkInterface . getCarrierIPAddress ( ) ] . map ( p => p . catch ( e => { } ) ) ) . then ( ( results ) => { let wifi = results [ 1 ] ; let otherInterfaces = results [ 2 ] ; let interfaces = [ ] ; if ( wifi ) interfaces . push ( wifi ) ; if ( otherInterfaces ) interfaces . push ( otherInterfaces ) ; this . zeroconf . watch ( '_http._tcp.' , 'local.' ) . subscribe ( zeroconfResult => { var action = zeroconfResult . action ; var service = zeroconfResult . service ; if ( service . port == Config . SERVER_PORT && service . ipv4Addresses && service . ipv4Addresses . length ) { console . log ( "[S-wfs] zeroconf ->" , zeroconfResult ) ; this . ngZone . run ( ( ) => { service . ipv4Addresses . forEach ( ipv4 => { if ( ipv4 && ipv4 . length ) { let isInSubnets = interfaces . map ( x => { return ipUtils . subnet ( x . ip + '/' + ipUtils . maskToCidr ( x . subnet ) ) . contains ( ipv4 ) ; } ) . findIndex ( x => x == true ) != - 1 ; if ( isInSubnets && this . watchForServersSubject != null ) { this . watchForServersSubject . next ( { server : new ServerModel ( ipv4 , service . hostname ) , action : action } ) ; } } } ) ; } ) ; } } ) ; } ) ; return this . watchForServersSubject . asObservable ( ) ; } async stopWatchForServers ( ) { console . error ( "[S] stopWatchForServers()" ) ; if ( this . watchForServersSubject ) { this . watchForServersSubject . complete ( ) ; this . watchForServersSubject = null ; } this . zeroconf . close ( ) ; } private clearReconnectInterval ( ) { if ( this . reconnectInterval ) { clearInterval ( this . reconnectInterval ) ; this . reconnectInterval = null ; this . reconnecting = false ; console . log ( "[S]: interval cleared." ) ; } } private async scheduleNewWsConnection ( server ) { console . log ( '[S-snwc]: scheduleNewWsConnection()->' ) ; this . reconnecting = true ; if ( this . pongTimeout ) clearTimeout ( this . pongTimeout ) ; if ( this . heartBeatInterval ) clearInterval ( this . heartBeatInterval ) ; if ( ! this . reconnectInterval ) { if ( this . serverQueue . length ) { console . log ( '[S-snwc]:    server queue is not empty, attemping a new reconnection whithout waiting' ) ; server = this . serverQueue . shift ( ) ; await this . wsConnect ( server ) ; } else { console . log ( '[S-snwc]:    server queue is empty, attemping a new reconnection to the same server in ' + ServerProvider . RECONNECT_INTERVAL + ' secs' ) ; this . reconnectInterval = setInterval ( async ( ) => { await this . wsConnect ( server ) ; } , ServerProvider . RECONNECT_INTERVAL ) ; } } } private isVersionMismatchDialogVisible = false ; private async showVersionMismatch ( ) { if ( ! this . isVersionMismatchDialogVisible ) { let dialog = this . alertCtrl . create ( { title : await this . utils . text ( 'showVersionMisMatchDialogTitle' ) , message : await this . utils . text ( 'showVersionMisMatchDialogMessage' , { "websiteName" : Config . WEBSITE_NAME } ) , buttons : [ { text : await this . utils . text ( 'showVersionMisMatchDialogOkButton' ) , role : 'cancel' } ] } ) ; dialog . didLeave . subscribe ( ( ) => { this . isVersionMismatchDialogVisible = false ; } ) ; this . isVersionMismatchDialogVisible = true ; dialog . present ( ) ; } } }