forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			408 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const PREF_APP_UPDATE_LASTUPDATETIME_FMT = "app.update.lastUpdateTime.%ID%";
 | 
						|
const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
 | 
						|
const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
 | 
						|
const PREF_APP_UPDATE_LOG = "app.update.log";
 | 
						|
 | 
						|
const CATEGORY_UPDATE_TIMER = "update-timer";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
  lazy,
 | 
						|
  "gLogEnabled",
 | 
						|
  PREF_APP_UPDATE_LOG,
 | 
						|
  false
 | 
						|
);
 | 
						|
 | 
						|
/**
 | 
						|
 *  Logs a string to the error console.
 | 
						|
 *  @param   string
 | 
						|
 *           The string to write to the error console.
 | 
						|
 *  @param   bool
 | 
						|
 *           Whether to log even if logging is disabled.
 | 
						|
 */
 | 
						|
function LOG(string, alwaysLog = false) {
 | 
						|
  if (alwaysLog || lazy.gLogEnabled) {
 | 
						|
    dump("*** UTM:SVC " + string + "\n");
 | 
						|
    Services.console.logStringMessage("UTM:SVC " + string);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 *  A manager for timers. Manages timers that fire over long periods of time
 | 
						|
 *  (e.g. days, weeks, months).
 | 
						|
 *  @constructor
 | 
						|
 */
 | 
						|
export function TimerManager() {
 | 
						|
  Services.obs.addObserver(this, "profile-before-change");
 | 
						|
}
 | 
						|
 | 
						|
TimerManager.prototype = {
 | 
						|
  /**
 | 
						|
   * nsINamed
 | 
						|
   */
 | 
						|
  name: "UpdateTimerManager",
 | 
						|
 | 
						|
  /**
 | 
						|
   * The Checker Timer
 | 
						|
   */
 | 
						|
  _timer: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * The Checker Timer minimum delay interval as specified by the
 | 
						|
   * app.update.timerMinimumDelay pref. If the app.update.timerMinimumDelay
 | 
						|
   * pref doesn't exist this will default to 120000.
 | 
						|
   */
 | 
						|
  _timerMinimumDelay: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * The set of registered timers.
 | 
						|
   */
 | 
						|
  _timers: {},
 | 
						|
 | 
						|
  /**
 | 
						|
   * See nsIObserver.idl
 | 
						|
   */
 | 
						|
  observe: function TM_observe(aSubject, aTopic, aData) {
 | 
						|
    // Prevent setting the timer interval to a value of less than 30 seconds.
 | 
						|
    var minInterval = 30000;
 | 
						|
    // Prevent setting the first timer interval to a value of less than 10
 | 
						|
    // seconds.
 | 
						|
    var minFirstInterval = 10000;
 | 
						|
    switch (aTopic) {
 | 
						|
      case "utm-test-init":
 | 
						|
        // Enforce a minimum timer interval of 500 ms for tests and fall through
 | 
						|
        // to profile-after-change to initialize the timer.
 | 
						|
        minInterval = 500;
 | 
						|
        minFirstInterval = 500;
 | 
						|
      // fall through
 | 
						|
      case "profile-after-change":
 | 
						|
        this._timerMinimumDelay = Math.max(
 | 
						|
          1000 *
 | 
						|
            Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERMINIMUMDELAY, 120),
 | 
						|
          minInterval
 | 
						|
        );
 | 
						|
        // Prevent the timer delay between notifications to other consumers from
 | 
						|
        // being greater than 5 minutes which is 300000 milliseconds.
 | 
						|
        this._timerMinimumDelay = Math.min(this._timerMinimumDelay, 300000);
 | 
						|
        // Prevent the first interval from being less than the value of minFirstInterval
 | 
						|
        let firstInterval = Math.max(
 | 
						|
          Services.prefs.getIntPref(PREF_APP_UPDATE_TIMERFIRSTINTERVAL, 30000),
 | 
						|
          minFirstInterval
 | 
						|
        );
 | 
						|
        // Prevent the first interval from being greater than 2 minutes which is
 | 
						|
        // 120000 milliseconds.
 | 
						|
        firstInterval = Math.min(firstInterval, 120000);
 | 
						|
        // Cancel the timer if it has already been initialized. This is primarily
 | 
						|
        // for tests.
 | 
						|
        this._canEnsureTimer = true;
 | 
						|
        this._ensureTimer(firstInterval);
 | 
						|
        break;
 | 
						|
      case "profile-before-change":
 | 
						|
        Services.obs.removeObserver(this, "profile-before-change");
 | 
						|
 | 
						|
        // Release everything we hold onto.
 | 
						|
        this._cancelTimer();
 | 
						|
        for (var timerID in this._timers) {
 | 
						|
          delete this._timers[timerID];
 | 
						|
        }
 | 
						|
        this._timers = null;
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Called when the checking timer fires.
 | 
						|
   *
 | 
						|
   * We only fire one notification each time, so that the operations are
 | 
						|
   * staggered. We don't want too many to happen at once, which could
 | 
						|
   * negatively impact responsiveness.
 | 
						|
   *
 | 
						|
   * @param   timer
 | 
						|
   *          The checking timer that fired.
 | 
						|
   */
 | 
						|
  notify: function TM_notify(timer) {
 | 
						|
    var nextDelay = null;
 | 
						|
    function updateNextDelay(delay) {
 | 
						|
      if (nextDelay === null || delay < nextDelay) {
 | 
						|
        nextDelay = delay;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Each timer calls tryFire(), which figures out which is the one that
 | 
						|
    // wanted to be called earliest. That one will be fired; the others are
 | 
						|
    // skipped and will be done later.
 | 
						|
    var now = Math.round(Date.now() / 1000);
 | 
						|
 | 
						|
    var callbacksToFire = [];
 | 
						|
    function tryFire(timerID, callback, intendedTime) {
 | 
						|
      if (intendedTime <= now) {
 | 
						|
        callbacksToFire.push({ timerID, callback, intendedTime });
 | 
						|
      } else {
 | 
						|
        updateNextDelay(intendedTime - now);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (let { value } of Services.catMan.enumerateCategory(
 | 
						|
      CATEGORY_UPDATE_TIMER
 | 
						|
    )) {
 | 
						|
      let [cid, method, timerID, prefInterval, defaultInterval, maxInterval] =
 | 
						|
        value.split(",");
 | 
						|
 | 
						|
      defaultInterval = parseInt(defaultInterval);
 | 
						|
      // cid and method are validated below when calling notify.
 | 
						|
      if (!timerID || !defaultInterval || isNaN(defaultInterval)) {
 | 
						|
        LOG(
 | 
						|
          "TimerManager:notify - update-timer category registered" +
 | 
						|
            (cid ? " for " + cid : "") +
 | 
						|
            " without required parameters - " +
 | 
						|
            "skipping"
 | 
						|
        );
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      let interval = Services.prefs.getIntPref(prefInterval, defaultInterval);
 | 
						|
      // Allow the update-timer category to specify a maximum value to prevent
 | 
						|
      // values larger than desired.
 | 
						|
      maxInterval = parseInt(maxInterval);
 | 
						|
      if (maxInterval && !isNaN(maxInterval)) {
 | 
						|
        interval = Math.min(interval, maxInterval);
 | 
						|
      }
 | 
						|
      let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
 | 
						|
        /%ID%/,
 | 
						|
        timerID
 | 
						|
      );
 | 
						|
      // Initialize the last update time to 0 when the preference isn't set so
 | 
						|
      // the timer will be notified soon after a new profile's first use.
 | 
						|
      let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0);
 | 
						|
 | 
						|
      // If the last update time is greater than the current time then reset
 | 
						|
      // it to 0 and the timer manager will correct the value when it fires
 | 
						|
      // next for this consumer.
 | 
						|
      if (lastUpdateTime > now) {
 | 
						|
        lastUpdateTime = 0;
 | 
						|
        Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
 | 
						|
      }
 | 
						|
 | 
						|
      tryFire(
 | 
						|
        timerID,
 | 
						|
        function () {
 | 
						|
          ChromeUtils.idleDispatch(() => {
 | 
						|
            try {
 | 
						|
              let startTime = Cu.now();
 | 
						|
              Cc[cid][method](Ci.nsITimerCallback).notify(timer);
 | 
						|
              ChromeUtils.addProfilerMarker(
 | 
						|
                "UpdateTimer",
 | 
						|
                { category: "Timer", startTime },
 | 
						|
                timerID
 | 
						|
              );
 | 
						|
              LOG("TimerManager:notify - notified " + cid);
 | 
						|
            } catch (e) {
 | 
						|
              LOG(
 | 
						|
                "TimerManager:notify - error notifying component id: " +
 | 
						|
                  cid +
 | 
						|
                  " ,error: " +
 | 
						|
                  e
 | 
						|
              );
 | 
						|
            }
 | 
						|
          });
 | 
						|
          Services.prefs.setIntPref(prefLastUpdate, now);
 | 
						|
          updateNextDelay(interval);
 | 
						|
        },
 | 
						|
        lastUpdateTime + interval
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    for (let _timerID in this._timers) {
 | 
						|
      let timerID = _timerID; // necessary for the closure to work properly
 | 
						|
      let timerData = this._timers[timerID];
 | 
						|
      // If the last update time is greater than the current time then reset
 | 
						|
      // it to 0 and the timer manager will correct the value when it fires
 | 
						|
      // next for this consumer.
 | 
						|
      if (timerData.lastUpdateTime > now) {
 | 
						|
        let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
 | 
						|
          /%ID%/,
 | 
						|
          timerID
 | 
						|
        );
 | 
						|
        timerData.lastUpdateTime = 0;
 | 
						|
        Services.prefs.setIntPref(prefLastUpdate, timerData.lastUpdateTime);
 | 
						|
      }
 | 
						|
      tryFire(
 | 
						|
        timerID,
 | 
						|
        function () {
 | 
						|
          if (timerData.callback && timerData.callback.notify) {
 | 
						|
            ChromeUtils.idleDispatch(() => {
 | 
						|
              try {
 | 
						|
                let startTime = Cu.now();
 | 
						|
                timerData.callback.notify(timer);
 | 
						|
                ChromeUtils.addProfilerMarker(
 | 
						|
                  "UpdateTimer",
 | 
						|
                  { category: "Timer", startTime },
 | 
						|
                  timerID
 | 
						|
                );
 | 
						|
                LOG(`TimerManager:notify - notified timerID: ${timerID}`);
 | 
						|
              } catch (e) {
 | 
						|
                LOG(
 | 
						|
                  `TimerManager:notify - error notifying timerID: ${timerID}, error: ${e}`
 | 
						|
                );
 | 
						|
              }
 | 
						|
            });
 | 
						|
          } else {
 | 
						|
            LOG(
 | 
						|
              `TimerManager:notify - timerID: ${timerID} doesn't implement nsITimerCallback - skipping`
 | 
						|
            );
 | 
						|
          }
 | 
						|
          timerData.lastUpdateTime = now;
 | 
						|
          let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(
 | 
						|
            /%ID%/,
 | 
						|
            timerID
 | 
						|
          );
 | 
						|
          Services.prefs.setIntPref(prefLastUpdate, now);
 | 
						|
          updateNextDelay(timerData.interval);
 | 
						|
        },
 | 
						|
        timerData.lastUpdateTime + timerData.interval
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (callbacksToFire.length) {
 | 
						|
      callbacksToFire.sort((a, b) => a.intendedTime - b.intendedTime);
 | 
						|
      for (let { intendedTime, timerID, callback } of callbacksToFire) {
 | 
						|
        LOG(
 | 
						|
          `TimerManager:notify - fire timerID: ${timerID} ` +
 | 
						|
            `intended time: ${intendedTime} (${new Date(
 | 
						|
              intendedTime * 1000
 | 
						|
            ).toISOString()})`
 | 
						|
        );
 | 
						|
        callback();
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (nextDelay !== null) {
 | 
						|
      timer.delay = Math.max(nextDelay * 1000, this._timerMinimumDelay);
 | 
						|
      this.lastTimerReset = Date.now();
 | 
						|
    } else {
 | 
						|
      this._cancelTimer();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Starts the timer, if necessary, and ensures that it will fire soon enough
 | 
						|
   * to happen after time |interval| (in milliseconds).
 | 
						|
   */
 | 
						|
  _ensureTimer(interval) {
 | 
						|
    if (!this._canEnsureTimer) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (!this._timer) {
 | 
						|
      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | 
						|
      this._timer.initWithCallback(
 | 
						|
        this,
 | 
						|
        interval,
 | 
						|
        Ci.nsITimer.TYPE_REPEATING_SLACK
 | 
						|
      );
 | 
						|
      this.lastTimerReset = Date.now();
 | 
						|
    } else if (
 | 
						|
      Date.now() + interval <
 | 
						|
      this.lastTimerReset + this._timer.delay
 | 
						|
    ) {
 | 
						|
      this._timer.delay = Math.max(
 | 
						|
        this.lastTimerReset + interval - Date.now(),
 | 
						|
        0
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stops the timer, if it is running.
 | 
						|
   */
 | 
						|
  _cancelTimer() {
 | 
						|
    if (this._timer) {
 | 
						|
      this._timer.cancel();
 | 
						|
      this._timer = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * See nsIUpdateTimerManager.idl
 | 
						|
   */
 | 
						|
  registerTimer: function TM_registerTimer(id, callback, interval, skipFirst) {
 | 
						|
    let markerText = `timerID: ${id} interval: ${interval}s`;
 | 
						|
    if (skipFirst) {
 | 
						|
      markerText += " skipFirst";
 | 
						|
    }
 | 
						|
    ChromeUtils.addProfilerMarker(
 | 
						|
      "RegisterUpdateTimer",
 | 
						|
      { category: "Timer" },
 | 
						|
      markerText
 | 
						|
    );
 | 
						|
    LOG(
 | 
						|
      `TimerManager:registerTimer - timerID: ${id} interval: ${interval} skipFirst: ${skipFirst}`
 | 
						|
    );
 | 
						|
    if (this._timers === null) {
 | 
						|
      // Use normal logging since reportError is not available while shutting
 | 
						|
      // down.
 | 
						|
      LOG(
 | 
						|
        "TimerManager:registerTimer called after profile-before-change " +
 | 
						|
          "notification. Ignoring timer registration for id: " +
 | 
						|
          id,
 | 
						|
        true
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (id in this._timers && callback != this._timers[id].callback) {
 | 
						|
      LOG(
 | 
						|
        "TimerManager:registerTimer - Ignoring second registration for " + id
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id);
 | 
						|
    // Initialize the last update time to 0 when the preference isn't set so
 | 
						|
    // the timer will be notified soon after a new profile's first use.
 | 
						|
    let lastUpdateTime = Services.prefs.getIntPref(prefLastUpdate, 0);
 | 
						|
    let now = Math.round(Date.now() / 1000);
 | 
						|
    if (lastUpdateTime > now) {
 | 
						|
      lastUpdateTime = 0;
 | 
						|
    }
 | 
						|
    if (lastUpdateTime == 0) {
 | 
						|
      if (skipFirst) {
 | 
						|
        lastUpdateTime = now;
 | 
						|
      }
 | 
						|
      Services.prefs.setIntPref(prefLastUpdate, lastUpdateTime);
 | 
						|
    }
 | 
						|
    this._timers[id] = { callback, interval, lastUpdateTime };
 | 
						|
 | 
						|
    this._ensureTimer(interval * 1000);
 | 
						|
  },
 | 
						|
 | 
						|
  unregisterTimer: function TM_unregisterTimer(id) {
 | 
						|
    ChromeUtils.addProfilerMarker(
 | 
						|
      "UnregisterUpdateTimer",
 | 
						|
      { category: "Timer" },
 | 
						|
      id
 | 
						|
    );
 | 
						|
    LOG("TimerManager:unregisterTimer - id: " + id);
 | 
						|
    if (id in this._timers) {
 | 
						|
      delete this._timers[id];
 | 
						|
    } else {
 | 
						|
      LOG(
 | 
						|
        "TimerManager:unregisterTimer - Ignoring unregistration request for " +
 | 
						|
          "unknown id: " +
 | 
						|
          id
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  classID: Components.ID("{B322A5C0-A419-484E-96BA-D7182163899F}"),
 | 
						|
  QueryInterface: ChromeUtils.generateQI([
 | 
						|
    "nsINamed",
 | 
						|
    "nsIObserver",
 | 
						|
    "nsITimerCallback",
 | 
						|
    "nsIUpdateTimerManager",
 | 
						|
  ]),
 | 
						|
};
 |