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 };