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.2KB

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