services/datareporting/sessions.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 #ifndef MERGED_COMPARTMENT
     9 this.EXPORTED_SYMBOLS = [
    10   "SessionRecorder",
    11 ];
    13 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    15 #endif
    17 Cu.import("resource://gre/modules/Preferences.jsm");
    18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 Cu.import("resource://gre/modules/Log.jsm");
    20 Cu.import("resource://services-common/utils.js");
    23 // We automatically prune sessions older than this.
    24 const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
    25 const STARTUP_RETRY_INTERVAL_MS = 5000;
    27 // Wait up to 5 minutes for startup measurements before giving up.
    28 const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
    30 /**
    31  * Records information about browser sessions.
    32  *
    33  * This serves as an interface to both current session information as
    34  * well as a history of previous sessions.
    35  *
    36  * Typically only one instance of this will be installed in an
    37  * application. It is typically managed by an XPCOM service. The
    38  * instance is instantiated at application start; onStartup is called
    39  * once the profile is installed; onShutdown is called during shutdown.
    40  *
    41  * We currently record state in preferences. However, this should be
    42  * invisible to external consumers. We could easily swap in a different
    43  * storage mechanism if desired.
    44  *
    45  * Please note the different semantics for storing times and dates in
    46  * preferences. Full dates (notably the session start time) are stored
    47  * as strings because preferences have a 32-bit limit on integer values
    48  * and milliseconds since UNIX epoch would overflow. Many times are
    49  * stored as integer offsets from the session start time because they
    50  * should not overflow 32 bits.
    51  *
    52  * Since this records history of all sessions, there is a possibility
    53  * for unbounded data aggregation. This is curtailed through:
    54  *
    55  *   1) An "idle-daily" observer which delete sessions older than
    56  *      MAX_SESSION_AGE_MS.
    57  *   2) The creator of this instance explicitly calling
    58  *      `pruneOldSessions`.
    59  *
    60  * @param branch
    61  *        (string) Preferences branch on which to record state.
    62  */
    63 this.SessionRecorder = function (branch) {
    64   if (!branch) {
    65     throw new Error("branch argument must be defined.");
    66   }
    68   if (!branch.endsWith(".")) {
    69     throw new Error("branch argument must end with '.': " + branch);
    70   }
    72   this._log = Log.repository.getLogger("Services.DataReporting.SessionRecorder");
    74   this._prefs = new Preferences(branch);
    75   this._lastActivityWasInactive = false;
    76   this._activeTicks = 0;
    77   this.fineTotalTime = 0;
    78   this._started = false;
    79   this._timer = null;
    80   this._startupFieldTries = 0;
    82   this._os = Cc["@mozilla.org/observer-service;1"]
    83                .getService(Ci.nsIObserverService);
    85 };
    87 SessionRecorder.prototype = Object.freeze({
    88   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
    90   STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
    92   get _currentIndex() {
    93     return this._prefs.get("currentIndex", 0);
    94   },
    96   set _currentIndex(value) {
    97     this._prefs.set("currentIndex", value);
    98   },
   100   get _prunedIndex() {
   101     return this._prefs.get("prunedIndex", 0);
   102   },
   104   set _prunedIndex(value) {
   105     this._prefs.set("prunedIndex", value);
   106   },
   108   get startDate() {
   109     return CommonUtils.getDatePref(this._prefs, "current.startTime");
   110   },
   112   set _startDate(value) {
   113     CommonUtils.setDatePref(this._prefs, "current.startTime", value);
   114   },
   116   get activeTicks() {
   117     return this._prefs.get("current.activeTicks", 0);
   118   },
   120   incrementActiveTicks: function () {
   121     this._prefs.set("current.activeTicks", ++this._activeTicks);
   122   },
   124   /**
   125    * Total time of this session in integer seconds.
   126    *
   127    * See also fineTotalTime for the time in milliseconds.
   128    */
   129   get totalTime() {
   130     return this._prefs.get("current.totalTime", 0);
   131   },
   133   updateTotalTime: function () {
   134     // We store millisecond precision internally to prevent drift from
   135     // repeated rounding.
   136     this.fineTotalTime = Date.now() - this.startDate;
   137     this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
   138   },
   140   get main() {
   141     return this._prefs.get("current.main", -1);
   142   },
   144   set _main(value) {
   145     if (!Number.isInteger(value)) {
   146       throw new Error("main time must be an integer.");
   147     }
   149     this._prefs.set("current.main", value);
   150   },
   152   get firstPaint() {
   153     return this._prefs.get("current.firstPaint", -1);
   154   },
   156   set _firstPaint(value) {
   157     if (!Number.isInteger(value)) {
   158       throw new Error("firstPaint must be an integer.");
   159     }
   161     this._prefs.set("current.firstPaint", value);
   162   },
   164   get sessionRestored() {
   165     return this._prefs.get("current.sessionRestored", -1);
   166   },
   168   set _sessionRestored(value) {
   169     if (!Number.isInteger(value)) {
   170       throw new Error("sessionRestored must be an integer.");
   171     }
   173     this._prefs.set("current.sessionRestored", value);
   174   },
   176   getPreviousSessions: function () {
   177     let result = {};
   179     for (let i = this._prunedIndex; i < this._currentIndex; i++) {
   180       let s = this.getPreviousSession(i);
   181       if (!s) {
   182         continue;
   183       }
   185       result[i] = s;
   186     }
   188     return result;
   189   },
   191   getPreviousSession: function (index) {
   192     return this._deserialize(this._prefs.get("previous." + index));
   193   },
   195   /**
   196    * Prunes old, completed sessions that started earlier than the
   197    * specified date.
   198    */
   199   pruneOldSessions: function (date) {
   200     for (let i = this._prunedIndex; i < this._currentIndex; i++) {
   201       let s = this.getPreviousSession(i);
   202       if (!s) {
   203         continue;
   204       }
   206       if (s.startDate >= date) {
   207         continue;
   208       }
   210       this._log.debug("Pruning session #" + i + ".");
   211       this._prefs.reset("previous." + i);
   212       this._prunedIndex = i;
   213     }
   214   },
   216   recordStartupFields: function () {
   217     let si = this._getStartupInfo();
   219     if (!si.process) {
   220       throw new Error("Startup info not available.");
   221     }
   223     let missing = false;
   225     for (let field of ["main", "firstPaint", "sessionRestored"]) {
   226       if (!(field in si)) {
   227         this._log.debug("Missing startup field: " + field);
   228         missing = true;
   229         continue;
   230       }
   232       this["_" + field] = si[field].getTime() - si.process.getTime();
   233     }
   235     if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
   236       this._clearStartupTimer();
   237       return;
   238     }
   240     // If we have missing fields, install a timer and keep waiting for
   241     // data.
   242     this._startupFieldTries++;
   244     if (!this._timer) {
   245       this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   246       this._timer.initWithCallback({
   247         notify: this.recordStartupFields.bind(this),
   248       }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
   249     }
   250   },
   252   _clearStartupTimer: function () {
   253     if (this._timer) {
   254       this._timer.cancel();
   255       delete this._timer;
   256     }
   257   },
   259   /**
   260    * Perform functionality on application startup.
   261    *
   262    * This is typically called in a "profile-do-change" handler.
   263    */
   264   onStartup: function () {
   265     if (this._started) {
   266       throw new Error("onStartup has already been called.");
   267     }
   269     let si = this._getStartupInfo();
   270     if (!si.process) {
   271       throw new Error("Process information not available. Misconfigured app?");
   272     }
   274     this._started = true;
   276     this._os.addObserver(this, "profile-before-change", false);
   277     this._os.addObserver(this, "user-interaction-active", false);
   278     this._os.addObserver(this, "user-interaction-inactive", false);
   279     this._os.addObserver(this, "idle-daily", false);
   281     // This has the side-effect of clearing current session state.
   282     this._moveCurrentToPrevious();
   284     this._startDate = si.process;
   285     this._prefs.set("current.activeTicks", 0);
   286     this.updateTotalTime();
   288     this.recordStartupFields();
   289   },
   291   /**
   292    * Record application activity.
   293    */
   294   onActivity: function (active) {
   295     let updateActive = active && !this._lastActivityWasInactive;
   296     this._lastActivityWasInactive = !active;
   298     this.updateTotalTime();
   300     if (updateActive) {
   301       this.incrementActiveTicks();
   302     }
   303   },
   305   onShutdown: function () {
   306     this._log.info("Recording clean session shutdown.");
   307     this._prefs.set("current.clean", true);
   308     this.updateTotalTime();
   309     this._clearStartupTimer();
   311     this._os.removeObserver(this, "profile-before-change");
   312     this._os.removeObserver(this, "user-interaction-active");
   313     this._os.removeObserver(this, "user-interaction-inactive");
   314     this._os.removeObserver(this, "idle-daily");
   315   },
   317   _CURRENT_PREFS: [
   318     "current.startTime",
   319     "current.activeTicks",
   320     "current.totalTime",
   321     "current.main",
   322     "current.firstPaint",
   323     "current.sessionRestored",
   324     "current.clean",
   325   ],
   327   // This is meant to be called only during onStartup().
   328   _moveCurrentToPrevious: function () {
   329     try {
   330       if (!this.startDate.getTime()) {
   331         this._log.info("No previous session. Is this first app run?");
   332         return;
   333       }
   335       let clean = this._prefs.get("current.clean", false);
   337       let count = this._currentIndex++;
   338       let obj = {
   339         s: this.startDate.getTime(),
   340         a: this.activeTicks,
   341         t: this.totalTime,
   342         c: clean,
   343         m: this.main,
   344         fp: this.firstPaint,
   345         sr: this.sessionRestored,
   346       };
   348       this._log.debug("Recording last sessions as #" + count + ".");
   349       this._prefs.set("previous." + count, JSON.stringify(obj));
   350     } catch (ex) {
   351       this._log.warn("Exception when migrating last session: " +
   352                      CommonUtils.exceptionStr(ex));
   353     } finally {
   354       this._log.debug("Resetting prefs from last session.");
   355       for (let pref of this._CURRENT_PREFS) {
   356         this._prefs.reset(pref);
   357       }
   358     }
   359   },
   361   _deserialize: function (s) {
   362     let o;
   363     try {
   364       o = JSON.parse(s);
   365     } catch (ex) {
   366       return null;
   367     }
   369     return {
   370       startDate: new Date(o.s),
   371       activeTicks: o.a,
   372       totalTime: o.t,
   373       clean: !!o.c,
   374       main: o.m,
   375       firstPaint: o.fp,
   376       sessionRestored: o.sr,
   377     };
   378   },
   380   // Implemented as a function to allow for monkeypatching in tests.
   381   _getStartupInfo: function () {
   382     return Cc["@mozilla.org/toolkit/app-startup;1"]
   383              .getService(Ci.nsIAppStartup)
   384              .getStartupInfo();
   385   },
   387   observe: function (subject, topic, data) {
   388     switch (topic) {
   389       case "profile-before-change":
   390         this.onShutdown();
   391         break;
   393       case "user-interaction-active":
   394         this.onActivity(true);
   395         break;
   397       case "user-interaction-inactive":
   398         this.onActivity(false);
   399         break;
   401       case "idle-daily":
   402         this.pruneOldSessions(new Date(Date.now() - MAX_SESSION_AGE_MS));
   403         break;
   404     }
   405   },
   406 });

mercurial