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.ts 7.9KB

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