1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/policies.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,963 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = [ 1.9 + "ErrorHandler", 1.10 + "SyncScheduler", 1.11 +]; 1.12 + 1.13 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.14 + 1.15 +Cu.import("resource://gre/modules/Log.jsm"); 1.16 +Cu.import("resource://services-sync/constants.js"); 1.17 +Cu.import("resource://services-sync/engines.js"); 1.18 +Cu.import("resource://services-sync/util.js"); 1.19 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "Status", 1.23 + "resource://services-sync/status.js"); 1.24 + 1.25 +this.SyncScheduler = function SyncScheduler(service) { 1.26 + this.service = service; 1.27 + this.init(); 1.28 +} 1.29 +SyncScheduler.prototype = { 1.30 + _log: Log.repository.getLogger("Sync.SyncScheduler"), 1.31 + 1.32 + _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME, 1.33 + LOGIN_FAILED_NO_PASSWORD, 1.34 + LOGIN_FAILED_NO_PASSPHRASE, 1.35 + LOGIN_FAILED_INVALID_PASSPHRASE, 1.36 + LOGIN_FAILED_LOGIN_REJECTED], 1.37 + 1.38 + /** 1.39 + * The nsITimer object that schedules the next sync. See scheduleNextSync(). 1.40 + */ 1.41 + syncTimer: null, 1.42 + 1.43 + setDefaults: function setDefaults() { 1.44 + this._log.trace("Setting SyncScheduler policy values to defaults."); 1.45 + 1.46 + let service = Cc["@mozilla.org/weave/service;1"] 1.47 + .getService(Ci.nsISupports) 1.48 + .wrappedJSObject; 1.49 + 1.50 + let part = service.fxAccountsEnabled ? "fxa" : "sync11"; 1.51 + let prefSDInterval = "scheduler." + part + ".singleDeviceInterval"; 1.52 + this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000; 1.53 + 1.54 + this.idleInterval = Svc.Prefs.get("scheduler.idleInterval") * 1000; 1.55 + this.activeInterval = Svc.Prefs.get("scheduler.activeInterval") * 1000; 1.56 + this.immediateInterval = Svc.Prefs.get("scheduler.immediateInterval") * 1000; 1.57 + this.eolInterval = Svc.Prefs.get("scheduler.eolInterval") * 1000; 1.58 + 1.59 + // A user is non-idle on startup by default. 1.60 + this.idle = false; 1.61 + 1.62 + this.hasIncomingItems = false; 1.63 + 1.64 + this.clearSyncTriggers(); 1.65 + }, 1.66 + 1.67 + // nextSync is in milliseconds, but prefs can't hold that much 1.68 + get nextSync() Svc.Prefs.get("nextSync", 0) * 1000, 1.69 + set nextSync(value) Svc.Prefs.set("nextSync", Math.floor(value / 1000)), 1.70 + 1.71 + get syncInterval() Svc.Prefs.get("syncInterval", this.singleDeviceInterval), 1.72 + set syncInterval(value) Svc.Prefs.set("syncInterval", value), 1.73 + 1.74 + get syncThreshold() Svc.Prefs.get("syncThreshold", SINGLE_USER_THRESHOLD), 1.75 + set syncThreshold(value) Svc.Prefs.set("syncThreshold", value), 1.76 + 1.77 + get globalScore() Svc.Prefs.get("globalScore", 0), 1.78 + set globalScore(value) Svc.Prefs.set("globalScore", value), 1.79 + 1.80 + get numClients() Svc.Prefs.get("numClients", 0), 1.81 + set numClients(value) Svc.Prefs.set("numClients", value), 1.82 + 1.83 + init: function init() { 1.84 + this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; 1.85 + this.setDefaults(); 1.86 + Svc.Obs.add("weave:engine:score:updated", this); 1.87 + Svc.Obs.add("network:offline-status-changed", this); 1.88 + Svc.Obs.add("weave:service:sync:start", this); 1.89 + Svc.Obs.add("weave:service:sync:finish", this); 1.90 + Svc.Obs.add("weave:engine:sync:finish", this); 1.91 + Svc.Obs.add("weave:engine:sync:error", this); 1.92 + Svc.Obs.add("weave:service:login:error", this); 1.93 + Svc.Obs.add("weave:service:logout:finish", this); 1.94 + Svc.Obs.add("weave:service:sync:error", this); 1.95 + Svc.Obs.add("weave:service:backoff:interval", this); 1.96 + Svc.Obs.add("weave:service:ready", this); 1.97 + Svc.Obs.add("weave:engine:sync:applied", this); 1.98 + Svc.Obs.add("weave:service:setup-complete", this); 1.99 + Svc.Obs.add("weave:service:start-over", this); 1.100 + Svc.Obs.add("FxA:hawk:backoff:interval", this); 1.101 + 1.102 + if (Status.checkSetup() == STATUS_OK) { 1.103 + Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); 1.104 + } 1.105 + }, 1.106 + 1.107 + observe: function observe(subject, topic, data) { 1.108 + this._log.trace("Handling " + topic); 1.109 + switch(topic) { 1.110 + case "weave:engine:score:updated": 1.111 + if (Status.login == LOGIN_SUCCEEDED) { 1.112 + Utils.namedTimer(this.calculateScore, SCORE_UPDATE_DELAY, this, 1.113 + "_scoreTimer"); 1.114 + } 1.115 + break; 1.116 + case "network:offline-status-changed": 1.117 + // Whether online or offline, we'll reschedule syncs 1.118 + this._log.trace("Network offline status change: " + data); 1.119 + this.checkSyncStatus(); 1.120 + break; 1.121 + case "weave:service:sync:start": 1.122 + // Clear out any potentially pending syncs now that we're syncing 1.123 + this.clearSyncTriggers(); 1.124 + 1.125 + // reset backoff info, if the server tells us to continue backing off, 1.126 + // we'll handle that later 1.127 + Status.resetBackoff(); 1.128 + 1.129 + this.globalScore = 0; 1.130 + break; 1.131 + case "weave:service:sync:finish": 1.132 + this.nextSync = 0; 1.133 + this.adjustSyncInterval(); 1.134 + 1.135 + if (Status.service == SYNC_FAILED_PARTIAL && this.requiresBackoff) { 1.136 + this.requiresBackoff = false; 1.137 + this.handleSyncError(); 1.138 + return; 1.139 + } 1.140 + 1.141 + let sync_interval; 1.142 + this._syncErrors = 0; 1.143 + if (Status.sync == NO_SYNC_NODE_FOUND) { 1.144 + this._log.trace("Scheduling a sync at interval NO_SYNC_NODE_FOUND."); 1.145 + sync_interval = NO_SYNC_NODE_INTERVAL; 1.146 + } 1.147 + this.scheduleNextSync(sync_interval); 1.148 + break; 1.149 + case "weave:engine:sync:finish": 1.150 + if (data == "clients") { 1.151 + // Update the client mode because it might change what we sync. 1.152 + this.updateClientMode(); 1.153 + } 1.154 + break; 1.155 + case "weave:engine:sync:error": 1.156 + // `subject` is the exception thrown by an engine's sync() method. 1.157 + let exception = subject; 1.158 + if (exception.status >= 500 && exception.status <= 504) { 1.159 + this.requiresBackoff = true; 1.160 + } 1.161 + break; 1.162 + case "weave:service:login:error": 1.163 + this.clearSyncTriggers(); 1.164 + 1.165 + if (Status.login == MASTER_PASSWORD_LOCKED) { 1.166 + // Try again later, just as if we threw an error... only without the 1.167 + // error count. 1.168 + this._log.debug("Couldn't log in: master password is locked."); 1.169 + this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"); 1.170 + this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); 1.171 + } else if (this._fatalLoginStatus.indexOf(Status.login) == -1) { 1.172 + // Not a fatal login error, just an intermittent network or server 1.173 + // issue. Keep on syncin'. 1.174 + this.checkSyncStatus(); 1.175 + } 1.176 + break; 1.177 + case "weave:service:logout:finish": 1.178 + // Start or cancel the sync timer depending on if 1.179 + // logged in or logged out 1.180 + this.checkSyncStatus(); 1.181 + break; 1.182 + case "weave:service:sync:error": 1.183 + // There may be multiple clients but if the sync fails, client mode 1.184 + // should still be updated so that the next sync has a correct interval. 1.185 + this.updateClientMode(); 1.186 + this.adjustSyncInterval(); 1.187 + this.nextSync = 0; 1.188 + this.handleSyncError(); 1.189 + break; 1.190 + case "FxA:hawk:backoff:interval": 1.191 + case "weave:service:backoff:interval": 1.192 + let requested_interval = subject * 1000; 1.193 + this._log.debug("Got backoff notification: " + requested_interval + "ms"); 1.194 + // Leave up to 25% more time for the back off. 1.195 + let interval = requested_interval * (1 + Math.random() * 0.25); 1.196 + Status.backoffInterval = interval; 1.197 + Status.minimumNextSync = Date.now() + requested_interval; 1.198 + this._log.debug("Fuzzed minimum next sync: " + Status.minimumNextSync); 1.199 + break; 1.200 + case "weave:service:ready": 1.201 + // Applications can specify this preference if they want autoconnect 1.202 + // to happen after a fixed delay. 1.203 + let delay = Svc.Prefs.get("autoconnectDelay"); 1.204 + if (delay) { 1.205 + this.delayedAutoConnect(delay); 1.206 + } 1.207 + break; 1.208 + case "weave:engine:sync:applied": 1.209 + let numItems = subject.succeeded; 1.210 + this._log.trace("Engine " + data + " successfully applied " + numItems + 1.211 + " items."); 1.212 + if (numItems) { 1.213 + this.hasIncomingItems = true; 1.214 + } 1.215 + break; 1.216 + case "weave:service:setup-complete": 1.217 + Services.prefs.savePrefFile(null); 1.218 + Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); 1.219 + break; 1.220 + case "weave:service:start-over": 1.221 + this.setDefaults(); 1.222 + try { 1.223 + Svc.Idle.removeIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); 1.224 + } catch (ex if (ex.result == Cr.NS_ERROR_FAILURE)) { 1.225 + // In all likelihood we didn't have an idle observer registered yet. 1.226 + // It's all good. 1.227 + } 1.228 + break; 1.229 + case "idle": 1.230 + this._log.trace("We're idle."); 1.231 + this.idle = true; 1.232 + // Adjust the interval for future syncs. This won't actually have any 1.233 + // effect until the next pending sync (which will happen soon since we 1.234 + // were just active.) 1.235 + this.adjustSyncInterval(); 1.236 + break; 1.237 + case "active": 1.238 + this._log.trace("Received notification that we're back from idle."); 1.239 + this.idle = false; 1.240 + Utils.namedTimer(function onBack() { 1.241 + if (this.idle) { 1.242 + this._log.trace("... and we're idle again. " + 1.243 + "Ignoring spurious back notification."); 1.244 + return; 1.245 + } 1.246 + 1.247 + this._log.trace("Genuine return from idle. Syncing."); 1.248 + // Trigger a sync if we have multiple clients. 1.249 + if (this.numClients > 1) { 1.250 + this.scheduleNextSync(0); 1.251 + } 1.252 + }, IDLE_OBSERVER_BACK_DELAY, this, "idleDebouncerTimer"); 1.253 + break; 1.254 + } 1.255 + }, 1.256 + 1.257 + adjustSyncInterval: function adjustSyncInterval() { 1.258 + if (Status.eol) { 1.259 + this._log.debug("Server status is EOL; using eolInterval."); 1.260 + this.syncInterval = this.eolInterval; 1.261 + return; 1.262 + } 1.263 + 1.264 + if (this.numClients <= 1) { 1.265 + this._log.trace("Adjusting syncInterval to singleDeviceInterval."); 1.266 + this.syncInterval = this.singleDeviceInterval; 1.267 + return; 1.268 + } 1.269 + 1.270 + // Only MULTI_DEVICE clients will enter this if statement 1.271 + // since SINGLE_USER clients will be handled above. 1.272 + if (this.idle) { 1.273 + this._log.trace("Adjusting syncInterval to idleInterval."); 1.274 + this.syncInterval = this.idleInterval; 1.275 + return; 1.276 + } 1.277 + 1.278 + if (this.hasIncomingItems) { 1.279 + this._log.trace("Adjusting syncInterval to immediateInterval."); 1.280 + this.hasIncomingItems = false; 1.281 + this.syncInterval = this.immediateInterval; 1.282 + } else { 1.283 + this._log.trace("Adjusting syncInterval to activeInterval."); 1.284 + this.syncInterval = this.activeInterval; 1.285 + } 1.286 + }, 1.287 + 1.288 + calculateScore: function calculateScore() { 1.289 + let engines = [this.service.clientsEngine].concat(this.service.engineManager.getEnabled()); 1.290 + for (let i = 0;i < engines.length;i++) { 1.291 + this._log.trace(engines[i].name + ": score: " + engines[i].score); 1.292 + this.globalScore += engines[i].score; 1.293 + engines[i]._tracker.resetScore(); 1.294 + } 1.295 + 1.296 + this._log.trace("Global score updated: " + this.globalScore); 1.297 + this.checkSyncStatus(); 1.298 + }, 1.299 + 1.300 + /** 1.301 + * Process the locally stored clients list to figure out what mode to be in 1.302 + */ 1.303 + updateClientMode: function updateClientMode() { 1.304 + // Nothing to do if it's the same amount 1.305 + let numClients = this.service.clientsEngine.stats.numClients; 1.306 + if (this.numClients == numClients) 1.307 + return; 1.308 + 1.309 + this._log.debug("Client count: " + this.numClients + " -> " + numClients); 1.310 + this.numClients = numClients; 1.311 + 1.312 + if (numClients <= 1) { 1.313 + this._log.trace("Adjusting syncThreshold to SINGLE_USER_THRESHOLD"); 1.314 + this.syncThreshold = SINGLE_USER_THRESHOLD; 1.315 + } else { 1.316 + this._log.trace("Adjusting syncThreshold to MULTI_DEVICE_THRESHOLD"); 1.317 + this.syncThreshold = MULTI_DEVICE_THRESHOLD; 1.318 + } 1.319 + this.adjustSyncInterval(); 1.320 + }, 1.321 + 1.322 + /** 1.323 + * Check if we should be syncing and schedule the next sync, if it's not scheduled 1.324 + */ 1.325 + checkSyncStatus: function checkSyncStatus() { 1.326 + // Should we be syncing now, if not, cancel any sync timers and return 1.327 + // if we're in backoff, we'll schedule the next sync. 1.328 + let ignore = [kSyncBackoffNotMet, kSyncMasterPasswordLocked]; 1.329 + let skip = this.service._checkSync(ignore); 1.330 + this._log.trace("_checkSync returned \"" + skip + "\"."); 1.331 + if (skip) { 1.332 + this.clearSyncTriggers(); 1.333 + return; 1.334 + } 1.335 + 1.336 + // Only set the wait time to 0 if we need to sync right away 1.337 + let wait; 1.338 + if (this.globalScore > this.syncThreshold) { 1.339 + this._log.debug("Global Score threshold hit, triggering sync."); 1.340 + wait = 0; 1.341 + } 1.342 + this.scheduleNextSync(wait); 1.343 + }, 1.344 + 1.345 + /** 1.346 + * Call sync() if Master Password is not locked. 1.347 + * 1.348 + * Otherwise, reschedule a sync for later. 1.349 + */ 1.350 + syncIfMPUnlocked: function syncIfMPUnlocked() { 1.351 + // No point if we got kicked out by the master password dialog. 1.352 + if (Status.login == MASTER_PASSWORD_LOCKED && 1.353 + Utils.mpLocked()) { 1.354 + this._log.debug("Not initiating sync: Login status is " + Status.login); 1.355 + 1.356 + // If we're not syncing now, we need to schedule the next one. 1.357 + this._log.trace("Scheduling a sync at MASTER_PASSWORD_LOCKED_RETRY_INTERVAL"); 1.358 + this.scheduleAtInterval(MASTER_PASSWORD_LOCKED_RETRY_INTERVAL); 1.359 + return; 1.360 + } 1.361 + 1.362 + Utils.nextTick(this.service.sync, this.service); 1.363 + }, 1.364 + 1.365 + /** 1.366 + * Set a timer for the next sync 1.367 + */ 1.368 + scheduleNextSync: function scheduleNextSync(interval) { 1.369 + // If no interval was specified, use the current sync interval. 1.370 + if (interval == null) { 1.371 + interval = this.syncInterval; 1.372 + } 1.373 + 1.374 + // Ensure the interval is set to no less than the backoff. 1.375 + if (Status.backoffInterval && interval < Status.backoffInterval) { 1.376 + this._log.trace("Requested interval " + interval + 1.377 + " ms is smaller than the backoff interval. " + 1.378 + "Using backoff interval " + 1.379 + Status.backoffInterval + " ms instead."); 1.380 + interval = Status.backoffInterval; 1.381 + } 1.382 + 1.383 + if (this.nextSync != 0) { 1.384 + // There's already a sync scheduled. Don't reschedule if there's already 1.385 + // a timer scheduled for sooner than requested. 1.386 + let currentInterval = this.nextSync - Date.now(); 1.387 + this._log.trace("There's already a sync scheduled in " + 1.388 + currentInterval + " ms."); 1.389 + if (currentInterval < interval && this.syncTimer) { 1.390 + this._log.trace("Ignoring scheduling request for next sync in " + 1.391 + interval + " ms."); 1.392 + return; 1.393 + } 1.394 + } 1.395 + 1.396 + // Start the sync right away if we're already late. 1.397 + if (interval <= 0) { 1.398 + this._log.trace("Requested sync should happen right away."); 1.399 + this.syncIfMPUnlocked(); 1.400 + return; 1.401 + } 1.402 + 1.403 + this._log.debug("Next sync in " + interval + " ms."); 1.404 + Utils.namedTimer(this.syncIfMPUnlocked, interval, this, "syncTimer"); 1.405 + 1.406 + // Save the next sync time in-case sync is disabled (logout/offline/etc.) 1.407 + this.nextSync = Date.now() + interval; 1.408 + }, 1.409 + 1.410 + 1.411 + /** 1.412 + * Incorporates the backoff/retry logic used in error handling and elective 1.413 + * non-syncing. 1.414 + */ 1.415 + scheduleAtInterval: function scheduleAtInterval(minimumInterval) { 1.416 + let interval = Utils.calculateBackoff(this._syncErrors, 1.417 + MINIMUM_BACKOFF_INTERVAL, 1.418 + Status.backoffInterval); 1.419 + if (minimumInterval) { 1.420 + interval = Math.max(minimumInterval, interval); 1.421 + } 1.422 + 1.423 + this._log.debug("Starting client-initiated backoff. Next sync in " + 1.424 + interval + " ms."); 1.425 + this.scheduleNextSync(interval); 1.426 + }, 1.427 + 1.428 + /** 1.429 + * Automatically start syncing after the given delay (in seconds). 1.430 + * 1.431 + * Applications can define the `services.sync.autoconnectDelay` preference 1.432 + * to have this called automatically during start-up with the pref value as 1.433 + * the argument. Alternatively, they can call it themselves to control when 1.434 + * Sync should first start to sync. 1.435 + */ 1.436 + delayedAutoConnect: function delayedAutoConnect(delay) { 1.437 + if (this.service._checkSetup() == STATUS_OK) { 1.438 + Utils.namedTimer(this.autoConnect, delay * 1000, this, "_autoTimer"); 1.439 + } 1.440 + }, 1.441 + 1.442 + autoConnect: function autoConnect() { 1.443 + if (this.service._checkSetup() == STATUS_OK && !this.service._checkSync()) { 1.444 + // Schedule a sync based on when a previous sync was scheduled. 1.445 + // scheduleNextSync() will do the right thing if that time lies in 1.446 + // the past. 1.447 + this.scheduleNextSync(this.nextSync - Date.now()); 1.448 + } 1.449 + 1.450 + // Once autoConnect is called we no longer need _autoTimer. 1.451 + if (this._autoTimer) { 1.452 + this._autoTimer.clear(); 1.453 + } 1.454 + }, 1.455 + 1.456 + _syncErrors: 0, 1.457 + /** 1.458 + * Deal with sync errors appropriately 1.459 + */ 1.460 + handleSyncError: function handleSyncError() { 1.461 + this._log.trace("In handleSyncError. Error count: " + this._syncErrors); 1.462 + this._syncErrors++; 1.463 + 1.464 + // Do nothing on the first couple of failures, if we're not in 1.465 + // backoff due to 5xx errors. 1.466 + if (!Status.enforceBackoff) { 1.467 + if (this._syncErrors < MAX_ERROR_COUNT_BEFORE_BACKOFF) { 1.468 + this.scheduleNextSync(); 1.469 + return; 1.470 + } 1.471 + this._log.debug("Sync error count has exceeded " + 1.472 + MAX_ERROR_COUNT_BEFORE_BACKOFF + "; enforcing backoff."); 1.473 + Status.enforceBackoff = true; 1.474 + } 1.475 + 1.476 + this.scheduleAtInterval(); 1.477 + }, 1.478 + 1.479 + 1.480 + /** 1.481 + * Remove any timers/observers that might trigger a sync 1.482 + */ 1.483 + clearSyncTriggers: function clearSyncTriggers() { 1.484 + this._log.debug("Clearing sync triggers and the global score."); 1.485 + this.globalScore = this.nextSync = 0; 1.486 + 1.487 + // Clear out any scheduled syncs 1.488 + if (this.syncTimer) 1.489 + this.syncTimer.clear(); 1.490 + }, 1.491 +}; 1.492 + 1.493 +const LOG_PREFIX_SUCCESS = "success-"; 1.494 +const LOG_PREFIX_ERROR = "error-"; 1.495 + 1.496 +this.ErrorHandler = function ErrorHandler(service) { 1.497 + this.service = service; 1.498 + this.init(); 1.499 +} 1.500 +ErrorHandler.prototype = { 1.501 + MINIMUM_ALERT_INTERVAL_MSEC: 604800000, // One week. 1.502 + 1.503 + /** 1.504 + * Flag that turns on error reporting for all errors, incl. network errors. 1.505 + */ 1.506 + dontIgnoreErrors: false, 1.507 + 1.508 + /** 1.509 + * Flag that indicates if we have already reported a prolonged failure. 1.510 + * Once set, we don't report it again, meaning this error is only reported 1.511 + * one per run. 1.512 + */ 1.513 + didReportProlongedError: false, 1.514 + 1.515 + init: function init() { 1.516 + Svc.Obs.add("weave:engine:sync:applied", this); 1.517 + Svc.Obs.add("weave:engine:sync:error", this); 1.518 + Svc.Obs.add("weave:service:login:error", this); 1.519 + Svc.Obs.add("weave:service:sync:error", this); 1.520 + Svc.Obs.add("weave:service:sync:finish", this); 1.521 + 1.522 + this.initLogs(); 1.523 + }, 1.524 + 1.525 + initLogs: function initLogs() { 1.526 + this._log = Log.repository.getLogger("Sync.ErrorHandler"); 1.527 + this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")]; 1.528 + this._cleaningUpFileLogs = false; 1.529 + 1.530 + let root = Log.repository.getLogger("Sync"); 1.531 + root.level = Log.Level[Svc.Prefs.get("log.rootLogger")]; 1.532 + 1.533 + let formatter = new Log.BasicFormatter(); 1.534 + let capp = new Log.ConsoleAppender(formatter); 1.535 + capp.level = Log.Level[Svc.Prefs.get("log.appender.console")]; 1.536 + root.addAppender(capp); 1.537 + 1.538 + let dapp = new Log.DumpAppender(formatter); 1.539 + dapp.level = Log.Level[Svc.Prefs.get("log.appender.dump")]; 1.540 + root.addAppender(dapp); 1.541 + 1.542 + let fapp = this._logAppender = new Log.StorageStreamAppender(formatter); 1.543 + fapp.level = Log.Level[Svc.Prefs.get("log.appender.file.level")]; 1.544 + root.addAppender(fapp); 1.545 + 1.546 + // Arrange for the FxA logs to also go to our file. 1.547 + Log.repository.getLogger("FirefoxAccounts").addAppender(fapp); 1.548 + }, 1.549 + 1.550 + observe: function observe(subject, topic, data) { 1.551 + this._log.trace("Handling " + topic); 1.552 + switch(topic) { 1.553 + case "weave:engine:sync:applied": 1.554 + if (subject.newFailed) { 1.555 + // An engine isn't able to apply one or more incoming records. 1.556 + // We don't fail hard on this, but it usually indicates a bug, 1.557 + // so for now treat it as sync error (c.f. Service._syncEngine()) 1.558 + Status.engines = [data, ENGINE_APPLY_FAIL]; 1.559 + this._log.debug(data + " failed to apply some records."); 1.560 + } 1.561 + break; 1.562 + case "weave:engine:sync:error": 1.563 + let exception = subject; // exception thrown by engine's sync() method 1.564 + let engine_name = data; // engine name that threw the exception 1.565 + 1.566 + this.checkServerError(exception); 1.567 + 1.568 + Status.engines = [engine_name, exception.failureCode || ENGINE_UNKNOWN_FAIL]; 1.569 + this._log.debug(engine_name + " failed: " + Utils.exceptionStr(exception)); 1.570 + break; 1.571 + case "weave:service:login:error": 1.572 + this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"), 1.573 + LOG_PREFIX_ERROR); 1.574 + 1.575 + if (this.shouldReportError()) { 1.576 + this.notifyOnNextTick("weave:ui:login:error"); 1.577 + } else { 1.578 + this.notifyOnNextTick("weave:ui:clear-error"); 1.579 + } 1.580 + 1.581 + this.dontIgnoreErrors = false; 1.582 + break; 1.583 + case "weave:service:sync:error": 1.584 + if (Status.sync == CREDENTIALS_CHANGED) { 1.585 + this.service.logout(); 1.586 + } 1.587 + 1.588 + this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"), 1.589 + LOG_PREFIX_ERROR); 1.590 + 1.591 + if (this.shouldReportError()) { 1.592 + this.notifyOnNextTick("weave:ui:sync:error"); 1.593 + } else { 1.594 + this.notifyOnNextTick("weave:ui:sync:finish"); 1.595 + } 1.596 + 1.597 + this.dontIgnoreErrors = false; 1.598 + break; 1.599 + case "weave:service:sync:finish": 1.600 + this._log.trace("Status.service is " + Status.service); 1.601 + 1.602 + // Check both of these status codes: in the event of a failure in one 1.603 + // engine, Status.service will be SYNC_FAILED_PARTIAL despite 1.604 + // Status.sync being SYNC_SUCCEEDED. 1.605 + // *facepalm* 1.606 + if (Status.sync == SYNC_SUCCEEDED && 1.607 + Status.service == STATUS_OK) { 1.608 + // Great. Let's clear our mid-sync 401 note. 1.609 + this._log.trace("Clearing lastSyncReassigned."); 1.610 + Svc.Prefs.reset("lastSyncReassigned"); 1.611 + } 1.612 + 1.613 + if (Status.service == SYNC_FAILED_PARTIAL) { 1.614 + this._log.debug("Some engines did not sync correctly."); 1.615 + this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnError"), 1.616 + LOG_PREFIX_ERROR); 1.617 + 1.618 + if (this.shouldReportError()) { 1.619 + this.dontIgnoreErrors = false; 1.620 + this.notifyOnNextTick("weave:ui:sync:error"); 1.621 + break; 1.622 + } 1.623 + } else { 1.624 + this.resetFileLog(Svc.Prefs.get("log.appender.file.logOnSuccess"), 1.625 + LOG_PREFIX_SUCCESS); 1.626 + } 1.627 + this.dontIgnoreErrors = false; 1.628 + this.notifyOnNextTick("weave:ui:sync:finish"); 1.629 + break; 1.630 + } 1.631 + }, 1.632 + 1.633 + notifyOnNextTick: function notifyOnNextTick(topic) { 1.634 + Utils.nextTick(function() { 1.635 + this._log.trace("Notifying " + topic + 1.636 + ". Status.login is " + Status.login + 1.637 + ". Status.sync is " + Status.sync); 1.638 + Svc.Obs.notify(topic); 1.639 + }, this); 1.640 + }, 1.641 + 1.642 + /** 1.643 + * Trigger a sync and don't muffle any errors, particularly network errors. 1.644 + */ 1.645 + syncAndReportErrors: function syncAndReportErrors() { 1.646 + this._log.debug("Beginning user-triggered sync."); 1.647 + 1.648 + this.dontIgnoreErrors = true; 1.649 + Utils.nextTick(this.service.sync, this.service); 1.650 + }, 1.651 + 1.652 + /** 1.653 + * Finds all logs older than maxErrorAge and deletes them without tying up I/O. 1.654 + */ 1.655 + cleanupLogs: function cleanupLogs() { 1.656 + let direntries = FileUtils.getDir("ProfD", ["weave", "logs"]).directoryEntries; 1.657 + let oldLogs = []; 1.658 + let index = 0; 1.659 + let threshold = Date.now() - 1000 * Svc.Prefs.get("log.appender.file.maxErrorAge"); 1.660 + 1.661 + this._log.debug("Log cleanup threshold time: " + threshold); 1.662 + while (direntries.hasMoreElements()) { 1.663 + let logFile = direntries.getNext().QueryInterface(Ci.nsIFile); 1.664 + if (logFile.lastModifiedTime < threshold) { 1.665 + this._log.trace(" > Noting " + logFile.leafName + 1.666 + " for cleanup (" + logFile.lastModifiedTime + ")"); 1.667 + oldLogs.push(logFile); 1.668 + } 1.669 + } 1.670 + 1.671 + // Deletes a file from oldLogs each tick until there are none left. 1.672 + let errorHandler = this; 1.673 + function deleteFile() { 1.674 + if (index >= oldLogs.length) { 1.675 + errorHandler._log.debug("Done deleting files."); 1.676 + errorHandler._cleaningUpFileLogs = false; 1.677 + Svc.Obs.notify("weave:service:cleanup-logs"); 1.678 + return; 1.679 + } 1.680 + try { 1.681 + let file = oldLogs[index]; 1.682 + file.remove(false); 1.683 + errorHandler._log.trace("Deleted " + file.leafName + "."); 1.684 + } catch (ex) { 1.685 + errorHandler._log._debug("Encountered error trying to clean up old log file '" 1.686 + + oldLogs[index].leafName + "':" 1.687 + + Utils.exceptionStr(ex)); 1.688 + } 1.689 + index++; 1.690 + Utils.nextTick(deleteFile); 1.691 + } 1.692 + 1.693 + if (oldLogs.length > 0) { 1.694 + this._cleaningUpFileLogs = true; 1.695 + Utils.nextTick(deleteFile); 1.696 + } else { 1.697 + this._log.debug("No logs to clean up."); 1.698 + } 1.699 + }, 1.700 + 1.701 + /** 1.702 + * Generate a log file for the sync that just completed 1.703 + * and refresh the input & output streams. 1.704 + * 1.705 + * @param flushToFile 1.706 + * the log file to be flushed/reset 1.707 + * 1.708 + * @param filenamePrefix 1.709 + * a value of either LOG_PREFIX_SUCCESS or LOG_PREFIX_ERROR 1.710 + * to be used as the log filename prefix 1.711 + */ 1.712 + resetFileLog: function resetFileLog(flushToFile, filenamePrefix) { 1.713 + let inStream = this._logAppender.getInputStream(); 1.714 + this._logAppender.reset(); 1.715 + if (flushToFile && inStream) { 1.716 + this._log.debug("Flushing file log."); 1.717 + try { 1.718 + let filename = filenamePrefix + Date.now() + ".txt"; 1.719 + let file = FileUtils.getFile("ProfD", ["weave", "logs", filename]); 1.720 + let outStream = FileUtils.openFileOutputStream(file); 1.721 + 1.722 + this._log.trace("Beginning stream copy to " + file.leafName + ": " + 1.723 + Date.now()); 1.724 + NetUtil.asyncCopy(inStream, outStream, function onCopyComplete() { 1.725 + this._log.trace("onCopyComplete: " + Date.now()); 1.726 + this._log.trace("Output file timestamp: " + file.lastModifiedTime); 1.727 + Svc.Obs.notify("weave:service:reset-file-log"); 1.728 + this._log.trace("Notified: " + Date.now()); 1.729 + if (filenamePrefix == LOG_PREFIX_ERROR && 1.730 + !this._cleaningUpFileLogs) { 1.731 + this._log.trace("Scheduling cleanup."); 1.732 + Utils.nextTick(this.cleanupLogs, this); 1.733 + } 1.734 + }.bind(this)); 1.735 + } catch (ex) { 1.736 + Svc.Obs.notify("weave:service:reset-file-log"); 1.737 + } 1.738 + } else { 1.739 + Svc.Obs.notify("weave:service:reset-file-log"); 1.740 + } 1.741 + }, 1.742 + 1.743 + /** 1.744 + * Translates server error codes to meaningful strings. 1.745 + * 1.746 + * @param code 1.747 + * server error code as an integer 1.748 + */ 1.749 + errorStr: function errorStr(code) { 1.750 + switch (code.toString()) { 1.751 + case "1": 1.752 + return "illegal-method"; 1.753 + case "2": 1.754 + return "invalid-captcha"; 1.755 + case "3": 1.756 + return "invalid-username"; 1.757 + case "4": 1.758 + return "cannot-overwrite-resource"; 1.759 + case "5": 1.760 + return "userid-mismatch"; 1.761 + case "6": 1.762 + return "json-parse-failure"; 1.763 + case "7": 1.764 + return "invalid-password"; 1.765 + case "8": 1.766 + return "invalid-record"; 1.767 + case "9": 1.768 + return "weak-password"; 1.769 + default: 1.770 + return "generic-server-error"; 1.771 + } 1.772 + }, 1.773 + 1.774 + shouldReportError: function shouldReportError() { 1.775 + if (Status.login == MASTER_PASSWORD_LOCKED) { 1.776 + this._log.trace("shouldReportError: false (master password locked)."); 1.777 + return false; 1.778 + } 1.779 + 1.780 + if (this.dontIgnoreErrors) { 1.781 + return true; 1.782 + } 1.783 + 1.784 + let lastSync = Svc.Prefs.get("lastSync"); 1.785 + if (lastSync && ((Date.now() - Date.parse(lastSync)) > 1.786 + Svc.Prefs.get("errorhandler.networkFailureReportTimeout") * 1000)) { 1.787 + Status.sync = PROLONGED_SYNC_FAILURE; 1.788 + if (this.didReportProlongedError) { 1.789 + this._log.trace("shouldReportError: false (prolonged sync failure, but" + 1.790 + " we've already reported it)."); 1.791 + return false; 1.792 + } 1.793 + this._log.trace("shouldReportError: true (first prolonged sync failure)."); 1.794 + this.didReportProlongedError = true; 1.795 + return true; 1.796 + } 1.797 + 1.798 + // We got a 401 mid-sync. Wait for the next sync before actually handling 1.799 + // an error. This assumes that we'll get a 401 again on a login fetch in 1.800 + // order to report the error. 1.801 + if (!this.service.clusterURL) { 1.802 + this._log.trace("shouldReportError: false (no cluster URL; " + 1.803 + "possible node reassignment)."); 1.804 + return false; 1.805 + } 1.806 + 1.807 + return ([Status.login, Status.sync].indexOf(SERVER_MAINTENANCE) == -1 && 1.808 + [Status.login, Status.sync].indexOf(LOGIN_FAILED_NETWORK_ERROR) == -1); 1.809 + }, 1.810 + 1.811 + get currentAlertMode() { 1.812 + return Svc.Prefs.get("errorhandler.alert.mode"); 1.813 + }, 1.814 + 1.815 + set currentAlertMode(str) { 1.816 + return Svc.Prefs.set("errorhandler.alert.mode", str); 1.817 + }, 1.818 + 1.819 + get earliestNextAlert() { 1.820 + return Svc.Prefs.get("errorhandler.alert.earliestNext", 0) * 1000; 1.821 + }, 1.822 + 1.823 + set earliestNextAlert(msec) { 1.824 + return Svc.Prefs.set("errorhandler.alert.earliestNext", msec / 1000); 1.825 + }, 1.826 + 1.827 + clearServerAlerts: function () { 1.828 + // If we have any outstanding alerts, apparently they're no longer relevant. 1.829 + Svc.Prefs.resetBranch("errorhandler.alert"); 1.830 + }, 1.831 + 1.832 + /** 1.833 + * X-Weave-Alert headers can include a JSON object: 1.834 + * 1.835 + * { 1.836 + * "code": // One of "hard-eol", "soft-eol". 1.837 + * "url": // For "Learn more" link. 1.838 + * "message": // Logged in Sync logs. 1.839 + * } 1.840 + */ 1.841 + handleServerAlert: function (xwa) { 1.842 + if (!xwa.code) { 1.843 + this._log.warn("Got structured X-Weave-Alert, but no alert code."); 1.844 + return; 1.845 + } 1.846 + 1.847 + switch (xwa.code) { 1.848 + // Gently and occasionally notify the user that this service will be 1.849 + // shutting down. 1.850 + case "soft-eol": 1.851 + // Fall through. 1.852 + 1.853 + // Tell the user that this service has shut down, and drop our syncing 1.854 + // frequency dramatically. 1.855 + case "hard-eol": 1.856 + // Note that both of these alerts should be subservient to future "sign 1.857 + // in with your Firefox Account" storage alerts. 1.858 + if ((this.currentAlertMode != xwa.code) || 1.859 + (this.earliestNextAlert < Date.now())) { 1.860 + Utils.nextTick(function() { 1.861 + Svc.Obs.notify("weave:eol", xwa); 1.862 + }, this); 1.863 + this._log.error("X-Weave-Alert: " + xwa.code + ": " + xwa.message); 1.864 + this.earliestNextAlert = Date.now() + this.MINIMUM_ALERT_INTERVAL_MSEC; 1.865 + this.currentAlertMode = xwa.code; 1.866 + } 1.867 + break; 1.868 + default: 1.869 + this._log.debug("Got unexpected X-Weave-Alert code: " + xwa.code); 1.870 + } 1.871 + }, 1.872 + 1.873 + /** 1.874 + * Handle HTTP response results or exceptions and set the appropriate 1.875 + * Status.* bits. 1.876 + * 1.877 + * This method also looks for "side-channel" warnings. 1.878 + */ 1.879 + checkServerError: function (resp) { 1.880 + switch (resp.status) { 1.881 + case 200: 1.882 + case 404: 1.883 + case 513: 1.884 + let xwa = resp.headers['x-weave-alert']; 1.885 + 1.886 + // Only process machine-readable alerts. 1.887 + if (!xwa || !xwa.startsWith("{")) { 1.888 + this.clearServerAlerts(); 1.889 + return; 1.890 + } 1.891 + 1.892 + try { 1.893 + xwa = JSON.parse(xwa); 1.894 + } catch (ex) { 1.895 + this._log.warn("Malformed X-Weave-Alert from server: " + xwa); 1.896 + return; 1.897 + } 1.898 + 1.899 + this.handleServerAlert(xwa); 1.900 + break; 1.901 + 1.902 + case 400: 1.903 + if (resp == RESPONSE_OVER_QUOTA) { 1.904 + Status.sync = OVER_QUOTA; 1.905 + } 1.906 + break; 1.907 + 1.908 + case 401: 1.909 + this.service.logout(); 1.910 + this._log.info("Got 401 response; resetting clusterURL."); 1.911 + Svc.Prefs.reset("clusterURL"); 1.912 + 1.913 + let delay = 0; 1.914 + if (Svc.Prefs.get("lastSyncReassigned")) { 1.915 + // We got a 401 in the middle of the previous sync, and we just got 1.916 + // another. Login must have succeeded in order for us to get here, so 1.917 + // the password should be correct. 1.918 + // This is likely to be an intermittent server issue, so back off and 1.919 + // give it time to recover. 1.920 + this._log.warn("Last sync also failed for 401. Delaying next sync."); 1.921 + delay = MINIMUM_BACKOFF_INTERVAL; 1.922 + } else { 1.923 + this._log.debug("New mid-sync 401 failure. Making a note."); 1.924 + Svc.Prefs.set("lastSyncReassigned", true); 1.925 + } 1.926 + this._log.info("Attempting to schedule another sync."); 1.927 + this.service.scheduler.scheduleNextSync(delay); 1.928 + break; 1.929 + 1.930 + case 500: 1.931 + case 502: 1.932 + case 503: 1.933 + case 504: 1.934 + Status.enforceBackoff = true; 1.935 + if (resp.status == 503 && resp.headers["retry-after"]) { 1.936 + let retryAfter = resp.headers["retry-after"]; 1.937 + this._log.debug("Got Retry-After: " + retryAfter); 1.938 + if (this.service.isLoggedIn) { 1.939 + Status.sync = SERVER_MAINTENANCE; 1.940 + } else { 1.941 + Status.login = SERVER_MAINTENANCE; 1.942 + } 1.943 + Svc.Obs.notify("weave:service:backoff:interval", 1.944 + parseInt(retryAfter, 10)); 1.945 + } 1.946 + break; 1.947 + } 1.948 + 1.949 + switch (resp.result) { 1.950 + case Cr.NS_ERROR_UNKNOWN_HOST: 1.951 + case Cr.NS_ERROR_CONNECTION_REFUSED: 1.952 + case Cr.NS_ERROR_NET_TIMEOUT: 1.953 + case Cr.NS_ERROR_NET_RESET: 1.954 + case Cr.NS_ERROR_NET_INTERRUPT: 1.955 + case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED: 1.956 + // The constant says it's about login, but in fact it just 1.957 + // indicates general network error. 1.958 + if (this.service.isLoggedIn) { 1.959 + Status.sync = LOGIN_FAILED_NETWORK_ERROR; 1.960 + } else { 1.961 + Status.login = LOGIN_FAILED_NETWORK_ERROR; 1.962 + } 1.963 + break; 1.964 + } 1.965 + }, 1.966 +};