tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

policies.sys.mjs (35475B)


      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/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 import { Log } from "resource://gre/modules/Log.sys.mjs";
      8 
      9 import {
     10  CREDENTIALS_CHANGED,
     11  ENGINE_APPLY_FAIL,
     12  ENGINE_UNKNOWN_FAIL,
     13  IDLE_OBSERVER_BACK_DELAY,
     14  LOGIN_FAILED_INVALID_PASSPHRASE,
     15  LOGIN_FAILED_LOGIN_REJECTED,
     16  LOGIN_FAILED_NETWORK_ERROR,
     17  LOGIN_FAILED_NO_PASSPHRASE,
     18  LOGIN_SUCCEEDED,
     19  MASTER_PASSWORD_LOCKED,
     20  MASTER_PASSWORD_LOCKED_RETRY_INTERVAL,
     21  MAX_ERROR_COUNT_BEFORE_BACKOFF,
     22  MINIMUM_BACKOFF_INTERVAL,
     23  MULTI_DEVICE_THRESHOLD,
     24  NO_SYNC_NODE_FOUND,
     25  NO_SYNC_NODE_INTERVAL,
     26  OVER_QUOTA,
     27  RESPONSE_OVER_QUOTA,
     28  SCORE_UPDATE_DELAY,
     29  SERVER_MAINTENANCE,
     30  SINGLE_USER_THRESHOLD,
     31  STATUS_OK,
     32  SYNC_FAILED_PARTIAL,
     33  SYNC_SUCCEEDED,
     34  kSyncBackoffNotMet,
     35  kSyncMasterPasswordLocked,
     36 } from "resource://services-sync/constants.sys.mjs";
     37 
     38 import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
     39 
     40 import { logManager } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     41 import { Async } from "resource://services-common/async.sys.mjs";
     42 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
     43 
     44 const lazy = {};
     45 
     46 ChromeUtils.defineESModuleGetters(lazy, {
     47  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     48  Status: "resource://services-sync/status.sys.mjs",
     49 });
     50 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
     51  return ChromeUtils.importESModule(
     52    "resource://gre/modules/FxAccounts.sys.mjs"
     53  ).getFxAccountsSingleton();
     54 });
     55 XPCOMUtils.defineLazyServiceGetter(
     56  lazy,
     57  "IdleService",
     58  "@mozilla.org/widget/useridleservice;1",
     59  Ci.nsIUserIdleService
     60 );
     61 XPCOMUtils.defineLazyServiceGetter(
     62  lazy,
     63  "CaptivePortalService",
     64  "@mozilla.org/network/captive-portal-service;1",
     65  Ci.nsICaptivePortalService
     66 );
     67 
     68 // Get the value for an interval that's stored in preferences. To save users
     69 // from themselves (and us from them!) the minimum time they can specify
     70 // is 60s.
     71 function getThrottledIntervalPreference(prefName) {
     72  return Math.max(Svc.PrefBranch.getIntPref(prefName), 60) * 1000;
     73 }
     74 
     75 export function SyncScheduler(service) {
     76  this.service = service;
     77  this.init();
     78 }
     79 
     80 SyncScheduler.prototype = {
     81  _log: Log.repository.getLogger("Sync.SyncScheduler"),
     82 
     83  _fatalLoginStatus: [
     84    LOGIN_FAILED_NO_PASSPHRASE,
     85    LOGIN_FAILED_INVALID_PASSPHRASE,
     86    LOGIN_FAILED_LOGIN_REJECTED,
     87  ],
     88 
     89  /**
     90   * The nsITimer object that schedules the next sync. See scheduleNextSync().
     91   */
     92  syncTimer: null,
     93 
     94  setDefaults: function setDefaults() {
     95    this._log.trace("Setting SyncScheduler policy values to defaults.");
     96 
     97    this.singleDeviceInterval = getThrottledIntervalPreference(
     98      "scheduler.fxa.singleDeviceInterval"
     99    );
    100    this.idleInterval = getThrottledIntervalPreference(
    101      "scheduler.idleInterval"
    102    );
    103    this.activeInterval = getThrottledIntervalPreference(
    104      "scheduler.activeInterval"
    105    );
    106    this.immediateInterval = getThrottledIntervalPreference(
    107      "scheduler.immediateInterval"
    108    );
    109 
    110    // A user is non-idle on startup by default.
    111    this.idle = false;
    112 
    113    this.hasIncomingItems = false;
    114    // This is the last number of clients we saw when previously updating the
    115    // client mode. If this != currentNumClients (obtained from prefs written
    116    // by the clients engine) then we need to transition to and from
    117    // single and multi-device mode.
    118    this.numClientsLastSync = 0;
    119 
    120    this._resyncs = 0;
    121 
    122    this.clearSyncTriggers();
    123  },
    124 
    125  // nextSync is in milliseconds, but prefs can't hold that much
    126  get nextSync() {
    127    return Svc.PrefBranch.getIntPref("nextSync", 0) * 1000;
    128  },
    129  set nextSync(value) {
    130    Svc.PrefBranch.setIntPref("nextSync", Math.floor(value / 1000));
    131  },
    132 
    133  get missedFxACommandsFetchInterval() {
    134    return Services.prefs.getIntPref(
    135      "identity.fxaccounts.commands.missed.fetch_interval"
    136    );
    137  },
    138 
    139  get missedFxACommandsLastFetch() {
    140    return Services.prefs.getIntPref(
    141      "identity.fxaccounts.commands.missed.last_fetch",
    142      0
    143    );
    144  },
    145 
    146  set missedFxACommandsLastFetch(val) {
    147    Services.prefs.setIntPref(
    148      "identity.fxaccounts.commands.missed.last_fetch",
    149      val
    150    );
    151  },
    152 
    153  get syncInterval() {
    154    return this._syncInterval;
    155  },
    156  set syncInterval(value) {
    157    if (value != this._syncInterval) {
    158      Services.prefs.setIntPref("services.sync.syncInterval", value);
    159    }
    160  },
    161 
    162  get syncThreshold() {
    163    return this._syncThreshold;
    164  },
    165  set syncThreshold(value) {
    166    if (value != this._syncThreshold) {
    167      Services.prefs.setIntPref("services.sync.syncThreshold", value);
    168    }
    169  },
    170 
    171  get globalScore() {
    172    return this._globalScore;
    173  },
    174  set globalScore(value) {
    175    if (this._globalScore != value) {
    176      Services.prefs.setIntPref("services.sync.globalScore", value);
    177    }
    178  },
    179 
    180  // Managed by the clients engine (by way of prefs)
    181  get numClients() {
    182    return this.numDesktopClients + this.numMobileClients;
    183  },
    184  set numClients(value) {
    185    throw new Error("Don't set numClients - the clients engine manages it.");
    186  },
    187 
    188  get offline() {
    189    // Services.io.offline has slowly become fairly useless over the years - it
    190    // no longer attempts to track the actual network state by default, but one
    191    // thing stays true: if it says we're offline then we are definitely not online.
    192    //
    193    // We also ask the captive portal service if we are behind a locked captive
    194    // portal.
    195    //
    196    // We don't check on the NetworkLinkService however, because it gave us
    197    // false positives in the past in a vm environment.
    198    try {
    199      if (
    200        Services.io.offline ||
    201        lazy.CaptivePortalService.state ==
    202          lazy.CaptivePortalService.LOCKED_PORTAL
    203      ) {
    204        return true;
    205      }
    206    } catch (ex) {
    207      this._log.warn("Could not determine network status.", ex);
    208    }
    209    return false;
    210  },
    211 
    212  _initPrefGetters() {
    213    XPCOMUtils.defineLazyPreferenceGetter(
    214      this,
    215      "idleTime",
    216      "services.sync.scheduler.idleTime"
    217    );
    218    XPCOMUtils.defineLazyPreferenceGetter(
    219      this,
    220      "maxResyncs",
    221      "services.sync.maxResyncs",
    222      0
    223    );
    224 
    225    // The number of clients we have is maintained in preferences via the
    226    // clients engine, and only updated after a successsful sync.
    227    XPCOMUtils.defineLazyPreferenceGetter(
    228      this,
    229      "numDesktopClients",
    230      "services.sync.clients.devices.desktop",
    231      0
    232    );
    233    XPCOMUtils.defineLazyPreferenceGetter(
    234      this,
    235      "numMobileClients",
    236      "services.sync.clients.devices.mobile",
    237      0
    238    );
    239 
    240    // Scheduler state that seems to be read more often than it's written.
    241    // We also check if the value has changed before writing in the setters.
    242    XPCOMUtils.defineLazyPreferenceGetter(
    243      this,
    244      "_syncThreshold",
    245      "services.sync.syncThreshold",
    246      SINGLE_USER_THRESHOLD
    247    );
    248    XPCOMUtils.defineLazyPreferenceGetter(
    249      this,
    250      "_syncInterval",
    251      "services.sync.syncInterval",
    252      this.singleDeviceInterval
    253    );
    254    XPCOMUtils.defineLazyPreferenceGetter(
    255      this,
    256      "_globalScore",
    257      "services.sync.globalScore",
    258      0
    259    );
    260  },
    261 
    262  init: function init() {
    263    this._log.manageLevelFromPref("services.sync.log.logger.service.main");
    264    this.setDefaults();
    265    this._initPrefGetters();
    266    Svc.Obs.add("weave:engine:score:updated", this);
    267    Svc.Obs.add("network:offline-status-changed", this);
    268    Svc.Obs.add("network:link-status-changed", this);
    269    Svc.Obs.add("captive-portal-detected", this);
    270    Svc.Obs.add("weave:service:sync:start", this);
    271    Svc.Obs.add("weave:service:sync:finish", this);
    272    Svc.Obs.add("weave:engine:sync:finish", this);
    273    Svc.Obs.add("weave:engine:sync:error", this);
    274    Svc.Obs.add("weave:service:login:error", this);
    275    Svc.Obs.add("weave:service:logout:finish", this);
    276    Svc.Obs.add("weave:service:sync:error", this);
    277    Svc.Obs.add("weave:service:backoff:interval", this);
    278    Svc.Obs.add("weave:engine:sync:applied", this);
    279    Svc.Obs.add("weave:service:setup-complete", this);
    280    Svc.Obs.add("weave:service:start-over", this);
    281    Svc.Obs.add("FxA:hawk:backoff:interval", this);
    282 
    283    if (lazy.Status.checkSetup() == STATUS_OK) {
    284      Svc.Obs.add("wake_notification", this);
    285      Svc.Obs.add("captive-portal-login-success", this);
    286      Svc.Obs.add("sleep_notification", this);
    287      lazy.IdleService.addIdleObserver(this, this.idleTime);
    288    }
    289  },
    290 
    291  // eslint-disable-next-line complexity
    292  observe: function observe(subject, topic, data) {
    293    this._log.trace("Handling " + topic);
    294    switch (topic) {
    295      case "weave:engine:score:updated":
    296        if (lazy.Status.login == LOGIN_SUCCEEDED) {
    297          CommonUtils.namedTimer(
    298            this.calculateScore,
    299            SCORE_UPDATE_DELAY,
    300            this,
    301            "_scoreTimer"
    302          );
    303        }
    304        break;
    305      case "network:link-status-changed": {
    306        // Note: NetworkLinkService is unreliable, we get false negatives for it
    307        // in cases such as VMs (bug 1420802), so we don't want to use it in
    308        // `get offline`, but we assume that it's probably reliable if we're
    309        // getting status changed events. (We might be wrong about this, but
    310        // if that's true, then the only downside is that we won't sync as
    311        // promptly).
    312        let isOffline = this.offline;
    313        this._log.debug(
    314          `Network link status changed to "${data}". Offline?`,
    315          isOffline
    316        );
    317        // Data may be one of `up`, `down`, `change`, or `unknown`. We only want
    318        // to sync if it's "up".
    319        if (data == "up" && !isOffline) {
    320          this._log.debug("Network link looks up. Syncing.");
    321          this.scheduleNextSync(0, { why: topic });
    322        } else if (data == "down") {
    323          // Unschedule pending syncs if we know we're going down. We don't do
    324          // this via `checkSyncStatus`, since link status isn't reflected in
    325          // `this.offline`.
    326          this.clearSyncTriggers();
    327        }
    328        break;
    329      }
    330      case "network:offline-status-changed":
    331      case "captive-portal-detected":
    332        // Whether online or offline, we'll reschedule syncs
    333        this._log.trace("Network offline status change: " + data);
    334        this.checkSyncStatus();
    335        break;
    336      case "weave:service:sync:start":
    337        // Clear out any potentially pending syncs now that we're syncing
    338        this.clearSyncTriggers();
    339 
    340        // reset backoff info, if the server tells us to continue backing off,
    341        // we'll handle that later
    342        lazy.Status.resetBackoff();
    343 
    344        this.globalScore = 0;
    345        break;
    346      case "weave:service:sync:finish": {
    347        this.nextSync = 0;
    348        this.adjustSyncInterval();
    349 
    350        if (
    351          lazy.Status.service == SYNC_FAILED_PARTIAL &&
    352          this.requiresBackoff
    353        ) {
    354          this.requiresBackoff = false;
    355          this.handleSyncError();
    356          return;
    357        }
    358 
    359        let sync_interval;
    360        let nextSyncReason = "schedule";
    361        this.updateGlobalScore();
    362        if (
    363          this.globalScore > this.syncThreshold &&
    364          lazy.Status.service == STATUS_OK
    365        ) {
    366          // The global score should be 0 after a sync. If it's not, either
    367          // items were changed during the last sync (and we should schedule an
    368          // immediate follow-up sync), or an engine skipped
    369          this._resyncs++;
    370          if (this._resyncs <= this.maxResyncs) {
    371            sync_interval = 0;
    372            nextSyncReason = "resync";
    373          } else {
    374            this._log.warn(
    375              `Resync attempt ${this._resyncs} exceeded ` +
    376                `maximum ${this.maxResyncs}`
    377            );
    378            Svc.Obs.notify("weave:service:resyncs-finished");
    379          }
    380        } else {
    381          this._resyncs = 0;
    382          Svc.Obs.notify("weave:service:resyncs-finished");
    383        }
    384 
    385        this._syncErrors = 0;
    386        if (lazy.Status.sync == NO_SYNC_NODE_FOUND) {
    387          // If we don't have a Sync node, override the interval, even if we've
    388          // scheduled a follow-up sync.
    389          this._log.trace("Scheduling a sync at interval NO_SYNC_NODE_FOUND.");
    390          sync_interval = NO_SYNC_NODE_INTERVAL;
    391        }
    392        this.scheduleNextSync(sync_interval, { why: nextSyncReason });
    393        break;
    394      }
    395      case "weave:engine:sync:finish":
    396        if (data == "clients") {
    397          // Update the client mode because it might change what we sync.
    398          this.updateClientMode();
    399        }
    400        break;
    401      case "weave:engine:sync:error": {
    402        // `subject` is the exception thrown by an engine's sync() method.
    403        let exception = subject;
    404        if (exception.status >= 500 && exception.status <= 504) {
    405          this.requiresBackoff = true;
    406        }
    407        break;
    408      }
    409      case "weave:service:login:error":
    410        this.clearSyncTriggers();
    411 
    412        if (lazy.Status.login == MASTER_PASSWORD_LOCKED) {
    413          // Try again later, just as if we threw an error... only without the
    414          // error count.
    415          this._log.debug("Couldn't log in: master password is locked.");
    416          this._log.trace(
    417            "Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"
    418          );
    419          this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
    420        } else if (!this._fatalLoginStatus.includes(lazy.Status.login)) {
    421          // Not a fatal login error, just an intermittent network or server
    422          // issue. Keep on syncin'.
    423          this.checkSyncStatus();
    424        }
    425        break;
    426      case "weave:service:logout:finish":
    427        // Start or cancel the sync timer depending on if
    428        // logged in or logged out
    429        this.checkSyncStatus();
    430        break;
    431      case "weave:service:sync:error":
    432        // There may be multiple clients but if the sync fails, client mode
    433        // should still be updated so that the next sync has a correct interval.
    434        this.updateClientMode();
    435        this.adjustSyncInterval();
    436        this.nextSync = 0;
    437        this.handleSyncError();
    438        break;
    439      case "FxA:hawk:backoff:interval":
    440      case "weave:service:backoff:interval": {
    441        let requested_interval = subject * 1000;
    442        this._log.debug(
    443          "Got backoff notification: " + requested_interval + "ms"
    444        );
    445        // Leave up to 25% more time for the back off.
    446        let interval = requested_interval * (1 + Math.random() * 0.25);
    447        lazy.Status.backoffInterval = interval;
    448        lazy.Status.minimumNextSync = Date.now() + requested_interval;
    449        this._log.debug(
    450          "Fuzzed minimum next sync: " + lazy.Status.minimumNextSync
    451        );
    452        break;
    453      }
    454      case "weave:engine:sync:applied": {
    455        let numItems = subject.succeeded;
    456        this._log.trace(
    457          "Engine " + data + " successfully applied " + numItems + " items."
    458        );
    459        // Bug 1800186 - the tabs engine always reports incoming items, so we don't
    460        // want special scheduling in this scenario.
    461        // (However, even when we fix the underlying cause of that, we probably still can
    462        // ignore tabs here - new incoming tabs don't need to trigger the extra syncs we do
    463        // based on this flag.)
    464        if (data != "tabs" && numItems) {
    465          this.hasIncomingItems = true;
    466        }
    467        if (subject.newFailed) {
    468          this._log.error(
    469            `Engine ${data} found ${subject.newFailed} new records that failed to apply`
    470          );
    471        }
    472        break;
    473      }
    474      case "weave:service:setup-complete":
    475        Services.prefs.savePrefFile(null);
    476        lazy.IdleService.addIdleObserver(this, this.idleTime);
    477        Svc.Obs.add("wake_notification", this);
    478        Svc.Obs.add("captive-portal-login-success", this);
    479        Svc.Obs.add("sleep_notification", this);
    480        break;
    481      case "weave:service:start-over":
    482        this.setDefaults();
    483        try {
    484          lazy.IdleService.removeIdleObserver(this, this.idleTime);
    485        } catch (ex) {
    486          if (ex.result != Cr.NS_ERROR_FAILURE) {
    487            throw ex;
    488          }
    489          // In all likelihood we didn't have an idle observer registered yet.
    490          // It's all good.
    491        }
    492        break;
    493      case "idle":
    494        this._log.trace("We're idle.");
    495        this.idle = true;
    496        // Adjust the interval for future syncs. This won't actually have any
    497        // effect until the next pending sync (which will happen soon since we
    498        // were just active.)
    499        this.adjustSyncInterval();
    500        break;
    501      case "active":
    502        this._log.trace("Received notification that we're back from idle.");
    503        this.idle = false;
    504        CommonUtils.namedTimer(
    505          function onBack() {
    506            if (this.idle) {
    507              this._log.trace(
    508                "... and we're idle again. " +
    509                  "Ignoring spurious back notification."
    510              );
    511              return;
    512            }
    513 
    514            this._log.trace("Genuine return from idle. Syncing.");
    515            // Trigger a sync if we have multiple clients.
    516            if (this.numClients > 1) {
    517              this.scheduleNextSync(0, { why: topic });
    518            }
    519          },
    520          IDLE_OBSERVER_BACK_DELAY,
    521          this,
    522          "idleDebouncerTimer"
    523        );
    524        break;
    525      case "wake_notification":
    526        this._log.debug("Woke from sleep.");
    527        CommonUtils.nextTick(() => {
    528          // Trigger a sync if we have multiple clients. We give it 2 seconds
    529          // so the browser can recover from the wake and do more important
    530          // operations first (timers etc).
    531          if (this.numClients > 1) {
    532            if (!this.offline) {
    533              this._log.debug("Online, will sync in 2s.");
    534              this.scheduleNextSync(2000, { why: topic });
    535            }
    536          }
    537        });
    538        break;
    539      case "captive-portal-login-success":
    540        this._log.debug("Captive portal login success. Scheduling a sync.");
    541        CommonUtils.nextTick(() => {
    542          this.scheduleNextSync(3000, { why: topic });
    543        });
    544        break;
    545      case "sleep_notification":
    546        if (this.service.engineManager.get("tabs")._tracker.modified) {
    547          this._log.debug("Going to sleep, doing a quick sync.");
    548          this.scheduleNextSync(0, { engines: ["tabs"], why: "sleep" });
    549        }
    550        break;
    551    }
    552  },
    553 
    554  adjustSyncInterval: function adjustSyncInterval() {
    555    if (this.numClients <= 1) {
    556      this._log.trace("Adjusting syncInterval to singleDeviceInterval.");
    557      this.syncInterval = this.singleDeviceInterval;
    558      return;
    559    }
    560 
    561    // Only MULTI_DEVICE clients will enter this if statement
    562    // since SINGLE_USER clients will be handled above.
    563    if (this.idle) {
    564      this._log.trace("Adjusting syncInterval to idleInterval.");
    565      this.syncInterval = this.idleInterval;
    566      return;
    567    }
    568 
    569    if (this.hasIncomingItems) {
    570      this._log.trace("Adjusting syncInterval to immediateInterval.");
    571      this.hasIncomingItems = false;
    572      this.syncInterval = this.immediateInterval;
    573    } else {
    574      this._log.trace("Adjusting syncInterval to activeInterval.");
    575      this.syncInterval = this.activeInterval;
    576    }
    577  },
    578 
    579  updateGlobalScore() {
    580    let engines = [this.service.clientsEngine].concat(
    581      this.service.engineManager.getEnabled()
    582    );
    583    let globalScore = this.globalScore;
    584    for (let i = 0; i < engines.length; i++) {
    585      this._log.trace(engines[i].name + ": score: " + engines[i].score);
    586      globalScore += engines[i].score;
    587      engines[i]._tracker.resetScore();
    588    }
    589    this.globalScore = globalScore;
    590    this._log.trace("Global score updated: " + globalScore);
    591  },
    592 
    593  calculateScore() {
    594    this.updateGlobalScore();
    595    this.checkSyncStatus();
    596  },
    597 
    598  /**
    599   * Query the number of known clients to figure out what mode to be in
    600   */
    601  updateClientMode: function updateClientMode() {
    602    // Nothing to do if it's the same amount
    603    let numClients = this.numClients;
    604    if (numClients == this.numClientsLastSync) {
    605      return;
    606    }
    607 
    608    this._log.debug(
    609      `Client count: ${this.numClientsLastSync} -> ${numClients}`
    610    );
    611    this.numClientsLastSync = numClients;
    612 
    613    if (numClients <= 1) {
    614      this._log.trace("Adjusting syncThreshold to SINGLE_USER_THRESHOLD");
    615      this.syncThreshold = SINGLE_USER_THRESHOLD;
    616    } else {
    617      this._log.trace("Adjusting syncThreshold to MULTI_DEVICE_THRESHOLD");
    618      this.syncThreshold = MULTI_DEVICE_THRESHOLD;
    619    }
    620    this.adjustSyncInterval();
    621  },
    622 
    623  /**
    624   * Check if we should be syncing and schedule the next sync, if it's not scheduled
    625   */
    626  checkSyncStatus: function checkSyncStatus() {
    627    // Should we be syncing now, if not, cancel any sync timers and return
    628    // if we're in backoff, we'll schedule the next sync.
    629    let ignore = [kSyncBackoffNotMet, kSyncMasterPasswordLocked];
    630    let skip = this.service._checkSync(ignore);
    631    this._log.trace('_checkSync returned "' + skip + '".');
    632    if (skip) {
    633      this.clearSyncTriggers();
    634      return;
    635    }
    636 
    637    let why = "schedule";
    638    // Only set the wait time to 0 if we need to sync right away
    639    let wait;
    640    if (this.globalScore > this.syncThreshold) {
    641      this._log.debug("Global Score threshold hit, triggering sync.");
    642      wait = 0;
    643      why = "score";
    644    }
    645    this.scheduleNextSync(wait, { why });
    646  },
    647 
    648  /**
    649   * Call sync() if Master Password is not locked.
    650   *
    651   * Otherwise, reschedule a sync for later.
    652   */
    653  syncIfMPUnlocked(engines, why) {
    654    // No point if we got kicked out by the master password dialog.
    655    if (lazy.Status.login == MASTER_PASSWORD_LOCKED && Utils.mpLocked()) {
    656      this._log.debug(
    657        "Not initiating sync: Login status is " + lazy.Status.login
    658      );
    659 
    660      // If we're not syncing now, we need to schedule the next one.
    661      this._log.trace(
    662        "Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"
    663      );
    664      this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL);
    665      return;
    666    }
    667 
    668    if (!Async.isAppReady()) {
    669      this._log.debug("Not initiating sync: app is shutting down");
    670      return;
    671    }
    672    Services.tm.dispatchToMainThread(() => {
    673      this.service.sync({ engines, why });
    674      const now = Math.round(new Date().getTime() / 1000);
    675      // Only fetch missed messages in a "scheduled" sync so we don't race against
    676      // the Push service reconnecting on a network link change for example.
    677      if (
    678        why == "schedule" &&
    679        now >=
    680          this.missedFxACommandsLastFetch + this.missedFxACommandsFetchInterval
    681      ) {
    682        lazy.fxAccounts.commands
    683          .pollDeviceCommands()
    684          .then(() => {
    685            this.missedFxACommandsLastFetch = now;
    686          })
    687          .catch(e => {
    688            this._log.error("Fetching missed remote commands failed.", e);
    689          });
    690      }
    691    });
    692  },
    693 
    694  /**
    695   * Set a timer for the next sync
    696   */
    697  scheduleNextSync(interval, { engines = null, why = null } = {}) {
    698    // If no interval was specified, use the current sync interval.
    699    if (interval == null) {
    700      interval = this.syncInterval;
    701    }
    702 
    703    // Ensure the interval is set to no less than the backoff.
    704    if (lazy.Status.backoffInterval && interval < lazy.Status.backoffInterval) {
    705      this._log.trace(
    706        "Requested interval " +
    707          interval +
    708          " ms is smaller than the backoff interval. " +
    709          "Using backoff interval " +
    710          lazy.Status.backoffInterval +
    711          " ms instead."
    712      );
    713      interval = lazy.Status.backoffInterval;
    714    }
    715    let nextSync = this.nextSync;
    716    if (nextSync != 0) {
    717      // There's already a sync scheduled. Don't reschedule if there's already
    718      // a timer scheduled for sooner than requested.
    719      let currentInterval = nextSync - Date.now();
    720      this._log.trace(
    721        "There's already a sync scheduled in " + currentInterval + " ms."
    722      );
    723      if (currentInterval < interval && this.syncTimer) {
    724        this._log.trace(
    725          "Ignoring scheduling request for next sync in " + interval + " ms."
    726        );
    727        return;
    728      }
    729    }
    730 
    731    // Start the sync right away if we're already late.
    732    if (interval <= 0) {
    733      this._log.trace(`Requested sync should happen right away. (why=${why})`);
    734      this.syncIfMPUnlocked(engines, why);
    735      return;
    736    }
    737 
    738    this._log.debug(`Next sync in ${interval} ms. (why=${why})`);
    739    CommonUtils.namedTimer(
    740      () => {
    741        this.syncIfMPUnlocked(engines, why);
    742      },
    743      interval,
    744      this,
    745      "syncTimer"
    746    );
    747 
    748    // Save the next sync time in-case sync is disabled (logout/offline/etc.)
    749    this.nextSync = Date.now() + interval;
    750  },
    751 
    752  /**
    753   * Incorporates the backoff/retry logic used in error handling and elective
    754   * non-syncing.
    755   */
    756  scheduleAtInterval: function scheduleAtInterval(minimumInterval) {
    757    let interval = Utils.calculateBackoff(
    758      this._syncErrors,
    759      MINIMUM_BACKOFF_INTERVAL,
    760      lazy.Status.backoffInterval
    761    );
    762    if (minimumInterval) {
    763      interval = Math.max(minimumInterval, interval);
    764    }
    765 
    766    this._log.debug(
    767      "Starting client-initiated backoff. Next sync in " + interval + " ms."
    768    );
    769    this.scheduleNextSync(interval, { why: "client-backoff-schedule" });
    770  },
    771 
    772  autoConnect: function autoConnect() {
    773    if (this.service._checkSetup() == STATUS_OK && !this.service._checkSync()) {
    774      // Schedule a sync based on when a previous sync was scheduled.
    775      // scheduleNextSync() will do the right thing if that time lies in
    776      // the past.
    777      this.scheduleNextSync(this.nextSync - Date.now(), { why: "startup" });
    778    }
    779  },
    780 
    781  _syncErrors: 0,
    782  /**
    783   * Deal with sync errors appropriately
    784   */
    785  handleSyncError: function handleSyncError() {
    786    this._log.trace("In handleSyncError. Error count: " + this._syncErrors);
    787    this._syncErrors++;
    788 
    789    // Do nothing on the first couple of failures, if we're not in
    790    // backoff due to 5xx errors.
    791    if (!lazy.Status.enforceBackoff) {
    792      if (this._syncErrors < MAX_ERROR_COUNT_BEFORE_BACKOFF) {
    793        this.scheduleNextSync(null, { why: "reschedule" });
    794        return;
    795      }
    796      this._log.debug(
    797        "Sync error count has exceeded " +
    798          MAX_ERROR_COUNT_BEFORE_BACKOFF +
    799          "; enforcing backoff."
    800      );
    801      lazy.Status.enforceBackoff = true;
    802    }
    803 
    804    this.scheduleAtInterval();
    805  },
    806 
    807  /**
    808   * Remove any timers/observers that might trigger a sync
    809   */
    810  clearSyncTriggers: function clearSyncTriggers() {
    811    this._log.debug("Clearing sync triggers and the global score.");
    812    this.globalScore = this.nextSync = 0;
    813 
    814    // Clear out any scheduled syncs
    815    if (this.syncTimer) {
    816      this.syncTimer.clear();
    817    }
    818  },
    819 };
    820 
    821 export function ErrorHandler(service) {
    822  this.service = service;
    823  this.init();
    824 }
    825 
    826 ErrorHandler.prototype = {
    827  init() {
    828    Svc.Obs.add("weave:engine:sync:applied", this);
    829    Svc.Obs.add("weave:engine:sync:error", this);
    830    Svc.Obs.add("weave:service:login:error", this);
    831    Svc.Obs.add("weave:service:sync:error", this);
    832    Svc.Obs.add("weave:service:sync:finish", this);
    833    Svc.Obs.add("weave:service:start-over:finish", this);
    834 
    835    this.initLogs();
    836  },
    837 
    838  initLogs: function initLogs() {
    839    // Set the root Sync logger level based on a pref. All other logs will
    840    // inherit this level unless they specifically override it.
    841    Log.repository
    842      .getLogger("Sync")
    843      .manageLevelFromPref(`services.sync.log.logger`);
    844    // And allow our specific log to have a custom level via a pref.
    845    this._log = Log.repository.getLogger("Sync.ErrorHandler");
    846    this._log.manageLevelFromPref("services.sync.log.logger.service.main");
    847  },
    848 
    849  observe(subject, topic, data) {
    850    this._log.trace("Handling " + topic);
    851    switch (topic) {
    852      case "weave:engine:sync:applied":
    853        if (subject.newFailed) {
    854          // An engine isn't able to apply one or more incoming records.
    855          // We don't fail hard on this, but it usually indicates a bug,
    856          // so for now treat it as sync error (c.f. Service._syncEngine())
    857          lazy.Status.engines = [data, ENGINE_APPLY_FAIL];
    858          this._log.debug(data + " failed to apply some records.");
    859        }
    860        break;
    861      case "weave:engine:sync:error": {
    862        let exception = subject; // exception thrown by engine's sync() method
    863        let engine_name = data; // engine name that threw the exception
    864 
    865        this.checkServerError(exception);
    866 
    867        lazy.Status.engines = [
    868          engine_name,
    869          exception.failureCode || ENGINE_UNKNOWN_FAIL,
    870        ];
    871        if (Async.isShutdownException(exception)) {
    872          this._log.debug(
    873            engine_name +
    874              " was interrupted due to the application shutting down"
    875          );
    876        } else {
    877          this._log.debug(engine_name + " failed", exception);
    878        }
    879        break;
    880      }
    881      case "weave:service:login:error":
    882        this._log.error("Sync encountered a login error");
    883        this.resetFileLog();
    884        break;
    885      case "weave:service:sync:error": {
    886        if (lazy.Status.sync == CREDENTIALS_CHANGED) {
    887          this.service.logout();
    888        }
    889 
    890        let exception = subject;
    891        if (Async.isShutdownException(exception)) {
    892          // If we are shutting down we just log the fact, attempt to flush
    893          // the log file and get out of here!
    894          this._log.error(
    895            "Sync was interrupted due to the application shutting down"
    896          );
    897          this.resetFileLog();
    898          break;
    899        }
    900 
    901        // Not a shutdown related exception...
    902        this._log.error("Sync encountered an error", exception);
    903        this.resetFileLog();
    904        break;
    905      }
    906      case "weave:service:sync:finish":
    907        this._log.trace("Status.service is " + lazy.Status.service);
    908 
    909        // Check both of these status codes: in the event of a failure in one
    910        // engine, Status.service will be SYNC_FAILED_PARTIAL despite
    911        // Status.sync being SYNC_SUCCEEDED.
    912        // *facepalm*
    913        if (
    914          lazy.Status.sync == SYNC_SUCCEEDED &&
    915          lazy.Status.service == STATUS_OK
    916        ) {
    917          // Great. Let's clear our mid-sync 401 note.
    918          this._log.trace("Clearing lastSyncReassigned.");
    919          Svc.PrefBranch.clearUserPref("lastSyncReassigned");
    920        }
    921 
    922        if (lazy.Status.service == SYNC_FAILED_PARTIAL) {
    923          this._log.error("Some engines did not sync correctly.");
    924        }
    925        this.resetFileLog();
    926        break;
    927      case "weave:service:start-over:finish":
    928        // ensure we capture any logs between the last sync and the reset completing.
    929        this.resetFileLog()
    930          .then(() => {
    931            // although for privacy reasons we also delete all logs (but we allow
    932            // a preference to avoid this to help with debugging.)
    933            if (!Svc.PrefBranch.getBoolPref("log.keepLogsOnReset", false)) {
    934              return logManager.removeAllLogs().then(() => {
    935                Svc.Obs.notify("weave:service:remove-file-log");
    936              });
    937            }
    938            return null;
    939          })
    940          .catch(err => {
    941            // So we failed to delete the logs - take the ironic option of
    942            // writing this error to the logs we failed to delete!
    943            this._log.error("Failed to delete logs on reset", err);
    944          });
    945        break;
    946    }
    947  },
    948 
    949  async _dumpAddons() {
    950    // Just dump the items that sync may be concerned with. Specifically,
    951    // active extensions that are not hidden.
    952    let addons = [];
    953    try {
    954      addons = await lazy.AddonManager.getAddonsByTypes(["extension"]);
    955    } catch (e) {
    956      this._log.warn("Failed to dump addons", e);
    957    }
    958 
    959    let relevantAddons = addons.filter(x => x.isActive && !x.hidden);
    960    this._log.trace("Addons installed", relevantAddons.length);
    961    for (let addon of relevantAddons) {
    962      this._log.trace(" - ${name}, version ${version}, id ${id}", addon);
    963    }
    964  },
    965 
    966  /**
    967   * Generate a log file for the sync that just completed
    968   * and refresh the input & output streams.
    969   */
    970  async resetFileLog() {
    971    // If we're writing an error log, dump extensions that may be causing problems.
    972    if (logManager.sawError) {
    973      await this._dumpAddons();
    974    }
    975    const logType = await logManager.resetFileLog();
    976    if (logType == logManager.ERROR_LOG_WRITTEN) {
    977      console.error(
    978        "Sync encountered an error - see about:sync-log for the log file."
    979      );
    980    }
    981    Svc.Obs.notify("weave:service:reset-file-log");
    982  },
    983 
    984  /**
    985   * Handle HTTP response results or exceptions and set the appropriate
    986   * Status.* bits.
    987   *
    988   * This method also looks for "side-channel" warnings.
    989   */
    990  checkServerError(resp) {
    991    // In this case we were passed a resolved value of Resource#_doRequest.
    992    switch (resp.status) {
    993      case 400:
    994        if (resp == RESPONSE_OVER_QUOTA) {
    995          lazy.Status.sync = OVER_QUOTA;
    996        }
    997        break;
    998 
    999      case 401: {
   1000        this.service.logout();
   1001        this._log.info("Got 401 response; resetting clusterURL.");
   1002        this.service.clusterURL = null;
   1003 
   1004        let delay = 0;
   1005        if (Svc.PrefBranch.getBoolPref("lastSyncReassigned", false)) {
   1006          // We got a 401 in the middle of the previous sync, and we just got
   1007          // another. Login must have succeeded in order for us to get here, so
   1008          // the password should be correct.
   1009          // This is likely to be an intermittent server issue, so back off and
   1010          // give it time to recover.
   1011          this._log.warn("Last sync also failed for 401. Delaying next sync.");
   1012          delay = MINIMUM_BACKOFF_INTERVAL;
   1013        } else {
   1014          this._log.debug("New mid-sync 401 failure. Making a note.");
   1015          Svc.PrefBranch.setBoolPref("lastSyncReassigned", true);
   1016        }
   1017        this._log.info("Attempting to schedule another sync.");
   1018        this.service.scheduler.scheduleNextSync(delay, { why: "reschedule" });
   1019        break;
   1020      }
   1021 
   1022      case 500:
   1023      case 502:
   1024      case 503:
   1025      case 504:
   1026        lazy.Status.enforceBackoff = true;
   1027        if (resp.status == 503 && resp.headers["retry-after"]) {
   1028          let retryAfter = resp.headers["retry-after"];
   1029          this._log.debug("Got Retry-After: " + retryAfter);
   1030          if (this.service.isLoggedIn) {
   1031            lazy.Status.sync = SERVER_MAINTENANCE;
   1032          } else {
   1033            lazy.Status.login = SERVER_MAINTENANCE;
   1034          }
   1035          Svc.Obs.notify(
   1036            "weave:service:backoff:interval",
   1037            parseInt(retryAfter, 10)
   1038          );
   1039        }
   1040        break;
   1041    }
   1042 
   1043    // In this other case we were passed a rejection value.
   1044    switch (resp.result) {
   1045      case Cr.NS_ERROR_UNKNOWN_HOST:
   1046      case Cr.NS_ERROR_CONNECTION_REFUSED:
   1047      case Cr.NS_ERROR_NET_TIMEOUT:
   1048      case Cr.NS_ERROR_NET_RESET:
   1049      case Cr.NS_ERROR_NET_INTERRUPT:
   1050      case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
   1051        // The constant says it's about login, but in fact it just
   1052        // indicates general network error.
   1053        if (this.service.isLoggedIn) {
   1054          lazy.Status.sync = LOGIN_FAILED_NETWORK_ERROR;
   1055        } else {
   1056          lazy.Status.login = LOGIN_FAILED_NETWORK_ERROR;
   1057        }
   1058        break;
   1059    }
   1060  },
   1061 };