@@ -5,6 +5,7 @@ import * as crypto from 'crypto';
55import * as linefeeder from '../../net/linefeeder' ;
66import * as logging from '../../logging/logging' ;
77import * as queue from '../../handler/queue' ;
8+ import Pinger from '../../net/pinger' ;
89
910// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
1011import * as ssh2 from 'ssh2' ;
@@ -121,18 +122,6 @@ function makeInstanceMessage(address:string, description?:string): any {
121122 } ;
122123}
123124
124- // To see how these fields are handled, see
125- // generic_core/social.ts#handleClient in the uProxy repo.
126- function makeClientState ( address : string ) : freedom . Social . ClientState {
127- return {
128- userId : address ,
129- clientId : address ,
130- // https://github.com/freedomjs/freedom/blob/master/interface/social.json
131- status : 'ONLINE' ,
132- timestamp : Date . now ( )
133- } ;
134- }
135-
136125// To see how these fields are handled, see
137126// generic_core/social.ts#handleUserProfile in the uProxy repo. We omit
138127// the status field since remote-user.ts#update will use FRIEND as a default.
@@ -179,6 +168,15 @@ export class CloudSocialProvider {
179168 // SSH connections, keyed by host.
180169 private clients_ : { [ host : string ] : Promise < Connection > } = { } ;
181170
171+ // Map from host to whether it is online. Hosts not in the map are assumed
172+ // to be offline.
173+ private onlineHosts_ : { [ host : string ] : boolean } = { } ;
174+
175+ // Map from host to intervalId used for monitoring online presence.
176+ private onlinePresenceMonitorIds_ : { [ host : string ] : NodeJS . Timer } = { } ;
177+
178+ private static PING_INTERVAL_ = 60000 ;
179+
182180 constructor ( private dispatchEvent_ : ( name : string , args : Object ) => void ) { }
183181
184182 // Emits the messages necessary to make the user appear online
@@ -187,16 +185,18 @@ export class CloudSocialProvider {
187185 this . dispatchEvent_ ( 'onUserProfile' , makeUserProfile (
188186 contact . invite . host , contact . invite . isAdmin ) ) ;
189187
190- var clientState = makeClientState ( contact . invite . host ) ;
188+ var clientState = this . makeClientState_ ( contact . invite . host ) ;
191189 this . dispatchEvent_ ( 'onClientState' , clientState ) ;
192190
193- // Pretend that we received a message from a remote uProxy client.
194- this . dispatchEvent_ ( 'onMessage' , {
195- from : clientState ,
196- // INSTANCE
197- message : JSON . stringify ( makeVersionedPeerMessage ( 3000 , makeInstanceMessage (
198- contact . invite . host , contact . description ) , contact . version ) )
199- } ) ;
191+ if ( this . isOnline_ ( contact . invite . host ) ) {
192+ // Pretend that we received a message from a remote uProxy client.
193+ this . dispatchEvent_ ( 'onMessage' , {
194+ from : clientState ,
195+ // INSTANCE
196+ message : JSON . stringify ( makeVersionedPeerMessage ( 3000 , makeInstanceMessage (
197+ contact . invite . host , contact . description ) , contact . version ) )
198+ } ) ;
199+ }
200200 }
201201
202202 // Establishes an SSH connection to a server, first shutting down
@@ -213,8 +213,10 @@ export class CloudSocialProvider {
213213 }
214214
215215 const connection = new Connection ( invite , ( message : Object ) => {
216+ // Set the server to online, since we are receiving messages from them.
217+ this . setOnlineStatus_ ( invite . host , true ) ;
216218 this . dispatchEvent_ ( 'onMessage' , {
217- from : makeClientState ( invite . host ) ,
219+ from : this . makeClientState_ ( invite . host ) ,
218220 // SIGNAL_FROM_SERVER_PEER,
219221 message : JSON . stringify ( makeVersionedPeerMessage ( 3002 ,
220222 message , connection . getVersion ( ) ) )
@@ -224,6 +226,9 @@ export class CloudSocialProvider {
224226 this . clients_ [ invite . host ] = connection . connect ( ) . then ( ( ) => {
225227 log . info ( 'connected to zork on %1' , invite . host ) ;
226228
229+ // Cloud server is online if a connection has succeeded.
230+ this . setOnlineStatus_ ( invite . host , true ) ;
231+
227232 // Fetch the banner, if available, then emit an instance message.
228233 connection . getBanner ( ) . then ( ( banner : string ) => {
229234 if ( banner . length < 1 ) {
@@ -265,6 +270,7 @@ export class CloudSocialProvider {
265270 if ( savedContacts . contacts ) {
266271 for ( let contact of savedContacts . contacts ) {
267272 this . savedContacts_ [ contact . invite . host ] = contact ;
273+ this . startMonitoringPresence_ ( contact . invite . host ) ;
268274 this . notifyOfUser_ ( contact ) ;
269275 }
270276 }
@@ -298,7 +304,7 @@ export class CloudSocialProvider {
298304 // TODO: emit an onUserProfile event, which can include an image URL
299305 // TODO: base this on the user's public key?
300306 // (shown in the "connected accounts" page)
301- return Promise . resolve ( makeClientState ( 'me' ) ) ;
307+ return Promise . resolve ( this . makeClientState_ ( 'me' ) ) ;
302308 }
303309
304310 public sendMessage = ( destinationClientId : string , message : string ) : Promise < void > => {
@@ -410,15 +416,9 @@ export class CloudSocialProvider {
410416 this . savedContacts_ [ invite . host ] = {
411417 invite : invite
412418 } ;
419+ this . startMonitoringPresence_ ( invite . host ) ;
413420 this . notifyOfUser_ ( this . savedContacts_ [ invite . host ] ) ;
414421 this . saveContacts_ ( ) ;
415-
416- // Connect in the background in order to fetch metadata such as
417- // the banner (description).
418- this . reconnect_ ( invite ) . catch ( ( e : Error ) => {
419- log . warn ( 'failed to log into cloud server during invite accept: %1' , e . message ) ;
420- } ) ;
421-
422422 return Promise . resolve ( ) ;
423423 } catch ( e ) {
424424 return Promise . reject ( new Error ( 'could not parse invite code: ' + e . message ) ) ;
@@ -439,12 +439,84 @@ export class CloudSocialProvider {
439439 log . warn ( 'cloud contact %1 is not in %2 - cannot remove from storage' , host , STORAGE_KEY ) ;
440440 return Promise . resolve ( ) ;
441441 }
442+ this . stopMonitoringPresence_ ( host ) ;
442443 // Remove host from savedContacts and clients
443444 delete this . savedContacts_ [ host ] ;
444445 delete this . clients_ [ host ] ;
445446 // Update storage with this.savedContacts_
446447 return this . saveContacts_ ( ) ;
447448 }
449+
450+ private startMonitoringPresence_ = ( host : string ) => {
451+ if ( this . onlinePresenceMonitorIds_ [ host ] ) {
452+ log . error ( 'unexpected call to startMonitoringPresence_ for ' + host ) ;
453+ return ;
454+ }
455+ // Ping server every minute to see if it is online. A server is considered
456+ // online if a connection can be established with the SSH port. We stop
457+ // pinging for presence once the host is online, so as to not give away
458+ // that we are pinging uProxy cloud servers with a regular heartbeat.
459+ const ping = ( ) : Promise < boolean > => {
460+ var pinger = new Pinger ( host , SSH_SERVER_PORT ) ;
461+ return pinger . pingOnce ( ) . then ( ( ) => {
462+ return true ;
463+ } ) . catch ( ( ) => {
464+ return false ;
465+ } ) . then ( ( newOnlineValue : boolean ) => {
466+ var oldOnlineValue = this . isOnline_ ( host ) ;
467+ this . setOnlineStatus_ ( host , newOnlineValue ) ;
468+ if ( newOnlineValue !== oldOnlineValue ) {
469+ // status changed, emit a new onClientState.
470+ this . notifyOfUser_ ( this . savedContacts_ [ host ] ) ;
471+ if ( newOnlineValue ) {
472+ // Connect in the background in order to fetch metadata such as
473+ // the banner (description).
474+ const invite = this . savedContacts_ [ host ] . invite ;
475+ this . reconnect_ ( invite ) . catch ( ( e : Error ) => {
476+ log . error ( 'failed to log into cloud server once online: %1' , e . message ) ;
477+ } ) ;
478+ }
479+ }
480+ } ) ;
481+ }
482+ this . onlinePresenceMonitorIds_ [ host ] = setInterval ( ping , CloudSocialProvider . PING_INTERVAL_ ) ;
483+ // Ping server immediately (so we don't have to wait 1 min for 1st result).
484+ ping ( ) ;
485+ }
486+
487+ private stopMonitoringPresence_ = ( host : string ) => {
488+ if ( ! this . onlinePresenceMonitorIds_ [ host ] ) {
489+ // We may have already stopped monitoring presence, e.g. because the
490+ // host has come online.
491+ return ;
492+ }
493+ clearInterval ( this . onlinePresenceMonitorIds_ [ host ] ) ;
494+ delete this . onlinePresenceMonitorIds_ [ host ] ;
495+ }
496+
497+ private isOnline_ = ( host : string ) => {
498+ return host === 'me' || this . onlineHosts_ [ host ] === true ;
499+ }
500+
501+ private setOnlineStatus_ = ( host : string , isOnline : boolean ) => {
502+ this . onlineHosts_ [ host ] = isOnline ;
503+ if ( isOnline ) {
504+ // Stop monitoring presence once the client is online.
505+ this . stopMonitoringPresence_ ( host ) ;
506+ }
507+ }
508+
509+ // To see how these fields are handled, see
510+ // generic_core/social.ts#handleClient in the uProxy repo.
511+ private makeClientState_ = ( address : string ) : freedom . Social . ClientState => {
512+ return {
513+ userId : address ,
514+ clientId : address ,
515+ // https://github.com/freedomjs/freedom/blob/master/interface/social.json
516+ status : this . isOnline_ ( address ) ? 'ONLINE' : 'OFFLINE' ,
517+ timestamp : Date . now ( )
518+ } ;
519+ }
448520}
449521
450522enum ConnectionState {
0 commit comments