enginesync.sys.mjs (14017B)
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 /** 6 * This file contains code for synchronizing engines. 7 */ 8 9 import { Log } from "resource://gre/modules/Log.sys.mjs"; 10 11 import { 12 ABORT_SYNC_COMMAND, 13 LOGIN_FAILED_NETWORK_ERROR, 14 NO_SYNC_NODE_FOUND, 15 STATUS_OK, 16 SYNC_FAILED_PARTIAL, 17 SYNC_SUCCEEDED, 18 WEAVE_VERSION, 19 kSyncNetworkOffline, 20 } from "resource://services-sync/constants.sys.mjs"; 21 22 import { Svc, Utils } from "resource://services-sync/util.sys.mjs"; 23 24 import { Async } from "resource://services-common/async.sys.mjs"; 25 26 const lazy = {}; 27 ChromeUtils.defineESModuleGetters(lazy, { 28 Doctor: "resource://services-sync/doctor.sys.mjs", 29 }); 30 31 /** 32 * Perform synchronization of engines. 33 * 34 * This was originally split out of service.js. The API needs lots of love. 35 */ 36 export function EngineSynchronizer(service) { 37 this._log = Log.repository.getLogger("Sync.Synchronizer"); 38 this._log.manageLevelFromPref("services.sync.log.logger.synchronizer"); 39 40 this.service = service; 41 } 42 43 EngineSynchronizer.prototype = { 44 async sync(engineNamesToSync, why) { 45 let fastSync = why && why == "sleep"; 46 let startTime = Date.now(); 47 48 this.service.status.resetSync(); 49 50 // Make sure we should sync or record why we shouldn't. 51 let reason = this.service._checkSync(); 52 if (reason) { 53 if (reason == kSyncNetworkOffline) { 54 this.service.status.sync = LOGIN_FAILED_NETWORK_ERROR; 55 } 56 57 // this is a purposeful abort rather than a failure, so don't set 58 // any status bits 59 reason = "Can't sync: " + reason; 60 throw new Error(reason); 61 } 62 63 // If we don't have a node, get one. If that fails, retry in 10 minutes. 64 if ( 65 !this.service.clusterURL && 66 !(await this.service.identity.setCluster()) 67 ) { 68 this.service.status.sync = NO_SYNC_NODE_FOUND; 69 this._log.info("No cluster URL found. Cannot sync."); 70 return; 71 } 72 73 // Ping the server with a special info request once a day. 74 let infoURL = this.service.infoURL; 75 let now = Math.floor(Date.now() / 1000); 76 let lastPing = Svc.PrefBranch.getIntPref("lastPing", 0); 77 if (now - lastPing > 86400) { 78 // 60 * 60 * 24 79 infoURL += "?v=" + WEAVE_VERSION; 80 Svc.PrefBranch.setIntPref("lastPing", now); 81 } 82 83 let engineManager = this.service.engineManager; 84 85 // Figure out what the last modified time is for each collection 86 let info = await this.service._fetchInfo(infoURL); 87 88 // Convert the response to an object and read out the modified times 89 for (let engine of [this.service.clientsEngine].concat( 90 engineManager.getAll() 91 )) { 92 engine.lastModified = info.obj[engine.name] || 0; 93 } 94 95 if (!(await this.service._remoteSetup(info, !fastSync))) { 96 throw new Error("Aborting sync, remote setup failed"); 97 } 98 99 if (!fastSync) { 100 // Make sure we have an up-to-date list of clients before sending commands 101 this._log.debug("Refreshing client list."); 102 if (!(await this._syncEngine(this.service.clientsEngine))) { 103 // Clients is an engine like any other; it can fail with a 401, 104 // and we can elect to abort the sync. 105 this._log.warn("Client engine sync failed. Aborting."); 106 return; 107 } 108 } 109 110 // We only honor the "hint" of what engines to Sync if this isn't 111 // a first sync. 112 let allowEnginesHint = false; 113 // Wipe data in the desired direction if necessary 114 switch (Svc.PrefBranch.getStringPref("firstSync", null)) { 115 case "resetClient": 116 await this.service.resetClient(engineManager.enabledEngineNames); 117 break; 118 case "wipeClient": 119 await this.service.wipeClient(engineManager.enabledEngineNames); 120 break; 121 case "wipeRemote": 122 await this.service.wipeRemote(engineManager.enabledEngineNames); 123 break; 124 default: 125 allowEnginesHint = true; 126 break; 127 } 128 129 if (!fastSync && this.service.clientsEngine.localCommands) { 130 try { 131 if (!(await this.service.clientsEngine.processIncomingCommands())) { 132 this.service.status.sync = ABORT_SYNC_COMMAND; 133 throw new Error("Processed command aborted sync."); 134 } 135 136 // Repeat remoteSetup in-case the commands forced us to reset 137 if (!(await this.service._remoteSetup(info))) { 138 throw new Error("Remote setup failed after processing commands."); 139 } 140 } finally { 141 // Always immediately attempt to push back the local client (now 142 // without commands). 143 // Note that we don't abort here; if there's a 401 because we've 144 // been reassigned, we'll handle it around another engine. 145 await this._syncEngine(this.service.clientsEngine); 146 } 147 } 148 149 // Update engines because it might change what we sync. 150 try { 151 await this._updateEnabledEngines(); 152 } catch (ex) { 153 this._log.debug("Updating enabled engines failed", ex); 154 this.service.errorHandler.checkServerError(ex); 155 throw ex; 156 } 157 158 await this.service.engineManager.switchAlternatives(); 159 160 // If the engines to sync has been specified, we sync in the order specified. 161 let enginesToSync; 162 if (allowEnginesHint && engineNamesToSync) { 163 this._log.info("Syncing specified engines", engineNamesToSync); 164 enginesToSync = engineManager 165 .get(engineNamesToSync) 166 .filter(e => e.enabled); 167 } else { 168 this._log.info("Syncing all enabled engines."); 169 enginesToSync = engineManager.getEnabled(); 170 } 171 try { 172 // We don't bother validating engines that failed to sync. 173 let enginesToValidate = []; 174 for (let engine of enginesToSync) { 175 if (engine.shouldSkipSync(why)) { 176 this._log.info(`Engine ${engine.name} asked to be skipped`); 177 continue; 178 } 179 // If there's any problems with syncing the engine, report the failure 180 if ( 181 !(await this._syncEngine(engine)) || 182 this.service.status.enforceBackoff 183 ) { 184 this._log.info("Aborting sync for failure in " + engine.name); 185 break; 186 } 187 enginesToValidate.push(engine); 188 } 189 190 // If _syncEngine fails for a 401, we might not have a cluster URL here. 191 // If that's the case, break out of this immediately, rather than 192 // throwing an exception when trying to fetch metaURL. 193 if (!this.service.clusterURL) { 194 this._log.debug( 195 "Aborting sync, no cluster URL: not uploading new meta/global." 196 ); 197 return; 198 } 199 200 // Upload meta/global if any engines changed anything. 201 let meta = await this.service.recordManager.get(this.service.metaURL); 202 if (meta.isNew || meta.changed) { 203 this._log.info("meta/global changed locally: reuploading."); 204 try { 205 await this.service.uploadMetaGlobal(meta); 206 delete meta.isNew; 207 delete meta.changed; 208 } catch (error) { 209 this._log.error( 210 "Unable to upload meta/global. Leaving marked as new." 211 ); 212 } 213 } 214 215 if (!fastSync) { 216 await lazy.Doctor.consult(enginesToValidate); 217 } 218 219 // If there were no sync engine failures 220 if (this.service.status.service != SYNC_FAILED_PARTIAL) { 221 this.service.status.sync = SYNC_SUCCEEDED; 222 } 223 224 // Even if there were engine failures, bump lastSync even on partial since 225 // it's reflected in the UI (bug 1439777). 226 if ( 227 this.service.status.service == SYNC_FAILED_PARTIAL || 228 this.service.status.service == STATUS_OK 229 ) { 230 Svc.PrefBranch.setStringPref("lastSync", new Date().toString()); 231 } 232 } finally { 233 Svc.PrefBranch.clearUserPref("firstSync"); 234 235 let syncTime = ((Date.now() - startTime) / 1000).toFixed(2); 236 let dateStr = Utils.formatTimestamp(new Date()); 237 this._log.info( 238 "Sync completed at " + dateStr + " after " + syncTime + " secs." 239 ); 240 } 241 }, 242 243 // Returns true if sync should proceed. 244 // false / no return value means sync should be aborted. 245 async _syncEngine(engine) { 246 try { 247 await engine.sync(); 248 } catch (e) { 249 if (e.status == 401) { 250 // Maybe a 401, cluster update perhaps needed? 251 // We rely on ErrorHandler observing the sync failure notification to 252 // schedule another sync and clear node assignment values. 253 // Here we simply want to muffle the exception and return an 254 // appropriate value. 255 return false; 256 } 257 // Note that policies.js has already logged info about the exception... 258 if (Async.isShutdownException(e)) { 259 // Failure due to a shutdown exception should prevent other engines 260 // trying to start and immediately failing. 261 this._log.info( 262 `${engine.name} was interrupted by shutdown; no other engines will sync` 263 ); 264 return false; 265 } 266 } 267 268 return true; 269 }, 270 271 async _updateEnabledFromMeta( 272 meta, 273 numClients, 274 engineManager = this.service.engineManager 275 ) { 276 this._log.info("Updating enabled engines: " + numClients + " clients."); 277 278 if (meta.isNew || !meta.payload.engines) { 279 this._log.debug( 280 "meta/global isn't new, or is missing engines. Not updating enabled state." 281 ); 282 return; 283 } 284 285 // If we're the only client and the server has no data for us 286 // (neither enabled *nor* declined engines), just keep our local state. 287 // Belt-and-suspenders approach to Bug 615926. 288 let hasEnabledEngines = false; 289 for (let e in meta.payload.engines) { 290 if (e != "clients") { 291 hasEnabledEngines = true; 292 break; 293 } 294 } 295 296 let hasDeclinedEngines = 297 Array.isArray(meta.payload.declined) && meta.payload.declined.length; 298 299 if (numClients <= 1 && !hasEnabledEngines && !hasDeclinedEngines) { 300 this._log.info( 301 "One client and neither enabled nor declined engines on server: " + 302 "not touching local engine status." 303 ); 304 return; 305 } 306 307 this.service._ignorePrefObserver = true; 308 309 let enabled = engineManager.enabledEngineNames; 310 311 let toDecline = new Set(); 312 let toUndecline = new Set(); 313 314 for (let engineName in meta.payload.engines) { 315 if (engineName == "clients") { 316 // Clients is special. 317 continue; 318 } 319 let index = enabled.indexOf(engineName); 320 if (index != -1) { 321 // The engine is enabled locally. Nothing to do. 322 enabled.splice(index, 1); 323 continue; 324 } 325 let engine = engineManager.get(engineName); 326 if (!engine) { 327 // The engine doesn't exist locally. Nothing to do. 328 continue; 329 } 330 331 let attemptedEnable = false; 332 // If the engine was enabled remotely, enable it locally. 333 if ( 334 !Svc.PrefBranch.getBoolPref( 335 "engineStatusChanged." + engine.prefName, 336 false 337 ) 338 ) { 339 this._log.trace( 340 "Engine " + engineName + " was enabled. Marking as non-declined." 341 ); 342 toUndecline.add(engineName); 343 this._log.trace(engineName + " engine was enabled remotely."); 344 engine.enabled = true; 345 // Note that setting engine.enabled to true might not have worked for 346 // the password engine if a master-password is enabled. However, it's 347 // still OK that we added it to undeclined - the user *tried* to enable 348 // it remotely - so it still winds up as not being flagged as declined 349 // even though it's disabled remotely. 350 attemptedEnable = true; 351 } 352 353 // If either the engine was disabled locally or enabling the engine 354 // failed (see above re master-password) then wipe server data and 355 // disable it everywhere. 356 if (!engine.enabled) { 357 this._log.trace("Wiping data for " + engineName + " engine."); 358 await engine.wipeServer(); 359 delete meta.payload.engines[engineName]; 360 meta.changed = true; // the new enabled state must propagate 361 // We also here mark the engine as declined, because the pref 362 // was explicitly changed to false - unless we tried, and failed, 363 // to enable it - in which case we leave the declined state alone. 364 if (!attemptedEnable) { 365 // This will be reflected in meta/global in the next stage. 366 this._log.trace( 367 "Engine " + 368 engineName + 369 " was disabled locally. Marking as declined." 370 ); 371 toDecline.add(engineName); 372 } 373 } 374 } 375 376 // Any remaining engines were either enabled locally or disabled remotely. 377 for (let engineName of enabled) { 378 let engine = engineManager.get(engineName); 379 if ( 380 Svc.PrefBranch.getBoolPref( 381 "engineStatusChanged." + engine.prefName, 382 false 383 ) 384 ) { 385 this._log.trace("The " + engineName + " engine was enabled locally."); 386 toUndecline.add(engineName); 387 } else { 388 this._log.trace("The " + engineName + " engine was disabled remotely."); 389 390 // Don't automatically mark it as declined! 391 try { 392 engine.enabled = false; 393 } catch (e) { 394 this._log.trace("Failed to disable engine " + engineName); 395 } 396 } 397 } 398 399 engineManager.decline(toDecline); 400 engineManager.undecline(toUndecline); 401 402 for (const pref of Svc.PrefBranch.getChildList("engineStatusChanged.")) { 403 Svc.PrefBranch.clearUserPref(pref); 404 } 405 this.service._ignorePrefObserver = false; 406 }, 407 408 async _updateEnabledEngines() { 409 let meta = await this.service.recordManager.get(this.service.metaURL); 410 let numClients = this.service.scheduler.numClients; 411 let engineManager = this.service.engineManager; 412 413 await this._updateEnabledFromMeta(meta, numClients, engineManager); 414 }, 415 }; 416 Object.freeze(EngineSynchronizer.prototype);