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