2012-05-21 15:12:37 +04:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
/**
|
|
|
|
* How does the clients engine work?
|
|
|
|
*
|
|
|
|
* - We use 2 files - commands.json and commands-syncing.json.
|
|
|
|
*
|
|
|
|
* - At sync upload time, we attempt a rename of commands.json to
|
|
|
|
* commands-syncing.json, and ignore errors (helps for crash during sync!).
|
|
|
|
* - We load commands-syncing.json and stash the contents in
|
|
|
|
* _currentlySyncingCommands which lives for the duration of the upload process.
|
|
|
|
* - We use _currentlySyncingCommands to build the outgoing records
|
|
|
|
* - Immediately after successful upload, we delete commands-syncing.json from
|
|
|
|
* disk (and clear _currentlySyncingCommands). We reconcile our local records
|
|
|
|
* with what we just wrote in the server, and add failed IDs commands
|
|
|
|
* back in commands.json
|
|
|
|
* - Any time we need to "save" a command for future syncs, we load
|
|
|
|
* commands.json, update it, and write it back out.
|
|
|
|
*/
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
2012-08-30 01:43:41 +04:00
|
|
|
"ClientEngine",
|
|
|
|
"ClientsRec"
|
|
|
|
];
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2015-10-07 15:03:21 +03:00
|
|
|
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2016-01-13 07:55:00 +03:00
|
|
|
Cu.import("resource://services-common/async.js");
|
2012-04-06 10:26:06 +04:00
|
|
|
Cu.import("resource://services-common/stringbundle.js");
|
2010-06-17 01:30:08 +04:00
|
|
|
Cu.import("resource://services-sync/constants.js");
|
|
|
|
Cu.import("resource://services-sync/engines.js");
|
2011-01-19 03:23:30 +03:00
|
|
|
Cu.import("resource://services-sync/record.js");
|
2016-04-15 19:00:59 +03:00
|
|
|
Cu.import("resource://services-sync/resource.js");
|
2010-06-17 01:30:08 +04:00
|
|
|
Cu.import("resource://services-sync/util.js");
|
2016-01-12 04:21:27 +03:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2016-01-13 07:55:00 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
|
|
|
|
"resource://gre/modules/FxAccounts.jsm");
|
|
|
|
|
2011-01-19 03:23:20 +03:00
|
|
|
const CLIENTS_TTL = 1814400; // 21 days
|
2011-01-15 00:22:20 +03:00
|
|
|
const CLIENTS_TTL_REFRESH = 604800; // 7 days
|
2016-04-15 19:00:59 +03:00
|
|
|
const STALE_CLIENT_REMOTE_AGE = 604800; // 7 days
|
2011-01-15 00:22:20 +03:00
|
|
|
|
2014-02-11 06:36:02 +04:00
|
|
|
const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"];
|
2014-01-07 09:45:26 +04:00
|
|
|
|
2016-04-01 20:55:10 +03:00
|
|
|
function hasDupeCommand(commands, action) {
|
2016-04-05 01:39:37 +03:00
|
|
|
if (!commands) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-04-01 20:55:10 +03:00
|
|
|
return commands.some(other => other.command == action.command &&
|
|
|
|
Utils.deepEquals(other.args, action.args));
|
|
|
|
}
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.ClientsRec = function ClientsRec(collection, id) {
|
2011-01-19 03:23:20 +03:00
|
|
|
CryptoWrapper.call(this, collection, id);
|
|
|
|
}
|
|
|
|
ClientsRec.prototype = {
|
|
|
|
__proto__: CryptoWrapper.prototype,
|
2011-06-13 22:42:18 +04:00
|
|
|
_logName: "Sync.Record.Clients",
|
2011-01-19 03:23:20 +03:00
|
|
|
ttl: CLIENTS_TTL
|
|
|
|
};
|
|
|
|
|
2014-11-18 06:06:00 +03:00
|
|
|
Utils.deferGetSet(ClientsRec,
|
|
|
|
"cleartext",
|
|
|
|
["name", "type", "commands",
|
|
|
|
"version", "protocols",
|
2016-08-02 20:09:30 +03:00
|
|
|
"formfactor", "os", "appPackage", "application", "device",
|
|
|
|
"fxaDeviceId"]);
|
2011-01-19 03:23:20 +03:00
|
|
|
|
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.ClientEngine = function ClientEngine(service) {
|
2012-08-30 01:43:41 +04:00
|
|
|
SyncEngine.call(this, "Clients", service);
|
2010-02-12 02:25:31 +03:00
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
// Reset the last sync timestamp on every startup so that we fetch all clients
|
|
|
|
this.resetLastSync();
|
2009-01-07 00:54:18 +03:00
|
|
|
}
|
|
|
|
ClientEngine.prototype = {
|
|
|
|
__proto__: SyncEngine.prototype,
|
|
|
|
_storeObj: ClientStore,
|
2010-03-17 02:39:08 +03:00
|
|
|
_recordObj: ClientsRec,
|
2011-07-27 08:48:50 +04:00
|
|
|
_trackerObj: ClientsTracker,
|
2016-12-01 01:28:52 +03:00
|
|
|
allowSkippedRecord: false,
|
2010-03-17 02:39:08 +03:00
|
|
|
|
2010-05-06 04:16:17 +04:00
|
|
|
// Always sync client data as it controls other sync behavior
|
2015-09-23 12:40:53 +03:00
|
|
|
get enabled() {
|
|
|
|
return true;
|
|
|
|
},
|
2010-05-06 04:16:17 +04:00
|
|
|
|
2011-01-15 00:22:20 +03:00
|
|
|
get lastRecordUpload() {
|
|
|
|
return Svc.Prefs.get(this.name + ".lastRecordUpload", 0);
|
|
|
|
},
|
|
|
|
set lastRecordUpload(value) {
|
|
|
|
Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value));
|
|
|
|
},
|
|
|
|
|
2016-06-28 02:46:43 +03:00
|
|
|
get remoteClients() {
|
2016-07-19 09:30:01 +03:00
|
|
|
// return all non-stale clients for external consumption.
|
|
|
|
return Object.values(this._store._remoteClients).filter(v => !v.stale);
|
|
|
|
},
|
|
|
|
|
|
|
|
remoteClientExists(id) {
|
|
|
|
let client = this._store._remoteClients[id];
|
|
|
|
return !!(client && !client.stale);
|
2016-06-28 02:46:43 +03:00
|
|
|
},
|
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
// Aggregate some stats on the composition of clients on this account
|
|
|
|
get stats() {
|
|
|
|
let stats = {
|
2016-01-13 07:55:00 +03:00
|
|
|
hasMobile: this.localType == DEVICE_TYPE_MOBILE,
|
2010-03-17 02:39:08 +03:00
|
|
|
names: [this.localName],
|
|
|
|
numClients: 1,
|
|
|
|
};
|
|
|
|
|
2015-10-18 21:52:58 +03:00
|
|
|
for (let id in this._store._remoteClients) {
|
2016-07-19 09:30:01 +03:00
|
|
|
let {name, type, stale} = this._store._remoteClients[id];
|
|
|
|
if (!stale) {
|
|
|
|
stats.hasMobile = stats.hasMobile || type == DEVICE_TYPE_MOBILE;
|
|
|
|
stats.names.push(name);
|
|
|
|
stats.numClients++;
|
|
|
|
}
|
2010-03-17 02:39:08 +03:00
|
|
|
}
|
2009-01-24 02:09:21 +03:00
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
return stats;
|
2009-01-24 02:09:21 +03:00
|
|
|
},
|
|
|
|
|
2014-02-05 20:08:14 +04:00
|
|
|
/**
|
|
|
|
* Obtain information about device types.
|
|
|
|
*
|
|
|
|
* Returns a Map of device types to integer counts.
|
|
|
|
*/
|
|
|
|
get deviceTypes() {
|
|
|
|
let counts = new Map();
|
|
|
|
|
|
|
|
counts.set(this.localType, 1);
|
|
|
|
|
2015-10-18 21:52:58 +03:00
|
|
|
for (let id in this._store._remoteClients) {
|
|
|
|
let record = this._store._remoteClients[id];
|
2016-07-19 09:30:01 +03:00
|
|
|
if (record.stale) {
|
|
|
|
continue; // pretend "stale" records don't exist.
|
|
|
|
}
|
2014-02-05 20:08:14 +04:00
|
|
|
let type = record.type;
|
|
|
|
if (!counts.has(type)) {
|
|
|
|
counts.set(type, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
counts.set(type, counts.get(type) + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return counts;
|
|
|
|
},
|
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
get localID() {
|
|
|
|
// Generate a random GUID id we don't have one
|
|
|
|
let localID = Svc.Prefs.get("client.GUID", "");
|
|
|
|
return localID == "" ? this.localID = Utils.makeGUID() : localID;
|
2009-01-24 02:09:21 +03:00
|
|
|
},
|
2015-09-23 12:40:53 +03:00
|
|
|
set localID(value) {
|
|
|
|
Svc.Prefs.set("client.GUID", value);
|
|
|
|
},
|
2009-01-24 02:09:21 +03:00
|
|
|
|
2014-11-18 06:06:00 +03:00
|
|
|
get brandName() {
|
|
|
|
let brand = new StringBundle("chrome://branding/locale/brand.properties");
|
|
|
|
return brand.get("brandShortName");
|
|
|
|
},
|
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
get localName() {
|
2016-04-06 03:44:39 +03:00
|
|
|
let name = Utils.getDeviceName();
|
|
|
|
// If `getDeviceName` returns the default name, set the pref. FxA registers
|
|
|
|
// the device before syncing, so we don't need to update the registration
|
|
|
|
// in this case.
|
|
|
|
Svc.Prefs.set("client.name", name);
|
|
|
|
return name;
|
2009-11-20 10:31:04 +03:00
|
|
|
},
|
2015-09-23 12:40:53 +03:00
|
|
|
set localName(value) {
|
|
|
|
Svc.Prefs.set("client.name", value);
|
2016-04-06 03:44:39 +03:00
|
|
|
// Update the registration in the background.
|
|
|
|
fxAccounts.updateDeviceRegistration().catch(error => {
|
|
|
|
this._log.warn("failed to update fxa device registration", error);
|
|
|
|
});
|
2015-09-23 12:40:53 +03:00
|
|
|
},
|
2010-03-17 02:39:08 +03:00
|
|
|
|
2015-09-23 12:40:53 +03:00
|
|
|
get localType() {
|
2016-01-13 07:55:00 +03:00
|
|
|
return Utils.getDeviceType();
|
2015-09-23 12:40:53 +03:00
|
|
|
},
|
|
|
|
set localType(value) {
|
|
|
|
Svc.Prefs.set("client.type", value);
|
|
|
|
},
|
2009-02-27 09:36:14 +03:00
|
|
|
|
2016-04-11 21:10:40 +03:00
|
|
|
getClientName(id) {
|
|
|
|
if (id == this.localID) {
|
|
|
|
return this.localName;
|
|
|
|
}
|
|
|
|
let client = this._store._remoteClients[id];
|
|
|
|
return client ? client.name : "";
|
|
|
|
},
|
|
|
|
|
2016-08-02 20:09:30 +03:00
|
|
|
getClientFxaDeviceId(id) {
|
|
|
|
if (this._store._remoteClients[id]) {
|
|
|
|
return this._store._remoteClients[id].fxaDeviceId;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2010-03-13 03:14:09 +03:00
|
|
|
isMobile: function isMobile(id) {
|
2010-04-29 06:20:08 +04:00
|
|
|
if (this._store._remoteClients[id])
|
2016-01-13 07:55:00 +03:00
|
|
|
return this._store._remoteClients[id].type == DEVICE_TYPE_MOBILE;
|
2010-04-29 06:20:08 +04:00
|
|
|
return false;
|
2010-03-13 03:14:09 +03:00
|
|
|
},
|
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
_readCommands() {
|
|
|
|
let cb = Async.makeSpinningCallback();
|
|
|
|
Utils.jsonLoad("commands", this, commands => cb(null, commands));
|
|
|
|
return cb.wait() || {};
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Low level function, do not use directly (use _addClientCommand instead).
|
|
|
|
*/
|
|
|
|
_saveCommands(commands) {
|
|
|
|
let cb = Async.makeSpinningCallback();
|
|
|
|
Utils.jsonSave("commands", this, commands, error => {
|
|
|
|
if (error) {
|
|
|
|
this._log.error("Failed to save JSON outgoing commands", error);
|
|
|
|
}
|
|
|
|
cb();
|
|
|
|
});
|
|
|
|
cb.wait();
|
|
|
|
},
|
|
|
|
|
|
|
|
_prepareCommandsForUpload() {
|
|
|
|
let cb = Async.makeSpinningCallback();
|
|
|
|
Utils.jsonMove("commands", "commands-syncing", this).catch(() => {}) // Ignore errors
|
|
|
|
.then(() => {
|
|
|
|
Utils.jsonLoad("commands-syncing", this, commands => cb(null, commands));
|
|
|
|
});
|
|
|
|
return cb.wait() || {};
|
|
|
|
},
|
|
|
|
|
|
|
|
_deleteUploadedCommands() {
|
|
|
|
delete this._currentlySyncingCommands;
|
|
|
|
Async.promiseSpinningly(
|
|
|
|
Utils.jsonRemove("commands-syncing", this).catch(err => {
|
|
|
|
this._log.error("Failed to delete syncing-commands file", err);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
_addClientCommand(clientId, command) {
|
|
|
|
const allCommands = this._readCommands();
|
|
|
|
const clientCommands = allCommands[clientId] || [];
|
|
|
|
if (hasDupeCommand(clientCommands, command)) {
|
2016-11-04 04:46:57 +03:00
|
|
|
return false;
|
2016-08-11 21:01:15 +03:00
|
|
|
}
|
|
|
|
allCommands[clientId] = clientCommands.concat(command);
|
|
|
|
this._saveCommands(allCommands);
|
2016-11-04 04:46:57 +03:00
|
|
|
return true;
|
2016-08-11 21:01:15 +03:00
|
|
|
},
|
|
|
|
|
2017-01-25 01:05:12 +03:00
|
|
|
_removeClientCommands(clientId) {
|
|
|
|
const allCommands = this._readCommands();
|
|
|
|
delete allCommands[clientId];
|
|
|
|
this._saveCommands(allCommands);
|
|
|
|
},
|
|
|
|
|
2011-01-15 00:22:20 +03:00
|
|
|
_syncStartup: function _syncStartup() {
|
|
|
|
// Reupload new client record periodically.
|
|
|
|
if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) {
|
|
|
|
this._tracker.addChangedID(this.localID);
|
|
|
|
this.lastRecordUpload = Date.now() / 1000;
|
|
|
|
}
|
|
|
|
SyncEngine.prototype._syncStartup.call(this);
|
|
|
|
},
|
|
|
|
|
2016-04-01 20:55:10 +03:00
|
|
|
_processIncoming() {
|
|
|
|
// Fetch all records from the server.
|
|
|
|
this.lastSync = 0;
|
2016-04-15 19:00:59 +03:00
|
|
|
this._incomingClients = {};
|
2016-04-01 20:55:10 +03:00
|
|
|
try {
|
|
|
|
SyncEngine.prototype._processIncoming.call(this);
|
|
|
|
// Since clients are synced unconditionally, any records in the local store
|
|
|
|
// that don't exist on the server must be for disconnected clients. Remove
|
|
|
|
// them, so that we don't upload records with commands for clients that will
|
|
|
|
// never see them. We also do this to filter out stale clients from the
|
|
|
|
// tabs collection, since showing their list of tabs is confusing.
|
2016-04-15 19:00:59 +03:00
|
|
|
for (let id in this._store._remoteClients) {
|
|
|
|
if (!this._incomingClients[id]) {
|
|
|
|
this._log.info(`Removing local state for deleted client ${id}`);
|
|
|
|
this._removeRemoteClient(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Bug 1264498: Mobile clients don't remove themselves from the clients
|
2016-07-19 09:30:01 +03:00
|
|
|
// collection when the user disconnects Sync, so we mark as stale clients
|
2016-04-15 19:00:59 +03:00
|
|
|
// with the same name that haven't synced in over a week.
|
2016-07-19 09:30:01 +03:00
|
|
|
// (Note we can't simply delete them, or we re-apply them next sync - see
|
|
|
|
// bug 1287687)
|
2016-04-15 19:00:59 +03:00
|
|
|
delete this._incomingClients[this.localID];
|
|
|
|
let names = new Set([this.localName]);
|
|
|
|
for (let id in this._incomingClients) {
|
|
|
|
let record = this._store._remoteClients[id];
|
|
|
|
if (!names.has(record.name)) {
|
|
|
|
names.add(record.name);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let remoteAge = AsyncResource.serverTime - this._incomingClients[id];
|
|
|
|
if (remoteAge > STALE_CLIENT_REMOTE_AGE) {
|
|
|
|
this._log.info(`Hiding stale client ${id} with age ${remoteAge}`);
|
2016-07-19 09:30:01 +03:00
|
|
|
record.stale = true;
|
2016-04-15 19:00:59 +03:00
|
|
|
}
|
2016-04-01 20:55:10 +03:00
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
this._incomingClients = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-04-05 01:39:37 +03:00
|
|
|
_uploadOutgoing() {
|
2016-08-11 21:01:15 +03:00
|
|
|
this._currentlySyncingCommands = this._prepareCommandsForUpload();
|
|
|
|
const clientWithPendingCommands = Object.keys(this._currentlySyncingCommands);
|
|
|
|
for (let clientId of clientWithPendingCommands) {
|
|
|
|
if (this._store._remoteClients[clientId] || this.localID == clientId) {
|
2016-09-06 21:39:13 +03:00
|
|
|
this._modified.set(clientId, 0);
|
2016-08-11 21:01:15 +03:00
|
|
|
}
|
|
|
|
}
|
2016-04-05 01:39:37 +03:00
|
|
|
SyncEngine.prototype._uploadOutgoing.call(this);
|
|
|
|
},
|
|
|
|
|
2016-08-02 20:09:30 +03:00
|
|
|
_onRecordsWritten(succeeded, failed) {
|
2016-08-11 21:01:15 +03:00
|
|
|
// Reconcile the status of the local records with what we just wrote on the
|
|
|
|
// server
|
|
|
|
for (let id of succeeded) {
|
|
|
|
const commandChanges = this._currentlySyncingCommands[id];
|
|
|
|
if (id == this.localID) {
|
|
|
|
if (this.localCommands) {
|
|
|
|
this.localCommands = this.localCommands.filter(command => !hasDupeCommand(commandChanges, command));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const clientRecord = this._store._remoteClients[id];
|
|
|
|
if (!commandChanges || !clientRecord) {
|
|
|
|
// should be impossible, else we wouldn't have been writing it.
|
|
|
|
this._log.warn("No command/No record changes for a client we uploaded");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// fixup the client record, so our copy of _remoteClients matches what we uploaded.
|
|
|
|
clientRecord.commands = this._store.createRecord(id);
|
|
|
|
// we could do better and pass the reference to the record we just uploaded,
|
|
|
|
// but this will do for now
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Re-add failed commands
|
|
|
|
for (let id of failed) {
|
|
|
|
const commandChanges = this._currentlySyncingCommands[id];
|
|
|
|
if (!commandChanges) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this._addClientCommand(id, commandChanges);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._deleteUploadedCommands();
|
|
|
|
|
2016-08-02 20:09:30 +03:00
|
|
|
// Notify other devices that their own client collection changed
|
|
|
|
const idsToNotify = succeeded.reduce((acc, id) => {
|
|
|
|
if (id == this.localID) {
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
const fxaDeviceId = this.getClientFxaDeviceId(id);
|
|
|
|
return fxaDeviceId ? acc.concat(fxaDeviceId) : acc;
|
|
|
|
}, []);
|
|
|
|
if (idsToNotify.length > 0) {
|
|
|
|
this._notifyCollectionChanged(idsToNotify);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_notifyCollectionChanged(ids) {
|
|
|
|
const message = {
|
|
|
|
version: 1,
|
|
|
|
command: "sync:collection_changed",
|
|
|
|
data: {
|
|
|
|
collections: ["clients"]
|
|
|
|
}
|
|
|
|
};
|
|
|
|
fxAccounts.notifyDevices(ids, message, NOTIFY_TAB_SENT_TTL_SECS);
|
|
|
|
},
|
|
|
|
|
2016-01-12 04:21:27 +03:00
|
|
|
_syncFinish() {
|
2016-04-21 01:43:36 +03:00
|
|
|
// Record histograms for our device types, and also write them to a pref
|
|
|
|
// so non-histogram telemetry (eg, UITelemetry) has easy access to them.
|
2016-01-12 04:21:27 +03:00
|
|
|
for (let [deviceType, count] of this.deviceTypes) {
|
|
|
|
let hid;
|
2016-04-21 01:43:36 +03:00
|
|
|
let prefName = this.name + ".devices.";
|
2016-01-12 04:21:27 +03:00
|
|
|
switch (deviceType) {
|
|
|
|
case "desktop":
|
|
|
|
hid = "WEAVE_DEVICE_COUNT_DESKTOP";
|
2016-04-21 01:43:36 +03:00
|
|
|
prefName += "desktop";
|
2016-01-12 04:21:27 +03:00
|
|
|
break;
|
|
|
|
case "mobile":
|
|
|
|
hid = "WEAVE_DEVICE_COUNT_MOBILE";
|
2016-04-21 01:43:36 +03:00
|
|
|
prefName += "mobile";
|
2016-01-12 04:21:27 +03:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this._log.warn(`Unexpected deviceType "${deviceType}" recording device telemetry.`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Services.telemetry.getHistogramById(hid).add(count);
|
2016-04-21 01:43:36 +03:00
|
|
|
Svc.Prefs.set(prefName, count);
|
2016-01-12 04:21:27 +03:00
|
|
|
}
|
|
|
|
SyncEngine.prototype._syncFinish.call(this);
|
|
|
|
},
|
|
|
|
|
2016-04-01 20:55:10 +03:00
|
|
|
_reconcile: function _reconcile(item) {
|
|
|
|
// Every incoming record is reconciled, so we use this to track the
|
|
|
|
// contents of the collection on the server.
|
2016-04-15 19:00:59 +03:00
|
|
|
this._incomingClients[item.id] = item.modified;
|
2016-04-01 20:55:10 +03:00
|
|
|
|
|
|
|
if (!this._store.itemExists(item.id)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// Clients are synced unconditionally, so we'll always have new records.
|
|
|
|
// Unfortunately, this will cause the scheduler to use the immediate sync
|
|
|
|
// interval for the multi-device case, instead of the active interval. We
|
|
|
|
// work around this by updating the record during reconciliation, and
|
|
|
|
// returning false to indicate that the record doesn't need to be applied
|
|
|
|
// later.
|
|
|
|
this._store.update(item);
|
|
|
|
return false;
|
2009-12-11 05:39:51 +03:00
|
|
|
},
|
|
|
|
|
2010-02-20 00:36:42 +03:00
|
|
|
// Treat reset the same as wiping for locally cached clients
|
2015-01-25 10:50:01 +03:00
|
|
|
_resetClient() {
|
|
|
|
this._wipeClient();
|
|
|
|
},
|
2010-02-20 00:36:42 +03:00
|
|
|
|
|
|
|
_wipeClient: function _wipeClient() {
|
2009-12-03 01:44:17 +03:00
|
|
|
SyncEngine.prototype._resetClient.call(this);
|
2016-04-05 01:39:37 +03:00
|
|
|
delete this.localCommands;
|
2009-02-27 09:36:14 +03:00
|
|
|
this._store.wipe();
|
2016-08-11 21:01:15 +03:00
|
|
|
const logRemoveError = err => this._log.warn("Could not delete json file", err);
|
|
|
|
Async.promiseSpinningly(
|
|
|
|
Utils.jsonRemove("commands", this).catch(logRemoveError)
|
|
|
|
.then(Utils.jsonRemove("commands-syncing", this).catch(logRemoveError))
|
|
|
|
);
|
2011-03-01 22:56:29 +03:00
|
|
|
},
|
2011-04-09 01:51:55 +04:00
|
|
|
|
|
|
|
removeClientData: function removeClientData() {
|
2012-09-15 03:02:32 +04:00
|
|
|
let res = this.service.resource(this.engineURL + "/" + this.localID);
|
2011-04-09 01:51:55 +04:00
|
|
|
res.delete();
|
|
|
|
},
|
|
|
|
|
2011-03-01 22:56:29 +03:00
|
|
|
// Override the default behavior to delete bad records from the server.
|
2011-03-21 02:10:40 +03:00
|
|
|
handleHMACMismatch: function handleHMACMismatch(item, mayRetry) {
|
2011-03-01 22:56:29 +03:00
|
|
|
this._log.debug("Handling HMAC mismatch for " + item.id);
|
2011-07-27 08:48:50 +04:00
|
|
|
|
2011-03-21 02:10:40 +03:00
|
|
|
let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry);
|
|
|
|
if (base != SyncEngine.kRecoveryStrategy.error)
|
|
|
|
return base;
|
2011-03-01 22:56:29 +03:00
|
|
|
|
|
|
|
// It's a bad client record. Save it to be deleted at the end of the sync.
|
|
|
|
this._log.debug("Bad client record detected. Scheduling for deletion.");
|
|
|
|
this._deleteId(item.id);
|
|
|
|
|
2011-03-21 02:10:40 +03:00
|
|
|
// Neither try again nor error; we're going to delete it.
|
|
|
|
return SyncEngine.kRecoveryStrategy.ignore;
|
2011-08-05 03:19:02 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A hash of valid commands that the client knows about. The key is a command
|
|
|
|
* and the value is a hash containing information about the command such as
|
|
|
|
* number of arguments and description.
|
|
|
|
*/
|
|
|
|
_commands: {
|
|
|
|
resetAll: { args: 0, desc: "Clear temporary local data for all engines" },
|
|
|
|
resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
|
|
|
|
wipeAll: { args: 0, desc: "Delete all client data for all engines" },
|
|
|
|
wipeEngine: { args: 1, desc: "Delete all client data for engine" },
|
2011-08-09 20:23:55 +04:00
|
|
|
logout: { args: 0, desc: "Log out client" },
|
2012-03-27 21:13:52 +04:00
|
|
|
displayURI: { args: 3, desc: "Instruct a client to display a URI" },
|
2011-08-05 03:19:02 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a command+args pair to a specific client.
|
|
|
|
*
|
|
|
|
* @param command Command string
|
|
|
|
* @param args Array of arguments/data for command
|
|
|
|
* @param clientId Client to send command to
|
|
|
|
*/
|
2016-11-04 04:46:57 +03:00
|
|
|
_sendCommandToClient: function sendCommandToClient(command, args, clientId, flowID = null) {
|
2011-08-05 03:19:02 +04:00
|
|
|
this._log.trace("Sending " + command + " to " + clientId);
|
|
|
|
|
|
|
|
let client = this._store._remoteClients[clientId];
|
|
|
|
if (!client) {
|
|
|
|
throw new Error("Unknown remote client ID: '" + clientId + "'.");
|
|
|
|
}
|
2016-07-19 09:30:01 +03:00
|
|
|
if (client.stale) {
|
|
|
|
throw new Error("Stale remote client ID: '" + clientId + "'.");
|
|
|
|
}
|
2011-08-05 03:19:02 +04:00
|
|
|
|
|
|
|
let action = {
|
2017-01-10 20:09:02 +03:00
|
|
|
command,
|
|
|
|
args,
|
2016-11-04 04:46:57 +03:00
|
|
|
flowID: flowID || Utils.makeGUID(), // used for telemetry.
|
2011-08-05 03:19:02 +04:00
|
|
|
};
|
|
|
|
|
2016-11-04 04:46:57 +03:00
|
|
|
if (this._addClientCommand(clientId, action)) {
|
|
|
|
this._log.trace(`Client ${clientId} got a new action`, [command, args]);
|
|
|
|
this._tracker.addChangedID(clientId);
|
|
|
|
let deviceID;
|
|
|
|
try {
|
|
|
|
deviceID = this.service.identity.hashedDeviceID(clientId);
|
|
|
|
} catch (_) {}
|
|
|
|
this.service.recordTelemetryEvent("sendcommand", command, undefined,
|
|
|
|
{ flowID: action.flowID, deviceID });
|
|
|
|
} else {
|
|
|
|
this._log.trace(`Client ${clientId} got a duplicate action`, [command, args]);
|
|
|
|
}
|
2011-08-05 03:19:02 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the local client has any remote commands and perform them.
|
|
|
|
*
|
|
|
|
* @return false to abort sync
|
|
|
|
*/
|
|
|
|
processIncomingCommands: function processIncomingCommands() {
|
2011-08-06 03:01:49 +04:00
|
|
|
return this._notify("clients:process-commands", "", function() {
|
2016-08-11 21:01:15 +03:00
|
|
|
if (!this.localCommands) {
|
2016-08-23 02:17:59 +03:00
|
|
|
return true;
|
|
|
|
}
|
2016-08-11 21:01:15 +03:00
|
|
|
|
|
|
|
const clearedCommands = this._readCommands()[this.localID];
|
|
|
|
const commands = this.localCommands.filter(command => !hasDupeCommand(clearedCommands, command));
|
|
|
|
|
2016-08-23 02:17:59 +03:00
|
|
|
let URIsToDisplay = [];
|
2016-08-11 21:01:15 +03:00
|
|
|
// Process each command in order.
|
|
|
|
for (let rawCommand of commands) {
|
2016-11-04 04:46:57 +03:00
|
|
|
let {command, args, flowID} = rawCommand;
|
2011-08-05 03:19:02 +04:00
|
|
|
this._log.debug("Processing command: " + command + "(" + args + ")");
|
|
|
|
|
2016-11-04 04:46:57 +03:00
|
|
|
this.service.recordTelemetryEvent("processcommand", command, undefined,
|
|
|
|
{ flowID });
|
|
|
|
|
2011-08-05 03:19:02 +04:00
|
|
|
let engines = [args[0]];
|
|
|
|
switch (command) {
|
|
|
|
case "resetAll":
|
|
|
|
engines = null;
|
|
|
|
// Fallthrough
|
|
|
|
case "resetEngine":
|
2012-08-30 01:43:41 +04:00
|
|
|
this.service.resetClient(engines);
|
2011-08-05 03:19:02 +04:00
|
|
|
break;
|
|
|
|
case "wipeAll":
|
|
|
|
engines = null;
|
|
|
|
// Fallthrough
|
|
|
|
case "wipeEngine":
|
2012-08-30 01:43:41 +04:00
|
|
|
this.service.wipeClient(engines);
|
2011-08-05 03:19:02 +04:00
|
|
|
break;
|
|
|
|
case "logout":
|
2012-08-30 01:43:41 +04:00
|
|
|
this.service.logout();
|
2011-08-05 03:19:02 +04:00
|
|
|
return false;
|
2011-08-09 20:23:55 +04:00
|
|
|
case "displayURI":
|
2016-07-07 23:33:29 +03:00
|
|
|
let [uri, clientId, title] = args;
|
|
|
|
URIsToDisplay.push({ uri, clientId, title });
|
2011-08-09 20:23:55 +04:00
|
|
|
break;
|
2011-08-05 03:19:02 +04:00
|
|
|
default:
|
|
|
|
this._log.debug("Received an unknown command: " + command);
|
|
|
|
break;
|
|
|
|
}
|
2016-08-11 21:01:15 +03:00
|
|
|
// Add the command to the "cleared" commands list
|
|
|
|
this._addClientCommand(this.localID, rawCommand)
|
2011-08-05 03:19:02 +04:00
|
|
|
}
|
2016-08-11 21:01:15 +03:00
|
|
|
this._tracker.addChangedID(this.localID);
|
|
|
|
|
2016-07-07 23:33:29 +03:00
|
|
|
if (URIsToDisplay.length) {
|
|
|
|
this._handleDisplayURIs(URIsToDisplay);
|
|
|
|
}
|
2011-08-05 03:19:02 +04:00
|
|
|
|
|
|
|
return true;
|
|
|
|
})();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates and sends a command to a client or all clients.
|
|
|
|
*
|
|
|
|
* Calling this does not actually sync the command data to the server. If the
|
|
|
|
* client already has the command/args pair, it won't receive a duplicate
|
|
|
|
* command.
|
|
|
|
*
|
|
|
|
* @param command
|
|
|
|
* Command to invoke on remote clients
|
|
|
|
* @param args
|
|
|
|
* Array of arguments to give to the command
|
|
|
|
* @param clientId
|
|
|
|
* Client ID to send command to. If undefined, send to all remote
|
|
|
|
* clients.
|
2016-11-04 04:46:57 +03:00
|
|
|
* @param flowID
|
|
|
|
* A unique identifier used to track success for this operation across
|
|
|
|
* devices.
|
2011-08-05 03:19:02 +04:00
|
|
|
*/
|
2016-11-04 04:46:57 +03:00
|
|
|
sendCommand: function sendCommand(command, args, clientId, flowID = null) {
|
2011-08-05 03:19:02 +04:00
|
|
|
let commandData = this._commands[command];
|
|
|
|
// Don't send commands that we don't know about.
|
|
|
|
if (!commandData) {
|
|
|
|
this._log.error("Unknown command to send: " + command);
|
|
|
|
return;
|
2017-01-13 02:24:52 +03:00
|
|
|
} else if (!args || args.length != commandData.args) {
|
|
|
|
// Don't send a command with the wrong number of arguments.
|
2011-08-05 03:19:02 +04:00
|
|
|
this._log.error("Expected " + commandData.args + " args for '" +
|
|
|
|
command + "', but got " + args);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (clientId) {
|
2016-11-04 04:46:57 +03:00
|
|
|
this._sendCommandToClient(command, args, clientId, flowID);
|
2011-08-05 03:19:02 +04:00
|
|
|
} else {
|
2016-08-09 05:40:23 +03:00
|
|
|
for (let [id, record] of Object.entries(this._store._remoteClients)) {
|
2016-07-19 09:30:01 +03:00
|
|
|
if (!record.stale) {
|
2016-11-04 04:46:57 +03:00
|
|
|
this._sendCommandToClient(command, args, id, flowID);
|
2016-07-19 09:30:01 +03:00
|
|
|
}
|
2011-08-05 03:19:02 +04:00
|
|
|
}
|
|
|
|
}
|
2011-08-09 20:23:55 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send a URI to another client for display.
|
|
|
|
*
|
|
|
|
* A side effect is the score is increased dramatically to incur an
|
|
|
|
* immediate sync.
|
|
|
|
*
|
|
|
|
* If an unknown client ID is specified, sendCommand() will throw an
|
|
|
|
* Error object.
|
|
|
|
*
|
|
|
|
* @param uri
|
|
|
|
* URI (as a string) to send and display on the remote client
|
|
|
|
* @param clientId
|
|
|
|
* ID of client to send the command to. If not defined, will be sent
|
|
|
|
* to all remote clients.
|
2012-03-27 21:13:52 +04:00
|
|
|
* @param title
|
|
|
|
* Title of the page being sent.
|
2011-08-09 20:23:55 +04:00
|
|
|
*/
|
2012-03-27 21:13:52 +04:00
|
|
|
sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) {
|
|
|
|
this._log.info("Sending URI to client: " + uri + " -> " +
|
|
|
|
clientId + " (" + title + ")");
|
|
|
|
this.sendCommand("displayURI", [uri, this.localID, title], clientId);
|
2011-08-09 20:23:55 +04:00
|
|
|
|
2012-08-30 01:43:41 +04:00
|
|
|
this._tracker.score += SCORE_INCREMENT_XLARGE;
|
2011-08-09 20:23:55 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-07-07 23:33:29 +03:00
|
|
|
* Handle a bunch of received 'displayURI' commands.
|
2011-08-09 20:23:55 +04:00
|
|
|
*
|
2016-07-07 23:33:29 +03:00
|
|
|
* Interested parties should observe the "weave:engine:clients:display-uris"
|
|
|
|
* topic. The callback will receive an array as the subject parameter
|
|
|
|
* containing objects with the following keys:
|
2011-08-09 20:23:55 +04:00
|
|
|
*
|
2012-03-28 23:53:57 +04:00
|
|
|
* uri URI (string) that is requested for display.
|
|
|
|
* clientId ID of client that sent the command.
|
|
|
|
* title Title of page that loaded URI (likely) corresponds to.
|
2011-08-09 20:23:55 +04:00
|
|
|
*
|
|
|
|
* The 'data' parameter to the callback will not be defined.
|
|
|
|
*
|
2016-07-07 23:33:29 +03:00
|
|
|
* @param uris
|
|
|
|
* An array containing URI objects to display
|
|
|
|
* @param uris[].uri
|
2011-08-09 20:23:55 +04:00
|
|
|
* String URI that was received
|
2016-07-07 23:33:29 +03:00
|
|
|
* @param uris[].clientId
|
2011-08-09 20:23:55 +04:00
|
|
|
* ID of client that sent URI
|
2016-07-07 23:33:29 +03:00
|
|
|
* @param uris[].title
|
2012-03-28 23:53:57 +04:00
|
|
|
* String title of page that URI corresponds to. Older clients may not
|
|
|
|
* send this.
|
2011-08-09 20:23:55 +04:00
|
|
|
*/
|
2016-07-07 23:33:29 +03:00
|
|
|
_handleDisplayURIs: function _handleDisplayURIs(uris) {
|
|
|
|
Svc.Obs.notify("weave:engine:clients:display-uris", uris);
|
2016-04-01 20:55:10 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_removeRemoteClient(id) {
|
|
|
|
delete this._store._remoteClients[id];
|
|
|
|
this._tracker.removeChangedID(id);
|
2017-01-25 01:05:12 +03:00
|
|
|
this._removeClientCommands(id);
|
|
|
|
this._modified.delete(id);
|
2016-04-01 20:55:10 +03:00
|
|
|
},
|
2009-01-07 00:54:18 +03:00
|
|
|
};
|
|
|
|
|
2012-08-30 01:43:41 +04:00
|
|
|
function ClientStore(name, engine) {
|
|
|
|
Store.call(this, name, engine);
|
2009-01-07 00:54:18 +03:00
|
|
|
}
|
|
|
|
ClientStore.prototype = {
|
|
|
|
__proto__: Store.prototype,
|
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
_remoteClients: {},
|
|
|
|
|
2015-01-25 10:50:01 +03:00
|
|
|
create(record) {
|
2016-08-11 21:01:15 +03:00
|
|
|
this.update(record);
|
2015-01-25 10:50:01 +03:00
|
|
|
},
|
2009-04-01 10:56:32 +04:00
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
update: function update(record) {
|
2016-04-05 01:39:37 +03:00
|
|
|
if (record.id == this.engine.localID) {
|
2016-08-11 21:01:15 +03:00
|
|
|
// Only grab commands from the server; local name/type always wins
|
|
|
|
this.engine.localCommands = record.commands;
|
2016-04-05 01:39:37 +03:00
|
|
|
} else {
|
2010-03-17 02:39:08 +03:00
|
|
|
this._remoteClients[record.id] = record.cleartext;
|
2016-04-01 20:55:10 +03:00
|
|
|
}
|
2009-01-07 00:54:18 +03:00
|
|
|
},
|
|
|
|
|
2010-11-30 03:41:17 +03:00
|
|
|
createRecord: function createRecord(id, collection) {
|
|
|
|
let record = new ClientsRec(collection, id);
|
2009-02-20 12:52:07 +03:00
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
const commandsChanges = this.engine._currentlySyncingCommands ?
|
|
|
|
this.engine._currentlySyncingCommands[id] :
|
|
|
|
[];
|
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
// Package the individual components into a record for the local client
|
2012-08-30 01:43:41 +04:00
|
|
|
if (id == this.engine.localID) {
|
2016-01-13 07:55:00 +03:00
|
|
|
let cb = Async.makeSpinningCallback();
|
|
|
|
fxAccounts.getDeviceId().then(id => cb(null, id), cb);
|
|
|
|
try {
|
|
|
|
record.fxaDeviceId = cb.wait();
|
2017-01-10 20:09:02 +03:00
|
|
|
} catch (error) {
|
2016-01-13 07:55:00 +03:00
|
|
|
this._log.warn("failed to get fxa device id", error);
|
|
|
|
}
|
2012-08-30 01:43:41 +04:00
|
|
|
record.name = this.engine.localName;
|
|
|
|
record.type = this.engine.localType;
|
2014-01-07 06:41:12 +04:00
|
|
|
record.version = Services.appinfo.version;
|
2014-01-07 09:45:26 +04:00
|
|
|
record.protocols = SUPPORTED_PROTOCOL_VERSIONS;
|
2014-11-18 06:06:00 +03:00
|
|
|
|
2016-08-11 21:01:15 +03:00
|
|
|
// Substract the commands we recorded that we've already executed
|
|
|
|
if (commandsChanges && commandsChanges.length &&
|
|
|
|
this.engine.localCommands && this.engine.localCommands.length) {
|
|
|
|
record.commands = this.engine.localCommands.filter(command => !hasDupeCommand(commandsChanges, command));
|
|
|
|
}
|
|
|
|
|
2014-11-18 06:06:00 +03:00
|
|
|
// Optional fields.
|
|
|
|
record.os = Services.appinfo.OS; // "Darwin"
|
|
|
|
record.appPackage = Services.appinfo.ID;
|
|
|
|
record.application = this.engine.brandName // "Nightly"
|
|
|
|
|
|
|
|
// We can't compute these yet.
|
|
|
|
// record.device = ""; // Bug 1100723
|
|
|
|
// record.formfactor = ""; // Bug 1100722
|
|
|
|
} else {
|
2010-11-30 03:41:17 +03:00
|
|
|
record.cleartext = this._remoteClients[id];
|
2016-08-11 21:01:15 +03:00
|
|
|
|
|
|
|
// Add the commands we have to send
|
|
|
|
if (commandsChanges && commandsChanges.length) {
|
|
|
|
const recordCommands = record.cleartext.commands || [];
|
|
|
|
const newCommands = commandsChanges.filter(command => !hasDupeCommand(recordCommands, command));
|
|
|
|
record.cleartext.commands = recordCommands.concat(newCommands);
|
|
|
|
}
|
|
|
|
|
2016-07-19 09:30:01 +03:00
|
|
|
if (record.cleartext.stale) {
|
|
|
|
// It's almost certainly a logic error for us to upload a record we
|
|
|
|
// consider stale, so make log noise, but still remove the flag.
|
|
|
|
this._log.error(`Preparing to upload record ${id} that we consider stale`);
|
|
|
|
delete record.cleartext.stale;
|
|
|
|
}
|
2014-11-18 06:06:00 +03:00
|
|
|
}
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2009-04-01 10:56:32 +04:00
|
|
|
return record;
|
2009-01-07 00:54:18 +03:00
|
|
|
},
|
|
|
|
|
2015-01-25 10:50:01 +03:00
|
|
|
itemExists(id) {
|
|
|
|
return id in this.getAllIDs();
|
|
|
|
},
|
2009-01-07 00:54:18 +03:00
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
getAllIDs: function getAllIDs() {
|
|
|
|
let ids = {};
|
2012-08-30 01:43:41 +04:00
|
|
|
ids[this.engine.localID] = true;
|
2010-03-17 02:39:08 +03:00
|
|
|
for (let id in this._remoteClients)
|
|
|
|
ids[id] = true;
|
|
|
|
return ids;
|
2009-01-07 00:54:18 +03:00
|
|
|
},
|
|
|
|
|
2010-03-17 02:39:08 +03:00
|
|
|
wipe: function wipe() {
|
|
|
|
this._remoteClients = {};
|
2009-04-01 10:56:32 +04:00
|
|
|
},
|
2009-01-07 00:54:18 +03:00
|
|
|
};
|
2011-07-27 08:48:50 +04:00
|
|
|
|
2012-08-30 01:43:41 +04:00
|
|
|
function ClientsTracker(name, engine) {
|
|
|
|
Tracker.call(this, name, engine);
|
2011-07-27 08:48:50 +04:00
|
|
|
Svc.Obs.add("weave:engine:start-tracking", this);
|
|
|
|
Svc.Obs.add("weave:engine:stop-tracking", this);
|
|
|
|
}
|
|
|
|
ClientsTracker.prototype = {
|
|
|
|
__proto__: Tracker.prototype,
|
|
|
|
|
|
|
|
_enabled: false,
|
|
|
|
|
|
|
|
observe: function observe(subject, topic, data) {
|
|
|
|
switch (topic) {
|
|
|
|
case "weave:engine:start-tracking":
|
|
|
|
if (!this._enabled) {
|
|
|
|
Svc.Prefs.observe("client.name", this);
|
|
|
|
this._enabled = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "weave:engine:stop-tracking":
|
|
|
|
if (this._enabled) {
|
2016-07-04 09:26:49 +03:00
|
|
|
Svc.Prefs.ignore("client.name", this);
|
2011-07-27 08:48:50 +04:00
|
|
|
this._enabled = false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "nsPref:changed":
|
|
|
|
this._log.debug("client.name preference changed");
|
|
|
|
this.addChangedID(Svc.Prefs.get("client.GUID"));
|
|
|
|
this.score += SCORE_INCREMENT_XLARGE;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|