From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/network/NetworkStatsService.jsm | 1171 +++++++++++++++++++++++++++++++++++ 1 file changed, 1171 insertions(+) create mode 100644 dom/network/NetworkStatsService.jsm (limited to 'dom/network/NetworkStatsService.jsm') diff --git a/dom/network/NetworkStatsService.jsm b/dom/network/NetworkStatsService.jsm new file mode 100644 index 000000000..4b6d69498 --- /dev/null +++ b/dom/network/NetworkStatsService.jsm @@ -0,0 +1,1171 @@ +/* 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/. */ + +"use strict"; + +const DEBUG = false; +function debug(s) { + if (DEBUG) { + dump("-*- NetworkStatsService: " + s + "\n"); + } +} + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +this.EXPORTED_SYMBOLS = ["NetworkStatsService"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetworkStatsDB.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +const NET_NETWORKSTATSSERVICE_CONTRACTID = "@mozilla.org/network/netstatsservice;1"; +const NET_NETWORKSTATSSERVICE_CID = Components.ID("{18725604-e9ac-488a-8aa0-2471e7f6c0a4}"); + +const TOPIC_BANDWIDTH_CONTROL = "netd-bandwidth-control" + +const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed"; +const NET_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI; +const NET_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; + +// Networks have different status that NetworkStats API needs to be aware of. +// Network is present and ready, so NetworkManager provides the whole info. +const NETWORK_STATUS_READY = 0; +// Network is present but hasn't established a connection yet (e.g. SIM that has not +// enabled 3G since boot). +const NETWORK_STATUS_STANDBY = 1; +// Network is not present, but stored in database by the previous connections. +const NETWORK_STATUS_AWAY = 2; + +// The maximum traffic amount can be saved in the |cachedStats|. +const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB + +const QUEUE_TYPE_UPDATE_STATS = 0; +const QUEUE_TYPE_UPDATE_CACHE = 1; +const QUEUE_TYPE_WRITE_CACHE = 2; + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageListenerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gRil", + "@mozilla.org/ril;1", + "nsIRadioInterfaceLayer"); + +XPCOMUtils.defineLazyServiceGetter(this, "networkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "messenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "gIccService", + "@mozilla.org/icc/iccservice;1", + "nsIIccService"); + +this.NetworkStatsService = { + init: function() { + debug("Service started"); + + Services.obs.addObserver(this, "xpcom-shutdown", false); + Services.obs.addObserver(this, TOPIC_CONNECTION_STATE_CHANGED, false); + Services.obs.addObserver(this, TOPIC_BANDWIDTH_CONTROL, false); + Services.obs.addObserver(this, "profile-after-change", false); + + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + // Object to store network interfaces, each network interface is composed + // by a network object (network type and network Id) and a interfaceName + // that contains the name of the physical interface (wlan0, rmnet0, etc.). + // The network type can be 0 for wifi or 1 for mobile. On the other hand, + // the network id is '0' for wifi or the iccid for mobile (SIM). + // Each networkInterface is placed in the _networks object by the index of + // 'networkId + networkType'. + // + // _networks object allows to map available network interfaces at low level + // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a + // networkInterface per network but can't exist a networkInterface not + // being mapped to a network. + + this._networks = Object.create(null); + + // There is no way to know a priori if wifi connection is available, + // just when the wifi driver is loaded, but it is unloaded when + // wifi is switched off. So wifi connection is hardcoded + let netId = this.getNetworkId('0', NET_TYPE_WIFI); + this._networks[netId] = { network: { id: '0', + type: NET_TYPE_WIFI }, + interfaceName: null, + status: NETWORK_STATUS_STANDBY }; + + this.messages = ["NetworkStats:Get", + "NetworkStats:Clear", + "NetworkStats:ClearAll", + "NetworkStats:SetAlarm", + "NetworkStats:GetAlarms", + "NetworkStats:RemoveAlarms", + "NetworkStats:GetAvailableNetworks", + "NetworkStats:GetAvailableServiceTypes", + "NetworkStats:SampleRate", + "NetworkStats:MaxStorageAge"]; + + this.messages.forEach(function(aMsgName) { + ppmm.addMessageListener(aMsgName, this); + }, this); + + this._db = new NetworkStatsDB(); + + // Stats for all interfaces are updated periodically + this.timer.initWithCallback(this, this._db.sampleRate, + Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); + + // Stats not from netd are firstly stored in the cached. + this.cachedStats = Object.create(null); + this.cachedStatsDate = new Date(); + + this.updateQueue = []; + this.isQueueRunning = false; + + this._currentAlarms = {}; + this.initAlarms(); + }, + + receiveMessage: function(aMessage) { + if (!aMessage.target.assertPermission("networkstats-manage")) { + return; + } + + debug("receiveMessage " + aMessage.name); + + let mm = aMessage.target; + let msg = aMessage.json; + + switch (aMessage.name) { + case "NetworkStats:Get": + this.getSamples(mm, msg); + break; + case "NetworkStats:Clear": + this.clearInterfaceStats(mm, msg); + break; + case "NetworkStats:ClearAll": + this.clearDB(mm, msg); + break; + case "NetworkStats:SetAlarm": + this.setAlarm(mm, msg); + break; + case "NetworkStats:GetAlarms": + this.getAlarms(mm, msg); + break; + case "NetworkStats:RemoveAlarms": + this.removeAlarms(mm, msg); + break; + case "NetworkStats:GetAvailableNetworks": + this.getAvailableNetworks(mm, msg); + break; + case "NetworkStats:GetAvailableServiceTypes": + this.getAvailableServiceTypes(mm, msg); + break; + case "NetworkStats:SampleRate": + // This message is sync. + return this._db.sampleRate; + case "NetworkStats:MaxStorageAge": + // This message is sync. + return this._db.maxStorageSamples * this._db.sampleRate; + } + }, + + observe: function observe(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_CONNECTION_STATE_CHANGED: + + // If new interface is registered (notified from NetworkService), + // the stats are updated for the new interface without waiting to + // complete the updating period. + + let networkInfo = aSubject.QueryInterface(Ci.nsINetworkInfo); + debug("Network " + networkInfo.name + " of type " + networkInfo.type + " status change"); + + let netId = this.convertNetworkInfo(networkInfo); + if (!netId) { + break; + } + + this._updateCurrentAlarm(netId); + + debug("NetId: " + netId); + this.updateStats(netId); + break; + + case TOPIC_BANDWIDTH_CONTROL: + debug("Bandwidth message from netd: " + JSON.stringify(aData)); + + let interfaceName = aData.substring(aData.lastIndexOf(" ") + 1); + for (let networkId in this._networks) { + if (interfaceName == this._networks[networkId].interfaceName) { + let currentAlarm = this._currentAlarms[networkId]; + if (Object.getOwnPropertyNames(currentAlarm).length !== 0) { + this._fireAlarm(currentAlarm.alarm); + } + break; + } + } + break; + + case "xpcom-shutdown": + debug("Service shutdown"); + + this.messages.forEach(function(aMsgName) { + ppmm.removeMessageListener(aMsgName, this); + }, this); + + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.obs.removeObserver(this, "profile-after-change"); + Services.obs.removeObserver(this, TOPIC_CONNECTION_STATE_CHANGED); + Services.obs.removeObserver(this, TOPIC_BANDWIDTH_CONTROL); + + this.timer.cancel(); + this.timer = null; + + // Update stats before shutdown + this.updateAllStats(); + break; + } + }, + + /* + * nsITimerCallback + * Timer triggers the update of all stats + */ + notify: function(aTimer) { + this.updateAllStats(); + }, + + /* + * nsINetworkStatsService + */ + getRilNetworks: function() { + let networks = {}; + let numRadioInterfaces = gRil.numRadioInterfaces; + for (let i = 0; i < numRadioInterfaces; i++) { + let icc = gIccService.getIccByServiceId(i); + let radioInterface = gRil.getRadioInterface(i); + if (icc && icc.iccInfo) { + let netId = this.getNetworkId(icc.iccInfo.iccid, + NET_TYPE_MOBILE); + networks[netId] = { id : icc.iccInfo.iccid, + type: NET_TYPE_MOBILE }; + } + } + return networks; + }, + + convertNetworkInfo: function(aNetworkInfo) { + if (aNetworkInfo.type != NET_TYPE_MOBILE && + aNetworkInfo.type != NET_TYPE_WIFI) { + return null; + } + + let id = '0'; + if (aNetworkInfo.type == NET_TYPE_MOBILE) { + if (!(aNetworkInfo instanceof Ci.nsIRilNetworkInfo)) { + debug("Error! Mobile network should be an nsIRilNetworkInfo!"); + return null; + } + + let rilNetwork = aNetworkInfo.QueryInterface(Ci.nsIRilNetworkInfo); + id = rilNetwork.iccId; + } + + let netId = this.getNetworkId(id, aNetworkInfo.type); + + if (!this._networks[netId]) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = { id: id, + type: aNetworkInfo.type }; + } + + this._networks[netId].status = NETWORK_STATUS_READY; + this._networks[netId].interfaceName = aNetworkInfo.name; + return netId; + }, + + getNetworkId: function getNetworkId(aIccId, aNetworkType) { + return aIccId + '' + aNetworkType; + }, + + /* Function to ensure that one network is valid. The network is valid if its status is + * NETWORK_STATUS_READY, NETWORK_STATUS_STANDBY or NETWORK_STATUS_AWAY. + * + * The result is |netId| or null in case of a non-valid network + * aCallback is signatured as |function(netId)|. + */ + validateNetwork: function validateNetwork(aNetwork, aCallback) { + let netId = this.getNetworkId(aNetwork.id, aNetwork.type); + + if (this._networks[netId]) { + aCallback(netId); + return; + } + + // Check if network is valid (RIL entry) but has not established a connection yet. + // If so add to networks list with empty interfaceName. + let rilNetworks = this.getRilNetworks(); + if (rilNetworks[netId]) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = rilNetworks[netId]; + this._networks[netId].status = NETWORK_STATUS_STANDBY; + this._currentAlarms[netId] = Object.create(null); + aCallback(netId); + return; + } + + // Check if network is available in the DB. + this._db.isNetworkAvailable(aNetwork, function(aError, aResult) { + if (aResult) { + this._networks[netId] = Object.create(null); + this._networks[netId].network = aNetwork; + this._networks[netId].status = NETWORK_STATUS_AWAY; + this._currentAlarms[netId] = Object.create(null); + aCallback(netId); + return; + } + + aCallback(null); + }.bind(this)); + }, + + getAvailableNetworks: function getAvailableNetworks(mm, msg) { + let self = this; + let rilNetworks = this.getRilNetworks(); + this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { + + // Also return the networks that are valid but have not + // established connections yet. + for (let netId in rilNetworks) { + let found = false; + for (let i = 0; i < aResult.length; i++) { + if (netId == self.getNetworkId(aResult[i].id, aResult[i].type)) { + found = true; + break; + } + } + if (!found) { + aResult.push(rilNetworks[netId]); + } + } + + mm.sendAsyncMessage("NetworkStats:GetAvailableNetworks:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }, + + getAvailableServiceTypes: function getAvailableServiceTypes(mm, msg) { + this._db.getAvailableServiceTypes(function onGetServiceTypes(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }, + + initAlarms: function initAlarms() { + debug("Init usage alarms"); + let self = this; + + for (let netId in this._networks) { + this._currentAlarms[netId] = Object.create(null); + + this._db.getFirstAlarm(netId, function getResult(error, result) { + if (!error && result) { + self._setAlarm(result, function onSet(error, success) { + if (error == "InvalidStateError") { + self._fireAlarm(result); + } + }); + } + }); + } + }, + + /* + * Function called from manager to get stats from database. + * In order to return updated stats, first is performed a call to + * updateAllStats function, which will get last stats from netd + * and update the database. + * Then, depending on the request (stats per appId or total stats) + * it retrieve them from database and return to the manager. + */ + getSamples: function getSamples(mm, msg) { + let network = msg.network; + let netId = this.getNetworkId(network.id, network.type); + + let appId = 0; + let appManifestURL = msg.appManifestURL; + if (appManifestURL) { + appId = appsService.getAppLocalIdByManifestURL(appManifestURL); + + if (!appId) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, + error: "Invalid appManifestURL", result: null }); + return; + } + } + + let browsingTrafficOnly = msg.browsingTrafficOnly || false; + let serviceType = msg.serviceType || ""; + + let start = new Date(msg.start); + let end = new Date(msg.end); + + let callback = (function (aError, aResult) { + this._db.find(function onStatsFound(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: aError, result: aResult }); + }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL); + }).bind(this); + + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: "Invalid connectionType", result: null }); + return; + } + + // If network is currently active we need to update the cached stats first before + // retrieving stats from the DB. + if (this._networks[aNetId].status == NETWORK_STATUS_READY) { + debug("getstats for network " + network.id + " of type " + network.type); + debug("appId: " + appId + " from appManifestURL: " + appManifestURL); + debug("browsingTrafficOnly: " + browsingTrafficOnly); + debug("serviceType: " + serviceType); + + if (appId || serviceType) { + this.updateCachedStats(callback); + return; + } + + this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) { + this.updateCachedStats(callback); + }.bind(this)); + return; + } + + // Network not active, so no need to update + this._db.find(function onStatsFound(aError, aResult) { + mm.sendAsyncMessage("NetworkStats:Get:Return", + { id: msg.id, error: aError, result: aResult }); + }, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL); + }.bind(this)); + }, + + clearInterfaceStats: function clearInterfaceStats(mm, msg) { + let self = this; + let network = msg.network; + + debug("clear stats for network " + network.id + " of type " + network.type); + + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: "Invalid connectionType", result: null }); + return; + } + + network = {network: network, networkId: aNetId}; + self.updateStats(aNetId, function onUpdate(aResult, aMessage) { + if (!aResult) { + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: aMessage, result: null }); + return; + } + + self._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) { + self._updateCurrentAlarm(aNetId); + mm.sendAsyncMessage("NetworkStats:Clear:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }); + }); + }, + + clearDB: function clearDB(mm, msg) { + let self = this; + this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) { + if (aError) { + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aError, result: aResult }); + return; + } + + let networks = aResult; + networks.forEach(function(network, index) { + networks[index] = {network: network, networkId: self.getNetworkId(network.id, network.type)}; + }, self); + + self.updateAllStats(function onUpdate(aResult, aMessage){ + if (!aResult) { + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aMessage, result: null }); + return; + } + + self._db.clearStats(networks, function onDBCleared(aError, aResult) { + networks.forEach(function(network, index) { + self._updateCurrentAlarm(network.networkId); + }, self); + mm.sendAsyncMessage("NetworkStats:ClearAll:Return", + { id: msg.id, error: aError, result: aResult }); + }); + }); + }); + }, + + updateAllStats: function updateAllStats(aCallback) { + let elements = []; + let lastElement = null; + let callback = (function (success, message) { + this.updateCachedStats(aCallback); + }).bind(this); + + // For each connectionType create an object containning the type + // and the 'queueIndex', the 'queueIndex' is an integer representing + // the index of a connection type in the global queue array. So, if + // the connection type is already in the queue it is not appended again, + // else it is pushed in 'elements' array, which later will be pushed to + // the queue array. + for (let netId in this._networks) { + if (this._networks[netId].status != NETWORK_STATUS_READY) { + continue; + } + + lastElement = { netId: netId, + queueIndex: this.updateQueueIndex(netId) }; + + if (lastElement.queueIndex == -1) { + elements.push({ netId: lastElement.netId, + callbacks: [], + queueType: QUEUE_TYPE_UPDATE_STATS }); + } + } + + if (!lastElement) { + // No elements need to be updated, probably because status is different than + // NETWORK_STATUS_READY. + if (aCallback) { + aCallback(true, "OK"); + } + return; + } + + if (elements.length > 0) { + // If length of elements is greater than 0, callback is set to + // the last element. + elements[elements.length - 1].callbacks.push(callback); + this.updateQueue = this.updateQueue.concat(elements); + } else { + // Else, it means that all connection types are already in the queue to + // be updated, so callback for this request is added to + // the element in the main queue with the index of the last 'lastElement'. + // But before is checked that element is still in the queue because it can + // be processed while generating 'elements' array. + let element = this.updateQueue[lastElement.queueIndex]; + if (aCallback && + (!element || element.netId != lastElement.netId)) { + aCallback(); + return; + } + + this.updateQueue[lastElement.queueIndex].callbacks.push(callback); + } + + // Call the function that process the elements of the queue. + this.processQueue(); + + if (DEBUG) { + this.logAllRecords(); + } + }, + + updateStats: function updateStats(aNetId, aCallback) { + // Check if the connection is in the main queue, push a new element + // if it is not being processed or add a callback if it is. + let index = this.updateQueueIndex(aNetId); + if (index == -1) { + this.updateQueue.push({ netId: aNetId, + callbacks: [aCallback], + queueType: QUEUE_TYPE_UPDATE_STATS }); + } else { + this.updateQueue[index].callbacks.push(aCallback); + return; + } + + // Call the function that process the elements of the queue. + this.processQueue(); + }, + + /* + * Find if a connection is in the main queue array and return its + * index, if it is not in the array return -1. + */ + updateQueueIndex: function updateQueueIndex(aNetId) { + return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId); + }, + + /* + * Function responsible of process all requests in the queue. + */ + processQueue: function processQueue(aResult, aMessage) { + // If aResult is not undefined, the caller of the function is the result + // of processing an element, so remove that element and call the callbacks + // it has. + let self = this; + + if (aResult != undefined) { + let item = this.updateQueue.shift(); + for (let callback of item.callbacks) { + if (callback) { + callback(aResult, aMessage); + } + } + } else { + // The caller is a function that has pushed new elements to the queue, + // if isQueueRunning is false it means there is no processing currently + // being done, so start. + if (this.isQueueRunning) { + return; + } else { + this.isQueueRunning = true; + } + } + + // Check length to determine if queue is empty and stop processing. + if (this.updateQueue.length < 1) { + this.isQueueRunning = false; + return; + } + + // Process the next item as soon as possible. + setTimeout(function () { + self.run(self.updateQueue[0]); + }, 0); + }, + + run: function run(item) { + switch (item.queueType) { + case QUEUE_TYPE_UPDATE_STATS: + this.update(item.netId, this.processQueue.bind(this)); + break; + case QUEUE_TYPE_UPDATE_CACHE: + this.updateCache(this.processQueue.bind(this)); + break; + case QUEUE_TYPE_WRITE_CACHE: + this.writeCache(item.stats, this.processQueue.bind(this)); + break; + } + }, + + update: function update(aNetId, aCallback) { + // Check if connection type is valid. + if (!this._networks[aNetId]) { + if (aCallback) { + aCallback(false, "Invalid network " + aNetId); + } + return; + } + + let interfaceName = this._networks[aNetId].interfaceName; + debug("Update stats for " + interfaceName); + + // Request stats to NetworkService, which will get stats from netd, passing + // 'networkStatsAvailable' as a callback. + if (interfaceName) { + networkService.getNetworkInterfaceStats(interfaceName, + this.networkStatsAvailable.bind(this, aCallback, aNetId)); + return; + } + + if (aCallback) { + aCallback(true, "ok"); + } + }, + + /* + * Callback of request stats. Store stats in database. + */ + networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId, + aResult, aRxBytes, + aTxBytes, aTimestamp) { + if (!aResult) { + if (aCallback) { + aCallback(false, "Netd IPC error"); + } + return; + } + + let stats = { appId: 0, + isInBrowser: false, + serviceType: "", + networkId: this._networks[aNetId].network.id, + networkType: this._networks[aNetId].network.type, + date: new Date(aTimestamp), + rxBytes: aTxBytes, + txBytes: aRxBytes, + isAccumulative: true }; + + debug("Update stats for: " + JSON.stringify(stats)); + + this._db.saveStats(stats, function onSavedStats(aError, aResult) { + if (aCallback) { + if (aError) { + aCallback(false, aError); + return; + } + + aCallback(true, "OK"); + } + }); + }, + + /* + * Function responsible for receiving stats which are not from netd. + */ + saveStats: function saveStats(aAppId, aIsInIsolatedMozBrowser, aServiceType, + aNetworkInfo, aTimeStamp, aRxBytes, aTxBytes, + aIsAccumulative, aCallback) { + let netId = this.convertNetworkInfo(aNetworkInfo); + if (!netId) { + if (aCallback) { + aCallback(false, "Invalid network type"); + } + return; + } + + // Check if |aConnectionType|, |aAppId| and |aServiceType| are valid. + // There are two invalid cases for the combination of |aAppId| and + // |aServiceType|: + // a. Both |aAppId| is non-zero and |aServiceType| is non-empty. + // b. Both |aAppId| is zero and |aServiceType| is empty. + if (!this._networks[netId] || (aAppId && aServiceType) || + (!aAppId && !aServiceType)) { + debug("Invalid network interface, appId or serviceType"); + return; + } + + let stats = { appId: aAppId, + isInBrowser: aIsInIsolatedMozBrowser, + serviceType: aServiceType, + networkId: this._networks[netId].network.id, + networkType: this._networks[netId].network.type, + date: new Date(aTimeStamp), + rxBytes: aRxBytes, + txBytes: aTxBytes, + isAccumulative: aIsAccumulative }; + + this.updateQueue.push({ stats: stats, + callbacks: [aCallback], + queueType: QUEUE_TYPE_WRITE_CACHE }); + + this.processQueue(); + }, + + /* + * + */ + writeCache: function writeCache(aStats, aCallback) { + debug("saveStats: " + aStats.appId + " " + aStats.isInBrowser + " " + + aStats.serviceType + " " + aStats.networkId + " " + + aStats.networkType + " " + aStats.date + " " + + aStats.rxBytes + " " + aStats.txBytes); + + // Generate an unique key from |appId|, |isInBrowser|, |serviceType| and + // |netId|, which is used to retrieve data in |cachedStats|. + let netId = this.getNetworkId(aStats.networkId, aStats.networkType); + let key = aStats.appId + "" + aStats.isInBrowser + "" + + aStats.serviceType + "" + netId; + + // |cachedStats| only keeps the data with the same date. + // If the incoming date is different from |cachedStatsDate|, + // both |cachedStats| and |cachedStatsDate| will get updated. + let diff = (this._db.normalizeDate(aStats.date) - + this._db.normalizeDate(this.cachedStatsDate)) / + this._db.sampleRate; + if (diff != 0) { + this.updateCache(function onUpdated(success, message) { + this.cachedStatsDate = aStats.date; + this.cachedStats[key] = aStats; + + if (aCallback) { + aCallback(true, "ok"); + } + }.bind(this)); + return; + } + + // Try to find the matched row in the cached by |appId| and |connectionType|. + // If not found, save the incoming data into the cached. + let cachedStats = this.cachedStats[key]; + if (!cachedStats) { + this.cachedStats[key] = aStats; + if (aCallback) { + aCallback(true, "ok"); + } + return; + } + + // Find matched row, accumulate the traffic amount. + cachedStats.rxBytes += aStats.rxBytes; + cachedStats.txBytes += aStats.txBytes; + + // If new rxBytes or txBytes exceeds MAX_CACHED_TRAFFIC + // the corresponding row will be saved to indexedDB. + // Then, the row will be removed from the cached. + if (cachedStats.rxBytes > MAX_CACHED_TRAFFIC || + cachedStats.txBytes > MAX_CACHED_TRAFFIC) { + this._db.saveStats(cachedStats, function (error, result) { + debug("Application stats inserted in indexedDB"); + if (aCallback) { + aCallback(true, "ok"); + } + }); + delete this.cachedStats[key]; + return; + } + + if (aCallback) { + aCallback(true, "ok"); + } + }, + + updateCachedStats: function updateCachedStats(aCallback) { + this.updateQueue.push({ callbacks: [aCallback], + queueType: QUEUE_TYPE_UPDATE_CACHE }); + + this.processQueue(); + }, + + updateCache: function updateCache(aCallback) { + debug("updateCache: " + this.cachedStatsDate); + + let stats = Object.keys(this.cachedStats); + if (stats.length == 0) { + // |cachedStats| is empty, no need to update. + if (aCallback) { + aCallback(true, "no need to update"); + } + return; + } + + let index = 0; + this._db.saveStats(this.cachedStats[stats[index]], + function onSavedStats(error, result) { + debug("Application stats inserted in indexedDB"); + + // Clean up the |cachedStats| after updating. + if (index == stats.length - 1) { + this.cachedStats = Object.create(null); + + if (aCallback) { + aCallback(true, "ok"); + } + return; + } + + // Update is not finished, keep updating. + index += 1; + this._db.saveStats(this.cachedStats[stats[index]], + onSavedStats.bind(this, error, result)); + }.bind(this)); + }, + + get maxCachedTraffic () { + return MAX_CACHED_TRAFFIC; + }, + + logAllRecords: function logAllRecords() { + this._db.logAllRecords(function onResult(aError, aResult) { + if (aError) { + debug("Error: " + aError); + return; + } + + debug("===== LOG ====="); + debug("There are " + aResult.length + " items"); + debug(JSON.stringify(aResult)); + }); + }, + + getAlarms: function getAlarms(mm, msg) { + let self = this; + let network = msg.data.network; + let manifestURL = msg.data.manifestURL; + + if (network) { + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: "InvalidInterface", result: null }); + return; + } + + self._getAlarms(mm, msg, aNetId, manifestURL); + }); + return; + } + + this._getAlarms(mm, msg, null, manifestURL); + }, + + _getAlarms: function _getAlarms(mm, msg, aNetId, aManifestURL) { + let self = this; + this._db.getAlarms(aNetId, aManifestURL, function onCompleted(error, result) { + if (error) { + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: error, result: result }); + return; + } + + let alarms = [] + // NetworkStatsManager must return the network instead of the networkId. + for (let i = 0; i < result.length; i++) { + let alarm = result[i]; + alarms.push({ id: alarm.id, + network: self._networks[alarm.networkId].network, + threshold: alarm.absoluteThreshold, + data: alarm.data }); + } + + mm.sendAsyncMessage("NetworkStats:GetAlarms:Return", + { id: msg.id, error: null, result: alarms }); + }); + }, + + removeAlarms: function removeAlarms(mm, msg) { + let alarmId = msg.data.alarmId; + let manifestURL = msg.data.manifestURL; + + let self = this; + let callback = function onRemove(error, result) { + if (error) { + mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", + { id: msg.id, error: error, result: result }); + return; + } + + for (let i in self._currentAlarms) { + let currentAlarm = self._currentAlarms[i].alarm; + if (currentAlarm && ((alarmId == currentAlarm.id) || + (alarmId == -1 && currentAlarm.manifestURL == manifestURL))) { + + self._updateCurrentAlarm(currentAlarm.networkId); + } + } + + mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return", + { id: msg.id, error: error, result: true }); + }; + + if (alarmId == -1) { + this._db.removeAlarms(manifestURL, callback); + } else { + this._db.removeAlarm(alarmId, manifestURL, callback); + } + }, + + /* + * Function called from manager to set an alarm. + */ + setAlarm: function setAlarm(mm, msg) { + let options = msg.data; + let network = options.network; + let threshold = options.threshold; + + debug("Set alarm at " + threshold + " for " + JSON.stringify(network)); + + if (threshold < 0) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: "InvalidThresholdValue", result: null }); + return; + } + + let self = this; + this.validateNetwork(network, function onValidateNetwork(aNetId) { + if (!aNetId) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: "InvalidiConnectionType", result: null }); + return; + } + + let newAlarm = { + id: null, + networkId: aNetId, + absoluteThreshold: threshold, + relativeThreshold: null, + startTime: options.startTime, + data: options.data, + pageURL: options.pageURL, + manifestURL: options.manifestURL + }; + + self._getAlarmQuota(newAlarm, function onUpdate(error, quota) { + if (error) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: null }); + return; + } + + self._db.addAlarm(newAlarm, function addSuccessCb(error, newId) { + if (error) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: null }); + return; + } + + newAlarm.id = newId; + self._setAlarm(newAlarm, function onSet(error, success) { + mm.sendAsyncMessage("NetworkStats:SetAlarm:Return", + { id: msg.id, error: error, result: newId }); + + if (error == "InvalidStateError") { + self._fireAlarm(newAlarm); + } + }); + }); + }); + }); + }, + + _setAlarm: function _setAlarm(aAlarm, aCallback) { + let currentAlarm = this._currentAlarms[aAlarm.networkId]; + if ((Object.getOwnPropertyNames(currentAlarm).length !== 0 && + aAlarm.relativeThreshold > currentAlarm.alarm.relativeThreshold) || + this._networks[aAlarm.networkId].status != NETWORK_STATUS_READY) { + aCallback(null, true); + return; + } + + let self = this; + + this._getAlarmQuota(aAlarm, function onUpdate(aError, aQuota) { + if (aError) { + aCallback(aError, null); + return; + } + + let callback = function onAlarmSet(aError) { + if (aError) { + debug("Set alarm error: " + aError); + aCallback("netdError", null); + return; + } + + self._currentAlarms[aAlarm.networkId].alarm = aAlarm; + + aCallback(null, true); + }; + + debug("Set alarm " + JSON.stringify(aAlarm)); + let interfaceName = self._networks[aAlarm.networkId].interfaceName; + if (interfaceName) { + networkService.setNetworkInterfaceAlarm(interfaceName, + aQuota, + callback); + return; + } + + aCallback(null, true); + }); + }, + + _getAlarmQuota: function _getAlarmQuota(aAlarm, aCallback) { + let self = this; + this.updateStats(aAlarm.networkId, function onStatsUpdated(aResult, aMessage) { + self._db.getCurrentStats(self._networks[aAlarm.networkId].network, + aAlarm.startTime, + function onStatsFound(error, result) { + if (error) { + debug("Error getting stats for " + + JSON.stringify(self._networks[aAlarm.networkId]) + ": " + error); + aCallback(error, result); + return; + } + + let quota = aAlarm.absoluteThreshold - result.rxBytes - result.txBytes; + + // Alarm set to a threshold lower than current rx/tx bytes. + if (quota <= 0) { + aCallback("InvalidStateError", null); + return; + } + + aAlarm.relativeThreshold = aAlarm.startTime + ? result.rxTotalBytes + result.txTotalBytes + quota + : aAlarm.absoluteThreshold; + + aCallback(null, quota); + }); + }); + }, + + _fireAlarm: function _fireAlarm(aAlarm) { + debug("Fire alarm"); + + let self = this; + this._db.removeAlarm(aAlarm.id, null, function onRemove(aError, aResult){ + if (!aError && !aResult) { + return; + } + + self._fireSystemMessage(aAlarm); + self._updateCurrentAlarm(aAlarm.networkId); + }); + }, + + _updateCurrentAlarm: function _updateCurrentAlarm(aNetworkId) { + this._currentAlarms[aNetworkId] = Object.create(null); + + let self = this; + this._db.getFirstAlarm(aNetworkId, function onGet(error, result){ + if (error) { + debug("Error getting the first alarm"); + return; + } + + if (!result) { + let interfaceName = self._networks[aNetworkId].interfaceName; + networkService.setNetworkInterfaceAlarm(interfaceName, -1, + function onComplete(){}); + return; + } + + self._setAlarm(result, function onSet(error, success){ + if (error == "InvalidStateError") { + self._fireAlarm(result); + return; + } + }); + }); + }, + + _fireSystemMessage: function _fireSystemMessage(aAlarm) { + debug("Fire system message: " + JSON.stringify(aAlarm)); + + let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null); + let pageURI = Services.io.newURI(aAlarm.pageURL, null, null); + + let alarm = { "id": aAlarm.id, + "threshold": aAlarm.absoluteThreshold, + "data": aAlarm.data }; + messenger.sendMessage("networkstats-alarm", alarm, pageURI, manifestURI); + } +}; + +NetworkStatsService.init(); -- cgit v1.2.3