Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const DEBUG = false;
8 function debug(s) {
9 if (DEBUG) {
10 dump("-*- NetworkStatsService: " + s + "\n");
11 }
12 }
14 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
16 this.EXPORTED_SYMBOLS = ["NetworkStatsService"];
18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
19 Cu.import("resource://gre/modules/Services.jsm");
20 Cu.import("resource://gre/modules/NetworkStatsDB.jsm");
22 const NET_NETWORKSTATSSERVICE_CONTRACTID = "@mozilla.org/network/netstatsservice;1";
23 const NET_NETWORKSTATSSERVICE_CID = Components.ID("{18725604-e9ac-488a-8aa0-2471e7f6c0a4}");
25 const TOPIC_BANDWIDTH_CONTROL = "netd-bandwidth-control"
27 const TOPIC_INTERFACE_REGISTERED = "network-interface-registered";
28 const TOPIC_INTERFACE_UNREGISTERED = "network-interface-unregistered";
29 const NET_TYPE_WIFI = Ci.nsINetworkInterface.NETWORK_TYPE_WIFI;
30 const NET_TYPE_MOBILE = Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE;
32 // Networks have different status that NetworkStats API needs to be aware of.
33 // Network is present and ready, so NetworkManager provides the whole info.
34 const NETWORK_STATUS_READY = 0;
35 // Network is present but hasn't established a connection yet (e.g. SIM that has not
36 // enabled 3G since boot).
37 const NETWORK_STATUS_STANDBY = 1;
38 // Network is not present, but stored in database by the previous connections.
39 const NETWORK_STATUS_AWAY = 2;
41 // The maximum traffic amount can be saved in the |cachedStats|.
42 const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB
44 const QUEUE_TYPE_UPDATE_STATS = 0;
45 const QUEUE_TYPE_UPDATE_CACHE = 1;
46 const QUEUE_TYPE_WRITE_CACHE = 2;
48 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
49 "@mozilla.org/parentprocessmessagemanager;1",
50 "nsIMessageListenerManager");
52 XPCOMUtils.defineLazyServiceGetter(this, "gRil",
53 "@mozilla.org/ril;1",
54 "nsIRadioInterfaceLayer");
56 XPCOMUtils.defineLazyServiceGetter(this, "networkService",
57 "@mozilla.org/network/service;1",
58 "nsINetworkService");
60 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
61 "@mozilla.org/AppsService;1",
62 "nsIAppsService");
64 XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
65 "@mozilla.org/settingsService;1",
66 "nsISettingsService");
68 XPCOMUtils.defineLazyServiceGetter(this, "messenger",
69 "@mozilla.org/system-message-internal;1",
70 "nsISystemMessagesInternal");
72 this.NetworkStatsService = {
73 init: function() {
74 debug("Service started");
76 Services.obs.addObserver(this, "xpcom-shutdown", false);
77 Services.obs.addObserver(this, TOPIC_INTERFACE_REGISTERED, false);
78 Services.obs.addObserver(this, TOPIC_INTERFACE_UNREGISTERED, false);
79 Services.obs.addObserver(this, TOPIC_BANDWIDTH_CONTROL, false);
80 Services.obs.addObserver(this, "profile-after-change", false);
82 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
84 // Object to store network interfaces, each network interface is composed
85 // by a network object (network type and network Id) and a interfaceName
86 // that contains the name of the physical interface (wlan0, rmnet0, etc.).
87 // The network type can be 0 for wifi or 1 for mobile. On the other hand,
88 // the network id is '0' for wifi or the iccid for mobile (SIM).
89 // Each networkInterface is placed in the _networks object by the index of
90 // 'networkId + networkType'.
91 //
92 // _networks object allows to map available network interfaces at low level
93 // (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a
94 // networkInterface per network but can't exist a networkInterface not
95 // being mapped to a network.
97 this._networks = Object.create(null);
99 // There is no way to know a priori if wifi connection is available,
100 // just when the wifi driver is loaded, but it is unloaded when
101 // wifi is switched off. So wifi connection is hardcoded
102 let netId = this.getNetworkId('0', NET_TYPE_WIFI);
103 this._networks[netId] = { network: { id: '0',
104 type: NET_TYPE_WIFI },
105 interfaceName: null,
106 status: NETWORK_STATUS_STANDBY };
108 this.messages = ["NetworkStats:Get",
109 "NetworkStats:Clear",
110 "NetworkStats:ClearAll",
111 "NetworkStats:SetAlarm",
112 "NetworkStats:GetAlarms",
113 "NetworkStats:RemoveAlarms",
114 "NetworkStats:GetAvailableNetworks",
115 "NetworkStats:GetAvailableServiceTypes",
116 "NetworkStats:SampleRate",
117 "NetworkStats:MaxStorageAge"];
119 this.messages.forEach(function(aMsgName) {
120 ppmm.addMessageListener(aMsgName, this);
121 }, this);
123 this._db = new NetworkStatsDB();
125 // Stats for all interfaces are updated periodically
126 this.timer.initWithCallback(this, this._db.sampleRate,
127 Ci.nsITimer.TYPE_REPEATING_PRECISE);
129 // Stats not from netd are firstly stored in the cached.
130 this.cachedStats = Object.create(null);
131 this.cachedStatsDate = new Date();
133 this.updateQueue = [];
134 this.isQueueRunning = false;
136 this._currentAlarms = {};
137 this.initAlarms();
138 },
140 receiveMessage: function(aMessage) {
141 if (!aMessage.target.assertPermission("networkstats-manage")) {
142 return;
143 }
145 debug("receiveMessage " + aMessage.name);
147 let mm = aMessage.target;
148 let msg = aMessage.json;
150 switch (aMessage.name) {
151 case "NetworkStats:Get":
152 this.getSamples(mm, msg);
153 break;
154 case "NetworkStats:Clear":
155 this.clearInterfaceStats(mm, msg);
156 break;
157 case "NetworkStats:ClearAll":
158 this.clearDB(mm, msg);
159 break;
160 case "NetworkStats:SetAlarm":
161 this.setAlarm(mm, msg);
162 break;
163 case "NetworkStats:GetAlarms":
164 this.getAlarms(mm, msg);
165 break;
166 case "NetworkStats:RemoveAlarms":
167 this.removeAlarms(mm, msg);
168 break;
169 case "NetworkStats:GetAvailableNetworks":
170 this.getAvailableNetworks(mm, msg);
171 break;
172 case "NetworkStats:GetAvailableServiceTypes":
173 this.getAvailableServiceTypes(mm, msg);
174 break;
175 case "NetworkStats:SampleRate":
176 // This message is sync.
177 return this._db.sampleRate;
178 case "NetworkStats:MaxStorageAge":
179 // This message is sync.
180 return this._db.maxStorageSamples * this._db.sampleRate;
181 }
182 },
184 observe: function observe(aSubject, aTopic, aData) {
185 switch (aTopic) {
186 case TOPIC_INTERFACE_REGISTERED:
187 case TOPIC_INTERFACE_UNREGISTERED:
189 // If new interface is registered (notified from NetworkService),
190 // the stats are updated for the new interface without waiting to
191 // complete the updating period.
193 let network = aSubject.QueryInterface(Ci.nsINetworkInterface);
194 debug("Network " + network.name + " of type " + network.type + " status change");
196 let netId = this.convertNetworkInterface(network);
197 if (!netId) {
198 break;
199 }
201 this._updateCurrentAlarm(netId);
203 debug("NetId: " + netId);
204 this.updateStats(netId);
205 break;
207 case TOPIC_BANDWIDTH_CONTROL:
208 debug("Bandwidth message from netd: " + JSON.stringify(aData));
210 let interfaceName = aData.substring(aData.lastIndexOf(" ") + 1);
211 for (let networkId in this._networks) {
212 if (interfaceName == this._networks[networkId].interfaceName) {
213 let currentAlarm = this._currentAlarms[networkId];
214 if (Object.getOwnPropertyNames(currentAlarm).length !== 0) {
215 this._fireAlarm(currentAlarm.alarm);
216 }
217 break;
218 }
219 }
220 break;
222 case "xpcom-shutdown":
223 debug("Service shutdown");
225 this.messages.forEach(function(aMsgName) {
226 ppmm.removeMessageListener(aMsgName, this);
227 }, this);
229 Services.obs.removeObserver(this, "xpcom-shutdown");
230 Services.obs.removeObserver(this, "profile-after-change");
231 Services.obs.removeObserver(this, TOPIC_INTERFACE_REGISTERED);
232 Services.obs.removeObserver(this, TOPIC_INTERFACE_UNREGISTERED);
233 Services.obs.removeObserver(this, TOPIC_BANDWIDTH_CONTROL);
235 this.timer.cancel();
236 this.timer = null;
238 // Update stats before shutdown
239 this.updateAllStats();
240 break;
241 }
242 },
244 /*
245 * nsITimerCallback
246 * Timer triggers the update of all stats
247 */
248 notify: function(aTimer) {
249 this.updateAllStats();
250 },
252 /*
253 * nsINetworkStatsService
254 */
255 getRilNetworks: function() {
256 let networks = {};
257 let numRadioInterfaces = gRil.numRadioInterfaces;
258 for (let i = 0; i < numRadioInterfaces; i++) {
259 let radioInterface = gRil.getRadioInterface(i);
260 if (radioInterface.rilContext.iccInfo) {
261 let netId = this.getNetworkId(radioInterface.rilContext.iccInfo.iccid,
262 NET_TYPE_MOBILE);
263 networks[netId] = { id : radioInterface.rilContext.iccInfo.iccid,
264 type: NET_TYPE_MOBILE };
265 }
266 }
267 return networks;
268 },
270 convertNetworkInterface: function(aNetwork) {
271 if (aNetwork.type != NET_TYPE_MOBILE &&
272 aNetwork.type != NET_TYPE_WIFI) {
273 return null;
274 }
276 let id = '0';
277 if (aNetwork.type == NET_TYPE_MOBILE) {
278 if (!(aNetwork instanceof Ci.nsIRilNetworkInterface)) {
279 debug("Error! Mobile network should be an nsIRilNetworkInterface!");
280 return null;
281 }
283 let rilNetwork = aNetwork.QueryInterface(Ci.nsIRilNetworkInterface);
284 id = rilNetwork.iccId;
285 }
287 let netId = this.getNetworkId(id, aNetwork.type);
289 if (!this._networks[netId]) {
290 this._networks[netId] = Object.create(null);
291 this._networks[netId].network = { id: id,
292 type: aNetwork.type };
293 }
295 this._networks[netId].status = NETWORK_STATUS_READY;
296 this._networks[netId].interfaceName = aNetwork.name;
297 return netId;
298 },
300 getNetworkId: function getNetworkId(aIccId, aNetworkType) {
301 return aIccId + '' + aNetworkType;
302 },
304 /* Function to ensure that one network is valid. The network is valid if its status is
305 * NETWORK_STATUS_READY, NETWORK_STATUS_STANDBY or NETWORK_STATUS_AWAY.
306 *
307 * The result is |netId| or null in case of a non-valid network
308 * aCallback is signatured as |function(netId)|.
309 */
310 validateNetwork: function validateNetwork(aNetwork, aCallback) {
311 let netId = this.getNetworkId(aNetwork.id, aNetwork.type);
313 if (this._networks[netId]) {
314 aCallback(netId);
315 return;
316 }
318 // Check if network is valid (RIL entry) but has not established a connection yet.
319 // If so add to networks list with empty interfaceName.
320 let rilNetworks = this.getRilNetworks();
321 if (rilNetworks[netId]) {
322 this._networks[netId] = Object.create(null);
323 this._networks[netId].network = rilNetworks[netId];
324 this._networks[netId].status = NETWORK_STATUS_STANDBY;
325 this._currentAlarms[netId] = Object.create(null);
326 aCallback(netId);
327 return;
328 }
330 // Check if network is available in the DB.
331 this._db.isNetworkAvailable(aNetwork, function(aError, aResult) {
332 if (aResult) {
333 this._networks[netId] = Object.create(null);
334 this._networks[netId].network = aNetwork;
335 this._networks[netId].status = NETWORK_STATUS_AWAY;
336 this._currentAlarms[netId] = Object.create(null);
337 aCallback(netId);
338 return;
339 }
341 aCallback(null);
342 }.bind(this));
343 },
345 getAvailableNetworks: function getAvailableNetworks(mm, msg) {
346 let self = this;
347 let rilNetworks = this.getRilNetworks();
348 this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) {
350 // Also return the networks that are valid but have not
351 // established connections yet.
352 for (let netId in rilNetworks) {
353 let found = false;
354 for (let i = 0; i < aResult.length; i++) {
355 if (netId == self.getNetworkId(aResult[i].id, aResult[i].type)) {
356 found = true;
357 break;
358 }
359 }
360 if (!found) {
361 aResult.push(rilNetworks[netId]);
362 }
363 }
365 mm.sendAsyncMessage("NetworkStats:GetAvailableNetworks:Return",
366 { id: msg.id, error: aError, result: aResult });
367 });
368 },
370 getAvailableServiceTypes: function getAvailableServiceTypes(mm, msg) {
371 this._db.getAvailableServiceTypes(function onGetServiceTypes(aError, aResult) {
372 mm.sendAsyncMessage("NetworkStats:GetAvailableServiceTypes:Return",
373 { id: msg.id, error: aError, result: aResult });
374 });
375 },
377 initAlarms: function initAlarms() {
378 debug("Init usage alarms");
379 let self = this;
381 for (let netId in this._networks) {
382 this._currentAlarms[netId] = Object.create(null);
384 this._db.getFirstAlarm(netId, function getResult(error, result) {
385 if (!error && result) {
386 self._setAlarm(result, function onSet(error, success) {
387 if (error == "InvalidStateError") {
388 self._fireAlarm(result);
389 }
390 });
391 }
392 });
393 }
394 },
396 /*
397 * Function called from manager to get stats from database.
398 * In order to return updated stats, first is performed a call to
399 * updateAllStats function, which will get last stats from netd
400 * and update the database.
401 * Then, depending on the request (stats per appId or total stats)
402 * it retrieve them from database and return to the manager.
403 */
404 getSamples: function getSamples(mm, msg) {
405 let network = msg.network;
406 let netId = this.getNetworkId(network.id, network.type);
408 let appId = 0;
409 let appManifestURL = msg.appManifestURL;
410 if (appManifestURL) {
411 appId = appsService.getAppLocalIdByManifestURL(appManifestURL);
413 if (!appId) {
414 mm.sendAsyncMessage("NetworkStats:Get:Return",
415 { id: msg.id,
416 error: "Invalid appManifestURL", result: null });
417 return;
418 }
419 }
421 let serviceType = msg.serviceType || "";
423 let start = new Date(msg.start);
424 let end = new Date(msg.end);
426 let callback = (function (aError, aResult) {
427 this._db.find(function onStatsFound(aError, aResult) {
428 mm.sendAsyncMessage("NetworkStats:Get:Return",
429 { id: msg.id, error: aError, result: aResult });
430 }, appId, serviceType, network, start, end, appManifestURL);
431 }).bind(this);
433 this.validateNetwork(network, function onValidateNetwork(aNetId) {
434 if (!aNetId) {
435 mm.sendAsyncMessage("NetworkStats:Get:Return",
436 { id: msg.id, error: "Invalid connectionType", result: null });
437 return;
438 }
440 // If network is currently active we need to update the cached stats first before
441 // retrieving stats from the DB.
442 if (this._networks[aNetId].status == NETWORK_STATUS_READY) {
443 debug("getstats for network " + network.id + " of type " + network.type);
444 debug("appId: " + appId + " from appManifestURL: " + appManifestURL);
445 debug("serviceType: " + serviceType);
447 if (appId || serviceType) {
448 this.updateCachedStats(callback);
449 return;
450 }
452 this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) {
453 this.updateCachedStats(callback);
454 }.bind(this));
455 return;
456 }
458 // Network not active, so no need to update
459 this._db.find(function onStatsFound(aError, aResult) {
460 mm.sendAsyncMessage("NetworkStats:Get:Return",
461 { id: msg.id, error: aError, result: aResult });
462 }, appId, serviceType, network, start, end, appManifestURL);
463 }.bind(this));
464 },
466 clearInterfaceStats: function clearInterfaceStats(mm, msg) {
467 let self = this;
468 let network = msg.network;
470 debug("clear stats for network " + network.id + " of type " + network.type);
472 this.validateNetwork(network, function onValidateNetwork(aNetId) {
473 if (!aNetId) {
474 mm.sendAsyncMessage("NetworkStats:Clear:Return",
475 { id: msg.id, error: "Invalid connectionType", result: null });
476 return;
477 }
479 network = {network: network, networkId: aNetId};
480 self.updateStats(aNetId, function onUpdate(aResult, aMessage) {
481 if (!aResult) {
482 mm.sendAsyncMessage("NetworkStats:Clear:Return",
483 { id: msg.id, error: aMessage, result: null });
484 return;
485 }
487 self._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) {
488 self._updateCurrentAlarm(aNetId);
489 mm.sendAsyncMessage("NetworkStats:Clear:Return",
490 { id: msg.id, error: aError, result: aResult });
491 });
492 });
493 });
494 },
496 clearDB: function clearDB(mm, msg) {
497 let self = this;
498 this._db.getAvailableNetworks(function onGetNetworks(aError, aResult) {
499 if (aError) {
500 mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
501 { id: msg.id, error: aError, result: aResult });
502 return;
503 }
505 let networks = aResult;
506 networks.forEach(function(network, index) {
507 networks[index] = {network: network, networkId: self.getNetworkId(network.id, network.type)};
508 }, self);
510 self.updateAllStats(function onUpdate(aResult, aMessage){
511 if (!aResult) {
512 mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
513 { id: msg.id, error: aMessage, result: null });
514 return;
515 }
517 self._db.clearStats(networks, function onDBCleared(aError, aResult) {
518 networks.forEach(function(network, index) {
519 self._updateCurrentAlarm(network.networkId);
520 }, self);
521 mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
522 { id: msg.id, error: aError, result: aResult });
523 });
524 });
525 });
526 },
528 updateAllStats: function updateAllStats(aCallback) {
529 let elements = [];
530 let lastElement = null;
531 let callback = (function (success, message) {
532 this.updateCachedStats(aCallback);
533 }).bind(this);
535 // For each connectionType create an object containning the type
536 // and the 'queueIndex', the 'queueIndex' is an integer representing
537 // the index of a connection type in the global queue array. So, if
538 // the connection type is already in the queue it is not appended again,
539 // else it is pushed in 'elements' array, which later will be pushed to
540 // the queue array.
541 for (let netId in this._networks) {
542 if (this._networks[netId].status != NETWORK_STATUS_READY) {
543 continue;
544 }
546 lastElement = { netId: netId,
547 queueIndex: this.updateQueueIndex(netId) };
549 if (lastElement.queueIndex == -1) {
550 elements.push({ netId: lastElement.netId,
551 callbacks: [],
552 queueType: QUEUE_TYPE_UPDATE_STATS });
553 }
554 }
556 if (!lastElement) {
557 // No elements need to be updated, probably because status is different than
558 // NETWORK_STATUS_READY.
559 if (aCallback) {
560 aCallback(true, "OK");
561 }
562 return;
563 }
565 if (elements.length > 0) {
566 // If length of elements is greater than 0, callback is set to
567 // the last element.
568 elements[elements.length - 1].callbacks.push(callback);
569 this.updateQueue = this.updateQueue.concat(elements);
570 } else {
571 // Else, it means that all connection types are already in the queue to
572 // be updated, so callback for this request is added to
573 // the element in the main queue with the index of the last 'lastElement'.
574 // But before is checked that element is still in the queue because it can
575 // be processed while generating 'elements' array.
576 let element = this.updateQueue[lastElement.queueIndex];
577 if (aCallback &&
578 (!element || element.netId != lastElement.netId)) {
579 aCallback();
580 return;
581 }
583 this.updateQueue[lastElement.queueIndex].callbacks.push(callback);
584 }
586 // Call the function that process the elements of the queue.
587 this.processQueue();
589 if (DEBUG) {
590 this.logAllRecords();
591 }
592 },
594 updateStats: function updateStats(aNetId, aCallback) {
595 // Check if the connection is in the main queue, push a new element
596 // if it is not being processed or add a callback if it is.
597 let index = this.updateQueueIndex(aNetId);
598 if (index == -1) {
599 this.updateQueue.push({ netId: aNetId,
600 callbacks: [aCallback],
601 queueType: QUEUE_TYPE_UPDATE_STATS });
602 } else {
603 this.updateQueue[index].callbacks.push(aCallback);
604 return;
605 }
607 // Call the function that process the elements of the queue.
608 this.processQueue();
609 },
611 /*
612 * Find if a connection is in the main queue array and return its
613 * index, if it is not in the array return -1.
614 */
615 updateQueueIndex: function updateQueueIndex(aNetId) {
616 return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId);
617 },
619 /*
620 * Function responsible of process all requests in the queue.
621 */
622 processQueue: function processQueue(aResult, aMessage) {
623 // If aResult is not undefined, the caller of the function is the result
624 // of processing an element, so remove that element and call the callbacks
625 // it has.
626 if (aResult != undefined) {
627 let item = this.updateQueue.shift();
628 for (let callback of item.callbacks) {
629 if (callback) {
630 callback(aResult, aMessage);
631 }
632 }
633 } else {
634 // The caller is a function that has pushed new elements to the queue,
635 // if isQueueRunning is false it means there is no processing currently
636 // being done, so start.
637 if (this.isQueueRunning) {
638 return;
639 } else {
640 this.isQueueRunning = true;
641 }
642 }
644 // Check length to determine if queue is empty and stop processing.
645 if (this.updateQueue.length < 1) {
646 this.isQueueRunning = false;
647 return;
648 }
650 // Call the update function for the next element.
651 switch (this.updateQueue[0].queueType) {
652 case QUEUE_TYPE_UPDATE_STATS:
653 this.update(this.updateQueue[0].netId, this.processQueue.bind(this));
654 break;
655 case QUEUE_TYPE_UPDATE_CACHE:
656 this.updateCache(this.processQueue.bind(this));
657 break;
658 case QUEUE_TYPE_WRITE_CACHE:
659 this.writeCache(this.updateQueue[0].stats, this.processQueue.bind(this));
660 break;
661 }
662 },
664 update: function update(aNetId, aCallback) {
665 // Check if connection type is valid.
666 if (!this._networks[aNetId]) {
667 if (aCallback) {
668 aCallback(false, "Invalid network " + aNetId);
669 }
670 return;
671 }
673 let interfaceName = this._networks[aNetId].interfaceName;
674 debug("Update stats for " + interfaceName);
676 // Request stats to NetworkService, which will get stats from netd, passing
677 // 'networkStatsAvailable' as a callback.
678 if (interfaceName) {
679 networkService.getNetworkInterfaceStats(interfaceName,
680 this.networkStatsAvailable.bind(this, aCallback, aNetId));
681 return;
682 }
684 if (aCallback) {
685 aCallback(true, "ok");
686 }
687 },
689 /*
690 * Callback of request stats. Store stats in database.
691 */
692 networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId,
693 aResult, aRxBytes,
694 aTxBytes, aDate) {
695 if (!aResult) {
696 if (aCallback) {
697 aCallback(false, "Netd IPC error");
698 }
699 return;
700 }
702 let stats = { appId: 0,
703 serviceType: "",
704 networkId: this._networks[aNetId].network.id,
705 networkType: this._networks[aNetId].network.type,
706 date: aDate,
707 rxBytes: aTxBytes,
708 txBytes: aRxBytes,
709 isAccumulative: true };
711 debug("Update stats for: " + JSON.stringify(stats));
713 this._db.saveStats(stats, function onSavedStats(aError, aResult) {
714 if (aCallback) {
715 if (aError) {
716 aCallback(false, aError);
717 return;
718 }
720 aCallback(true, "OK");
721 }
722 });
723 },
725 /*
726 * Function responsible for receiving stats which are not from netd.
727 */
728 saveStats: function saveStats(aAppId, aServiceType, aNetwork, aTimeStamp,
729 aRxBytes, aTxBytes, aIsAccumulative,
730 aCallback) {
731 let netId = this.convertNetworkInterface(aNetwork);
732 if (!netId) {
733 if (aCallback) {
734 aCallback(false, "Invalid network type");
735 }
736 return;
737 }
739 // Check if |aConnectionType|, |aAppId| and |aServiceType| are valid.
740 // There are two invalid cases for the combination of |aAppId| and
741 // |aServiceType|:
742 // a. Both |aAppId| is non-zero and |aServiceType| is non-empty.
743 // b. Both |aAppId| is zero and |aServiceType| is empty.
744 if (!this._networks[netId] || (aAppId && aServiceType) ||
745 (!aAppId && !aServiceType)) {
746 debug("Invalid network interface, appId or serviceType");
747 return;
748 }
750 let stats = { appId: aAppId,
751 serviceType: aServiceType,
752 networkId: this._networks[netId].network.id,
753 networkType: this._networks[netId].network.type,
754 date: new Date(aTimeStamp),
755 rxBytes: aRxBytes,
756 txBytes: aTxBytes,
757 isAccumulative: aIsAccumulative };
759 this.updateQueue.push({ stats: stats,
760 callbacks: [aCallback],
761 queueType: QUEUE_TYPE_WRITE_CACHE });
763 this.processQueue();
764 },
766 /*
767 *
768 */
769 writeCache: function writeCache(aStats, aCallback) {
770 debug("saveStats: " + aStats.appId + " " + aStats.serviceType + " " +
771 aStats.networkId + " " + aStats.networkType + " " + aStats.date + " "
772 + aStats.date + " " + aStats.rxBytes + " " + aStats.txBytes);
774 // Generate an unique key from |appId|, |serviceType| and |netId|,
775 // which is used to retrieve data in |cachedStats|.
776 let netId = this.getNetworkId(aStats.networkId, aStats.networkType);
777 let key = aStats.appId + "" + aStats.serviceType + "" + netId;
779 // |cachedStats| only keeps the data with the same date.
780 // If the incoming date is different from |cachedStatsDate|,
781 // both |cachedStats| and |cachedStatsDate| will get updated.
782 let diff = (this._db.normalizeDate(aStats.date) -
783 this._db.normalizeDate(this.cachedStatsDate)) /
784 this._db.sampleRate;
785 if (diff != 0) {
786 this.updateCache(function onUpdated(success, message) {
787 this.cachedStatsDate = aStats.date;
788 this.cachedStats[key] = aStats;
790 if (aCallback) {
791 aCallback(true, "ok");
792 }
793 }.bind(this));
794 return;
795 }
797 // Try to find the matched row in the cached by |appId| and |connectionType|.
798 // If not found, save the incoming data into the cached.
799 let cachedStats = this.cachedStats[key];
800 if (!cachedStats) {
801 this.cachedStats[key] = aStats;
802 if (aCallback) {
803 aCallback(true, "ok");
804 }
805 return;
806 }
808 // Find matched row, accumulate the traffic amount.
809 cachedStats.rxBytes += aStats.rxBytes;
810 cachedStats.txBytes += aStats.txBytes;
812 // If new rxBytes or txBytes exceeds MAX_CACHED_TRAFFIC
813 // the corresponding row will be saved to indexedDB.
814 // Then, the row will be removed from the cached.
815 if (cachedStats.rxBytes > MAX_CACHED_TRAFFIC ||
816 cachedStats.txBytes > MAX_CACHED_TRAFFIC) {
817 this._db.saveStats(cachedStats, function (error, result) {
818 debug("Application stats inserted in indexedDB");
819 if (aCallback) {
820 aCallback(true, "ok");
821 }
822 });
823 delete this.cachedStats[key];
824 return;
825 }
827 if (aCallback) {
828 aCallback(true, "ok");
829 }
830 },
832 updateCachedStats: function updateCachedStats(aCallback) {
833 this.updateQueue.push({ callbacks: [aCallback],
834 queueType: QUEUE_TYPE_UPDATE_CACHE });
836 this.processQueue();
837 },
839 updateCache: function updateCache(aCallback) {
840 debug("updateCache: " + this.cachedStatsDate);
842 let stats = Object.keys(this.cachedStats);
843 if (stats.length == 0) {
844 // |cachedStats| is empty, no need to update.
845 if (aCallback) {
846 aCallback(true, "no need to update");
847 }
848 return;
849 }
851 let index = 0;
852 this._db.saveStats(this.cachedStats[stats[index]],
853 function onSavedStats(error, result) {
854 debug("Application stats inserted in indexedDB");
856 // Clean up the |cachedStats| after updating.
857 if (index == stats.length - 1) {
858 this.cachedStats = Object.create(null);
860 if (aCallback) {
861 aCallback(true, "ok");
862 }
863 return;
864 }
866 // Update is not finished, keep updating.
867 index += 1;
868 this._db.saveStats(this.cachedStats[stats[index]],
869 onSavedStats.bind(this, error, result));
870 }.bind(this));
871 },
873 get maxCachedTraffic () {
874 return MAX_CACHED_TRAFFIC;
875 },
877 logAllRecords: function logAllRecords() {
878 this._db.logAllRecords(function onResult(aError, aResult) {
879 if (aError) {
880 debug("Error: " + aError);
881 return;
882 }
884 debug("===== LOG =====");
885 debug("There are " + aResult.length + " items");
886 debug(JSON.stringify(aResult));
887 });
888 },
890 getAlarms: function getAlarms(mm, msg) {
891 let self = this;
892 let network = msg.data.network;
893 let manifestURL = msg.data.manifestURL;
895 if (network) {
896 this.validateNetwork(network, function onValidateNetwork(aNetId) {
897 if (!aNetId) {
898 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
899 { id: msg.id, error: "InvalidInterface", result: null });
900 return;
901 }
903 self._getAlarms(mm, msg, aNetId, manifestURL);
904 });
905 return;
906 }
908 this._getAlarms(mm, msg, null, manifestURL);
909 },
911 _getAlarms: function _getAlarms(mm, msg, aNetId, aManifestURL) {
912 let self = this;
913 this._db.getAlarms(aNetId, aManifestURL, function onCompleted(error, result) {
914 if (error) {
915 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
916 { id: msg.id, error: error, result: result });
917 return;
918 }
920 let alarms = []
921 // NetworkStatsManager must return the network instead of the networkId.
922 for (let i = 0; i < result.length; i++) {
923 let alarm = result[i];
924 alarms.push({ id: alarm.id,
925 network: self._networks[alarm.networkId].network,
926 threshold: alarm.absoluteThreshold,
927 data: alarm.data });
928 }
930 mm.sendAsyncMessage("NetworkStats:GetAlarms:Return",
931 { id: msg.id, error: null, result: alarms });
932 });
933 },
935 removeAlarms: function removeAlarms(mm, msg) {
936 let alarmId = msg.data.alarmId;
937 let manifestURL = msg.data.manifestURL;
939 let self = this;
940 let callback = function onRemove(error, result) {
941 if (error) {
942 mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return",
943 { id: msg.id, error: error, result: result });
944 return;
945 }
947 for (let i in self._currentAlarms) {
948 let currentAlarm = self._currentAlarms[i].alarm;
949 if (currentAlarm && ((alarmId == currentAlarm.id) ||
950 (alarmId == -1 && currentAlarm.manifestURL == manifestURL))) {
952 self._updateCurrentAlarm(currentAlarm.networkId);
953 }
954 }
956 mm.sendAsyncMessage("NetworkStats:RemoveAlarms:Return",
957 { id: msg.id, error: error, result: true });
958 };
960 if (alarmId == -1) {
961 this._db.removeAlarms(manifestURL, callback);
962 } else {
963 this._db.removeAlarm(alarmId, manifestURL, callback);
964 }
965 },
967 /*
968 * Function called from manager to set an alarm.
969 */
970 setAlarm: function setAlarm(mm, msg) {
971 let options = msg.data;
972 let network = options.network;
973 let threshold = options.threshold;
975 debug("Set alarm at " + threshold + " for " + JSON.stringify(network));
977 if (threshold < 0) {
978 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
979 { id: msg.id, error: "InvalidThresholdValue", result: null });
980 return;
981 }
983 let self = this;
984 this.validateNetwork(network, function onValidateNetwork(aNetId) {
985 if (!aNetId) {
986 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
987 { id: msg.id, error: "InvalidiConnectionType", result: null });
988 return;
989 }
991 let newAlarm = {
992 id: null,
993 networkId: aNetId,
994 absoluteThreshold: threshold,
995 relativeThreshold: null,
996 startTime: options.startTime,
997 data: options.data,
998 pageURL: options.pageURL,
999 manifestURL: options.manifestURL
1000 };
1002 self._getAlarmQuota(newAlarm, function onUpdate(error, quota) {
1003 if (error) {
1004 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
1005 { id: msg.id, error: error, result: null });
1006 return;
1007 }
1009 self._db.addAlarm(newAlarm, function addSuccessCb(error, newId) {
1010 if (error) {
1011 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
1012 { id: msg.id, error: error, result: null });
1013 return;
1014 }
1016 newAlarm.id = newId;
1017 self._setAlarm(newAlarm, function onSet(error, success) {
1018 mm.sendAsyncMessage("NetworkStats:SetAlarm:Return",
1019 { id: msg.id, error: error, result: newId });
1021 if (error == "InvalidStateError") {
1022 self._fireAlarm(newAlarm);
1023 }
1024 });
1025 });
1026 });
1027 });
1028 },
1030 _setAlarm: function _setAlarm(aAlarm, aCallback) {
1031 let currentAlarm = this._currentAlarms[aAlarm.networkId];
1032 if ((Object.getOwnPropertyNames(currentAlarm).length !== 0 &&
1033 aAlarm.relativeThreshold > currentAlarm.alarm.relativeThreshold) ||
1034 this._networks[aAlarm.networkId].status != NETWORK_STATUS_READY) {
1035 aCallback(null, true);
1036 return;
1037 }
1039 let self = this;
1041 this._getAlarmQuota(aAlarm, function onUpdate(aError, aQuota) {
1042 if (aError) {
1043 aCallback(aError, null);
1044 return;
1045 }
1047 let callback = function onAlarmSet(aError) {
1048 if (aError) {
1049 debug("Set alarm error: " + aError);
1050 aCallback("netdError", null);
1051 return;
1052 }
1054 self._currentAlarms[aAlarm.networkId].alarm = aAlarm;
1056 aCallback(null, true);
1057 };
1059 debug("Set alarm " + JSON.stringify(aAlarm));
1060 let interfaceName = self._networks[aAlarm.networkId].interfaceName;
1061 if (interfaceName) {
1062 networkService.setNetworkInterfaceAlarm(interfaceName,
1063 aQuota,
1064 callback);
1065 return;
1066 }
1068 aCallback(null, true);
1069 });
1070 },
1072 _getAlarmQuota: function _getAlarmQuota(aAlarm, aCallback) {
1073 let self = this;
1074 this.updateStats(aAlarm.networkId, function onStatsUpdated(aResult, aMessage) {
1075 self._db.getCurrentStats(self._networks[aAlarm.networkId].network,
1076 aAlarm.startTime,
1077 function onStatsFound(error, result) {
1078 if (error) {
1079 debug("Error getting stats for " +
1080 JSON.stringify(self._networks[aAlarm.networkId]) + ": " + error);
1081 aCallback(error, result);
1082 return;
1083 }
1085 let quota = aAlarm.absoluteThreshold - result.rxBytes - result.txBytes;
1087 // Alarm set to a threshold lower than current rx/tx bytes.
1088 if (quota <= 0) {
1089 aCallback("InvalidStateError", null);
1090 return;
1091 }
1093 aAlarm.relativeThreshold = aAlarm.startTime
1094 ? result.rxTotalBytes + result.txTotalBytes + quota
1095 : aAlarm.absoluteThreshold;
1097 aCallback(null, quota);
1098 });
1099 });
1100 },
1102 _fireAlarm: function _fireAlarm(aAlarm) {
1103 debug("Fire alarm");
1105 let self = this;
1106 this._db.removeAlarm(aAlarm.id, null, function onRemove(aError, aResult){
1107 if (!aError && !aResult) {
1108 return;
1109 }
1111 self._fireSystemMessage(aAlarm);
1112 self._updateCurrentAlarm(aAlarm.networkId);
1113 });
1114 },
1116 _updateCurrentAlarm: function _updateCurrentAlarm(aNetworkId) {
1117 this._currentAlarms[aNetworkId] = Object.create(null);
1119 let self = this;
1120 this._db.getFirstAlarm(aNetworkId, function onGet(error, result){
1121 if (error) {
1122 debug("Error getting the first alarm");
1123 return;
1124 }
1126 if (!result) {
1127 let interfaceName = self._networks[aNetworkId].interfaceName;
1128 networkService.setNetworkInterfaceAlarm(interfaceName, -1,
1129 function onComplete(){});
1130 return;
1131 }
1133 self._setAlarm(result, function onSet(error, success){
1134 if (error == "InvalidStateError") {
1135 self._fireAlarm(result);
1136 return;
1137 }
1138 });
1139 });
1140 },
1142 _fireSystemMessage: function _fireSystemMessage(aAlarm) {
1143 debug("Fire system message: " + JSON.stringify(aAlarm));
1145 let manifestURI = Services.io.newURI(aAlarm.manifestURL, null, null);
1146 let pageURI = Services.io.newURI(aAlarm.pageURL, null, null);
1148 let alarm = { "id": aAlarm.id,
1149 "threshold": aAlarm.absoluteThreshold,
1150 "data": aAlarm.data };
1151 messenger.sendMessage("networkstats-alarm", alarm, pageURI, manifestURI);
1152 }
1153 };
1155 NetworkStatsService.init();