You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

strophe.ping.js 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { getLogger } from 'jitsi-meet-logger';
  2. import { $iq, Strophe } from 'strophe.js';
  3. import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
  4. import ConnectionPlugin from './ConnectionPlugin';
  5. const logger = getLogger(__filename);
  6. /**
  7. * Default ping every 10 sec
  8. */
  9. const PING_DEFAULT_INTERVAL = 10000;
  10. /**
  11. * Default ping timeout error after 5 sec of waiting.
  12. */
  13. const PING_DEFAULT_TIMEOUT = 5000;
  14. /**
  15. * Default value for how many ping failures will be tolerated before the WebSocket connection is killed.
  16. * The worst case scenario in case of ping timing out without a response is (25 seconds at the time of this writing):
  17. * PING_THRESHOLD * PING_INTERVAL + PING_TIMEOUT
  18. */
  19. const PING_DEFAULT_THRESHOLD = 2;
  20. /**
  21. * XEP-0199 ping plugin.
  22. *
  23. * Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
  24. */
  25. export default class PingConnectionPlugin extends ConnectionPlugin {
  26. /**
  27. * Constructs new object
  28. * @param {Object} options
  29. * @param {Function} options.onPingThresholdExceeded - Callback called when ping fails too many times (controlled
  30. * by the {@link PING_THRESHOLD} constant).
  31. * @param {Function} options._getTimeSinceLastServerResponse - A function to obtain the last seen
  32. * response from the server.
  33. * @param {Object} options.pingOptions - The ping options if any.
  34. * @constructor
  35. */
  36. constructor({ getTimeSinceLastServerResponse, onPingThresholdExceeded, pingOptions = {} }) {
  37. super();
  38. this.failedPings = 0;
  39. this._onPingThresholdExceeded = onPingThresholdExceeded;
  40. this._getTimeSinceLastServerResponse = getTimeSinceLastServerResponse;
  41. this.pingInterval = typeof pingOptions.interval === 'number' ? pingOptions.interval : PING_DEFAULT_INTERVAL;
  42. this.pingTimeout = typeof pingOptions.timeout === 'number' ? pingOptions.timeout : PING_DEFAULT_TIMEOUT;
  43. this.pingThreshold = typeof pingOptions.threshold === 'number'
  44. ? pingOptions.threshold : PING_DEFAULT_THRESHOLD;
  45. // The number of timestamps of send pings to keep.
  46. // The current value is 2 minutes.
  47. this.pingTimestampsToKeep = Math.round(120000 / this.pingInterval);
  48. this.pingExecIntervals = new Array(this.pingTimestampsToKeep);
  49. }
  50. /**
  51. * Initializes the plugin. Method called by Strophe.
  52. * @param connection Strophe connection instance.
  53. */
  54. init(connection) {
  55. super.init(connection);
  56. Strophe.addNamespace('PING', 'urn:xmpp:ping');
  57. }
  58. /* eslint-disable max-params */
  59. /**
  60. * Sends "ping" to given <tt>jid</tt>
  61. * @param jid the JID to which ping request will be sent.
  62. * @param success callback called on success.
  63. * @param error callback called on error.
  64. * @param timeout ms how long are we going to wait for the response. On
  65. * timeout <tt>error<//t> callback is called with undefined error argument.
  66. */
  67. ping(jid, success, error, timeout) {
  68. this._addPingExecutionTimestamp();
  69. const iq = $iq({
  70. type: 'get',
  71. to: jid
  72. });
  73. iq.c('ping', { xmlns: Strophe.NS.PING });
  74. this.connection.sendIQ2(iq, { timeout })
  75. .then(success, error);
  76. }
  77. /* eslint-enable max-params */
  78. /**
  79. * Starts to send ping in given interval to specified remote JID.
  80. * This plugin supports only one such task and <tt>stopInterval</tt>
  81. * must be called before starting a new one.
  82. * @param remoteJid remote JID to which ping requests will be sent to.
  83. */
  84. startInterval(remoteJid) {
  85. clearInterval(this.intervalId);
  86. this.intervalId = window.setInterval(() => {
  87. // when there were some server responses in the interval since the last time we checked (_lastServerCheck)
  88. // let's skip the ping
  89. const now = Date.now();
  90. if (this._getTimeSinceLastServerResponse() < now - this._lastServerCheck) {
  91. // do this just to keep in sync the intervals so we can detect suspended device
  92. this._addPingExecutionTimestamp();
  93. this._lastServerCheck = now;
  94. this.failedPings = 0;
  95. return;
  96. }
  97. this.ping(remoteJid, () => {
  98. // server response is measured on raw input and ping response time is measured after all the xmpp
  99. // processing is done in js, so there can be some misalignment when we do the check above.
  100. // That's why we store the last time we got the response
  101. this._lastServerCheck = this._getTimeSinceLastServerResponse() + Date.now();
  102. this.failedPings = 0;
  103. }, error => {
  104. this.failedPings += 1;
  105. const errmsg = `Ping ${error ? 'error' : 'timeout'}`;
  106. if (this.failedPings >= this.pingThreshold) {
  107. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  108. logger.error(errmsg, error);
  109. this._onPingThresholdExceeded && this._onPingThresholdExceeded();
  110. } else {
  111. logger.warn(errmsg, error);
  112. }
  113. }, this.pingTimeout);
  114. }, this.pingInterval);
  115. logger.info(`XMPP pings will be sent every ${this.pingInterval} ms`);
  116. }
  117. /**
  118. * Stops current "ping" interval task.
  119. */
  120. stopInterval() {
  121. if (this.intervalId) {
  122. window.clearInterval(this.intervalId);
  123. this.intervalId = null;
  124. this.failedPings = 0;
  125. logger.info('Ping interval cleared');
  126. }
  127. }
  128. /**
  129. * Adds the current time to the array of send ping timestamps.
  130. * @private
  131. */
  132. _addPingExecutionTimestamp() {
  133. this.pingExecIntervals.push(new Date().getTime());
  134. // keep array length to PING_TIMESTAMPS_TO_KEEP
  135. if (this.pingExecIntervals.length > this.pingTimestampsToKeep) {
  136. this.pingExecIntervals.shift();
  137. }
  138. }
  139. /**
  140. * Returns the maximum time between the recent sent pings, if there is a
  141. * big value it means the computer was inactive for some time(suspended).
  142. * Checks the maximum gap between sending pings, considering and the
  143. * current time. Trying to detect computer inactivity (sleep).
  144. *
  145. * @returns {int} the time ping was suspended, if it was not 0 is returned.
  146. */
  147. getPingSuspendTime() {
  148. const pingIntervals = this.pingExecIntervals.slice();
  149. // we need current time, as if ping was sent now
  150. // if computer sleeps we will get correct interval after next
  151. // scheduled ping, bet we sometimes need that interval before waiting
  152. // for the next ping, on closing the connection on error.
  153. pingIntervals.push(new Date().getTime());
  154. let maxInterval = 0;
  155. let previousTS = pingIntervals[0];
  156. pingIntervals.forEach(e => {
  157. const currentInterval = e - previousTS;
  158. if (currentInterval > maxInterval) {
  159. maxInterval = currentInterval;
  160. }
  161. previousTS = e;
  162. });
  163. // remove the interval between the ping sent
  164. // this way in normal execution there is no suspend and the return
  165. // will be 0 or close to 0.
  166. maxInterval -= this.pingInterval;
  167. // make sure we do not return less than 0
  168. return Math.max(maxInterval, 0);
  169. }
  170. }