Bug 973543 - Part 1: Add reference counting to added routes. r=echen, a=2.2r+
authorJessica Jong <jjong@mozilla.com>
Thu, 20 Aug 2015 19:26:00 -0400
changeset 238849 608de6dfa1dd4f48855c37e88d7a75392cedf415
parent 238848 ac22cb098d887bd6dfcc02e7a99cc65d975e70cb
child 238850 9d6a45e199a1523de4cead96d79ad102712b5402
push id35
push userryanvm@gmail.com
push dateMon, 24 Aug 2015 15:58:41 +0000
reviewersechen, 2
bugs973543
milestone37.0
Bug 973543 - Part 1: Add reference counting to added routes. r=echen, a=2.2r+
dom/system/gonk/NetworkManager.js
dom/system/gonk/NetworkService.js
dom/system/gonk/NetworkUtils.cpp
--- a/dom/system/gonk/NetworkManager.js
+++ b/dom/system/gonk/NetworkManager.js
@@ -129,22 +129,61 @@ try {
 
 function defineLazyRegExp(obj, name, pattern) {
   obj.__defineGetter__(name, function() {
     delete obj[name];
     return obj[name] = new RegExp(pattern);
   });
 }
 
+function NetworkInterfaceLinks()
+{
+  this.resetLinks();
+}
+NetworkInterfaceLinks.prototype = {
+  linkRoutes: null,
+  gateways: null,
+  interfaceName: null,
+  extraRoutes: null,
+
+  setLinks: function(linkRoutes, gateways, interfaceName) {
+    this.linkRoutes = linkRoutes;
+    this.gateways = gateways;
+    this.interfaceName = interfaceName;
+  },
+
+  resetLinks: function() {
+    this.linkRoutes = [];
+    this.gateways = [];
+    this.interfaceName = "";
+    this.extraRoutes = [];
+  },
+
+  compareGateways: function(gateways) {
+    if (this.gateways.length != gateways.length) {
+      return false;
+    }
+
+    for (let i = 0; i < this.gateways.length; i++) {
+      if (this.gateways[i] != gateways[i]) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+};
+
 /**
  * This component watches for network interfaces changing state and then
  * adjusts routes etc. accordingly.
  */
 function NetworkManager() {
   this.networkInterfaces = {};
+  this.networkInterfaceLinks = {};
   Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false);
   Services.obs.addObserver(this, TOPIC_MOZSETTINGS_CHANGED, false);
 
   try {
     this._manageOfflineStatus =
       Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS);
   } catch(ex) {
     // Ignore.
@@ -300,29 +339,33 @@ NetworkManager.prototype = {
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     let networkId = this.getNetworkId(network);
     if (networkId in this.networkInterfaces) {
       throw Components.Exception("Network with that type already registered!",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     this.networkInterfaces[networkId] = network;
+    this.networkInterfaceLinks[networkId] = new NetworkInterfaceLinks();
 
     Services.obs.notifyObservers(network, TOPIC_INTERFACE_REGISTERED, null);
     debug("Network '" + networkId + "' registered.");
   },
 
   _addSubnetRoutes: function(network) {
     let ips = {};
     let prefixLengths = {};
     let length = network.getAddresses(ips, prefixLengths);
     for (let i = 0; i < length; i++) {
       debug('Adding subnet routes: ' + ips.value[i] + '/' + prefixLengths.value[i]);
       gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD,
-                                  network.name, ips.value[i], prefixLengths.value[i]);
+                                  network.name, ips.value[i], prefixLengths.value[i])
+        .catch((aError) => {
+          debug("_addSubnetRoutes error: " + aError);
+        });
     }
   },
 
   updateNetworkInterface: function(network) {
     if (!(network instanceof Ci.nsINetworkInterface)) {
       throw Components.Exception("Argument must be nsINetworkInterface.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
@@ -347,18 +390,27 @@ NetworkManager.prototype = {
           gNetworkService.removeDefaultRoute(network);
 
           // Set DNS server as early as possible to prevent from
           // premature domain name lookup.
           gNetworkService.setDNS(network, () => {
 
             // Add host route for data calls
             if (this.isNetworkTypeMobile(network.type)) {
-              gNetworkService.removeHostRoutes(network.name);
-              this.setHostRoutes(network);
+              let currentInterfaceLinks = this.networkInterfaceLinks[networkId];
+              let newLinkRoutes = network.getDnses().concat(network.httpProxyHost);
+              // If gateways have changed, remove all old routes first.
+              this._handleGateways(networkId, network.getGateways())
+                .then(() => this._updateRoutes(currentInterfaceLinks.linkRoutes,
+                                               newLinkRoutes,
+                                               network.getGateways(), network.name))
+                .then(() => currentInterfaceLinks.setLinks(newLinkRoutes,
+                                                           network.getGateways(),
+                                                           network.name));
+
             }
 
             // Dun type is a special case where we add the default route to a
             // secondary table.
             if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN) {
               this.setSecondaryDefaultRoute(network);
             }
 
@@ -384,17 +436,17 @@ NetworkManager.prototype = {
                                          this.convertConnectionType(network));
           });
         });
 
         break;
       case Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED:
         // Remove host route for data calls
         if (this.isNetworkTypeMobile(network.type)) {
-          this.removeHostRoutes(network);
+          this._cleanupAllHostRoutes(networkId);
         }
         // Remove secondary default route for dun.
         if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN) {
           this.removeSecondaryDefaultRoute(network);
         }
         // Remove routing table in /proc/net/route
         if (network.type == Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) {
           gNetworkService.resetRoutingTable(network);
@@ -429,26 +481,34 @@ NetworkManager.prototype = {
       throw Components.Exception("Argument must be nsINetworkInterface.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
     let networkId = this.getNetworkId(network);
     if (!(networkId in this.networkInterfaces)) {
       throw Components.Exception("No network with that type registered.",
                                  Cr.NS_ERROR_INVALID_ARG);
     }
+    // This is for in case a network gets unregistered without being
+    // DISCONNECTED.
+    if (this.isNetworkTypeMobile(network.type)) {
+      this._cleanupAllHostRoutes(networkId);
+    }
+
     delete this.networkInterfaces[networkId];
 
     Services.obs.notifyObservers(network, TOPIC_INTERFACE_UNREGISTERED, null);
     debug("Network '" + networkId + "' unregistered.");
   },
 
   _manageOfflineStatus: true,
 
   networkInterfaces: null,
 
+  networkInterfaceLinks: null,
+
   _dataDefaultServiceId: null,
 
   _preferredNetworkType: DEFAULT_PREFERRED_NETWORK_TYPE,
   get preferredNetworkType() {
     return this._preferredNetworkType;
   },
   set preferredNetworkType(val) {
     if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
@@ -466,17 +526,34 @@ NetworkManager.prototype = {
          Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE].indexOf(val) == -1) {
       throw "Invalid network type";
     }
 
     this._overriddenActive = network;
     this.setAndConfigureActive();
   },
 
-  _updateRoutes: function(doAdd, ipAddresses, networkName, gateways) {
+  _updateRoutes: function(oldLinks, newLinks, gateways, interfaceName) {
+    // Returns items that are in base but not in target.
+    function getDifference(base, target) {
+      return base.filter(function(i) { return target.indexOf(i) < 0; });
+    }
+
+    let addedLinks = getDifference(newLinks, oldLinks);
+    let removedLinks = getDifference(oldLinks, newLinks);
+
+    if (addedLinks.length === 0 && removedLinks.length === 0) {
+      return Promise.resolve();
+    }
+
+    return this._setHostRoutes(false, removedLinks, interfaceName, gateways)
+      .then(this._setHostRoutes(true, addedLinks, interfaceName, gateways));
+  },
+
+  _setHostRoutes: function(doAdd, ipAddresses, networkName, gateways) {
     let getMaxPrefixLength = (aIp) => {
       return aIp.match(this.REGEXP_IPV4) ? IPV4_MAX_PREFIX_LENGTH : IPV6_MAX_PREFIX_LENGTH;
     }
 
     let promises = [];
 
     ipAddresses.forEach((aIpAddress) => {
       let gateway = this.selectGateway(gateways, aIpAddress);
@@ -506,59 +583,104 @@ NetworkManager.prototype = {
   },
 
   addHostRoute: function(network, host) {
     if (!this.isValidatedNetwork(network)) {
       return Promise.reject("Invalid network interface.");
     }
 
     return this.resolveHostname(network, host)
-      .then((ipAddresses) => this._updateRoutes(true,
-                                                ipAddresses,
-                                                network.name,
-                                                network.getGateways()));
+      .then((ipAddresses) => {
+        let promises = [];
+        let networkId = this.getNetworkId(network);
+
+        ipAddresses.forEach((aIpAddress) => {
+          let promise =
+            this._setHostRoutes(true, [aIpAddress], network.name, network.getGateways())
+              .then(() => this.networkInterfaceLinks[networkId].extraRoutes.push(aIpAddress));
+
+          promises.push(promise);
+        });
+
+        return Promise.all(promises);
+      });
   },
 
   removeHostRoute: function(network, host) {
     if (!this.isValidatedNetwork(network)) {
       return Promise.reject("Invalid network interface.");
     }
 
     return this.resolveHostname(network, host)
-      .then((ipAddresses) => this._updateRoutes(false,
-                                                ipAddresses,
-                                                network.name,
-                                                network.getGateways()));
+      .then((ipAddresses) => {
+        let promises = [];
+        let networkId = this.getNetworkId(network);
+
+        ipAddresses.forEach((aIpAddress) => {
+          let found = this.networkInterfaceLinks[networkId].extraRoutes.indexOf(aIpAddress);
+          if (found < 0) {
+            return; // continue
+          }
+
+          let promise =
+            this._setHostRoutes(false, [aIpAddress], network.name, network.getGateways())
+              .then(() => {
+                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
+              }, () => {
+                // We should remove it even if the operation failed.
+                this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1);
+              });
+          promises.push(promise);
+        });
+
+        return Promise.all(promises);
+      });
   },
 
   isNetworkTypeSecondaryMobile: function(type) {
     return (type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_MMS ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_IMS ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN ||
             type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_FOTA);
   },
 
   isNetworkTypeMobile: function(type) {
     return (type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE ||
             this.isNetworkTypeSecondaryMobile(type));
   },
 
-  setHostRoutes: function(network) {
-    let hosts = network.getDnses().concat(network.httpProxyHost);
-
-    return this._updateRoutes(true, hosts, network.name, network.getGateways());
+  _handleGateways: function(networkId, gateways) {
+    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
+    if (currentNetworkLinks.gateways.length == 0 ||
+        currentNetworkLinks.compareGateways(gateways)) {
+      return Promise.resolve();
+    }
   },
 
-  removeHostRoutes: function(network) {
-    let hosts = network.getDnses().concat(network.httpProxyHost);
+  _cleanupAllHostRoutes: function(networkId) {
+    let currentNetworkLinks = this.networkInterfaceLinks[networkId];
+    let hostRoutes = currentNetworkLinks.linkRoutes.concat(
+      currentNetworkLinks.extraRoutes);
+
+    if (hostRoutes.length === 0) {
+      return Promise.resolve();
+    }
 
-    return this._updateRoutes(false, hosts, network.name, network.getGateways());
+    return this._setHostRoutes(false,
+                               hostRoutes,
+                               currentNetworkLinks.interfaceName,
+                               currentNetworkLinks.gateways)
+      .catch((aError) => {
+        debug("Error (" + aError + ") on _cleanupAllHostRoutes, keep proceeding.");
+      })
+      .then(() => currentNetworkLinks.resetLinks());
   },
 
+
   selectGateway: function(gateways, host) {
     for (let i = 0; i < gateways.length; i++) {
       let gateway = gateways[i];
       if (gateway.match(this.REGEXP_IPV4) && host.match(this.REGEXP_IPV4) ||
           gateway.indexOf(":") != -1 && host.indexOf(":") != -1) {
         return gateway;
       }
     }
--- a/dom/system/gonk/NetworkService.js
+++ b/dom/system/gonk/NetworkService.js
@@ -51,16 +51,76 @@ function isError(code) {
   let type = netdResponseType(code);
   return (type !== NETD_COMMAND_PROCEEDING && type !== NETD_COMMAND_OKAY);
 }
 
 function debug(msg) {
   dump("-*- NetworkService: " + msg + "\n");
 }
 
+function Task(id, params, setupFunction) {
+  this.id = id;
+  this.params = params;
+  this.setupFunction = setupFunction;
+}
+
+function NetworkWorkerRequestQueue(networkService) {
+  this.networkService = networkService;
+  this.tasks = [];
+}
+NetworkWorkerRequestQueue.prototype = {
+  runQueue: function() {
+    if (this.tasks.length === 0) {
+      return;
+    }
+
+    let task = this.tasks[0];
+    if (DEBUG) debug("run task id: " + task.id);
+
+    if (typeof task.setupFunction === 'function') {
+      // If setupFunction returns false, skip sending to Network Worker but call
+      // handleWorkerMessage() directly with task id, as if the response was
+      // returned from Network Worker.
+      if (!task.setupFunction()) {
+        this.networkService.handleWorkerMessage({id: task.id});
+        return;
+      }
+    }
+
+    gNetworkWorker.postMessage(task.params);
+  },
+
+  enqueue: function(id, params, setupFunction) {
+    if (DEBUG) debug("enqueue id: " + id);
+    this.tasks.push(new Task(id, params, setupFunction));
+
+    if (this.tasks.length === 1) {
+      this.runQueue();
+    }
+  },
+
+  dequeue: function(id) {
+    if (DEBUG) debug("dequeue id: " + id);
+
+    if (!this.tasks.length || this.tasks[0].id != id) {
+      if (DEBUG) debug("Id " + id + " is not on top of the queue");
+      return;
+    }
+
+    this.tasks.shift();
+    if (this.tasks.length > 0) {
+      // Run queue on the next tick.
+      Services.tm.currentThread.dispatch(() => {
+        this.runQueue();
+      }, Ci.nsIThread.DISPATCH_NORMAL);
+    }
+  }
+};
+
+
 /**
  * This component watches for network interfaces changing state and then
  * adjusts routes etc. accordingly.
  */
 function NetworkService() {
   if(DEBUG) debug("Starting net_worker.");
 
   let self = this;
@@ -71,41 +131,52 @@ function NetworkService() {
         self.handleWorkerMessage(event);
       }
     };
     gNetworkWorker.start(networkListener);
   }
   // Callbacks to invoke when a reply arrives from the net_worker.
   this.controlCallbacks = Object.create(null);
 
+  this.addedRoutes = new Map();
+  this.netWorkerRequestQueue = new NetworkWorkerRequestQueue(this);
   this.shutdown = false;
   Services.obs.addObserver(this, "xpcom-shutdown", false);
 }
 
 NetworkService.prototype = {
   classID:   NETWORKSERVICE_CID,
   classInfo: XPCOMUtils.generateCI({classID: NETWORKSERVICE_CID,
                                     contractID: NETWORKSERVICE_CONTRACTID,
                                     classDescription: "Network Service",
                                     interfaces: [Ci.nsINetworkService]}),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkService]),
 
   // Helpers
 
+  addedRoutes: null,
   idgen: 0,
-  controlMessage: function(params, callback) {
+  controlMessage: function(params, callback, setupFunction) {
     if (this.shutdown) {
       return;
     }
 
+    let id = this.idgen++;
+    params.id = id;
     if (callback) {
-      let id = this.idgen++;
-      params.id = id;
       this.controlCallbacks[id] = callback;
     }
+
+    // For now, we use setupFunction to determine if this command needs to be
+    // queued or not.
+    if (setupFunction) {
+      this.netWorkerRequestQueue.enqueue(id, params, setupFunction);
+      return;
+    }
+
     if (gNetworkWorker) {
       gNetworkWorker.postMessage(params);
     }
   },
 
   handleWorkerMessage: function(response) {
     if(DEBUG) debug("NetworkManager received message from worker: " + JSON.stringify(response));
     let id = response.id;
@@ -113,16 +184,18 @@ NetworkService.prototype = {
       Services.obs.notifyObservers(null, response.topic, response.reason);
       return;
     }
     let callback = this.controlCallbacks[id];
     if (callback) {
       callback.call(this, response);
       delete this.controlCallbacks[id];
     }
+
+    this.netWorkerRequestQueue.dequeue(id);
   },
 
   // nsINetworkService
 
   getNetworkInterfaceStats: function(networkName, callback) {
     if(DEBUG) debug("getNetworkInterfaceStats for " + networkName);
 
     let file = new FileUtils.File("/proc/net/dev");
@@ -298,48 +371,84 @@ NetworkService.prototype = {
     let options = {
       cmd: "removeDefaultRoute",
       ifname: network.name,
       gateways: gateways
     };
     this.controlMessage(options);
   },
 
+  _routeToString: function(interfaceName, host, prefixLength, gateway) {
+    return host + "-" + prefixLength + "-" + gateway + "-" + interfaceName;
+  },
+
   modifyRoute: function(action, interfaceName, host, prefixLength, gateway) {
     let command;
 
     switch (action) {
       case Ci.nsINetworkService.MODIFY_ROUTE_ADD:
         command = 'addHostRoute';
         break;
       case Ci.nsINetworkService.MODIFY_ROUTE_REMOVE:
         command = 'removeHostRoute';
         break;
       default:
         if (DEBUG) debug('Unknown action: ' + action);
         return Promise.reject();
     }
 
+    let route = this._routeToString(interfaceName, host, prefixLength, gateway);
+    let setupFunc = () => {
+      let count = this.addedRoutes.get(route);
+      if (DEBUG) debug(command + ": " + route + " -> " + count);
+
+      // Return false if there is no need to send the command to network worker.
+      if ((action == Ci.nsINetworkService.MODIFY_ROUTE_ADD && count) ||
+          (action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE &&
+           (!count || count > 1))) {
+        return false;
+      }
+
+      return true;
+    };
+
     if (DEBUG) debug(command + " " + host + " on " + interfaceName);
-    let deferred = Promise.defer();
     let options = {
       cmd: command,
       ifname: interfaceName,
       gateway: gateway,
       prefixLength: prefixLength,
       ip: host
     };
-    this.controlMessage(options, function(data) {
-      if (data.error) {
-        deferred.reject(data.reason);
-        return;
-      }
-      deferred.resolve();
+
+    return new Promise((aResolve, aReject) => {
+      this.controlMessage(options, (data) => {
+        let count = this.addedRoutes.get(route);
+
+        // Remove route from addedRoutes on success or failure.
+        if (action == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE) {
+          if (count > 1) {
+            this.addedRoutes.set(route, count - 1);
+          } else {
+            this.addedRoutes.delete(route);
+          }
+        }
+
+        if (data.error) {
+          aReject(data.reason);
+          return;
+        }
+
+        if (action == Ci.nsINetworkService.MODIFY_ROUTE_ADD) {
+          this.addedRoutes.set(route, count ? count + 1 : 1);
+        }
+
+        aResolve();
+      }, setupFunc);
     });
-    return deferred.promise;
   },
 
   removeHostRoutes: function(ifname) {
     if(DEBUG) debug("Going to remove all host routes on " + ifname);
     let options = {
       cmd: "removeHostRoutes",
       ifname: ifname,
     };
--- a/dom/system/gonk/NetworkUtils.cpp
+++ b/dom/system/gonk/NetworkUtils.cpp
@@ -2089,16 +2089,21 @@ CommandResult NetworkUtils::addHostRoute
   return CommandResult::Pending();
 }
 
 /**
  * Add host route for given network interface.
  */
 CommandResult NetworkUtils::addHostRouteLegacy(NetworkParams& aOptions)
 {
+  if (aOptions.mGateway.IsEmpty()) {
+    ERROR("addHostRouteLegacy does not support empty gateway.");
+    return EINVAL;
+  }
+
   NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname);
   NS_ConvertUTF16toUTF8 autoHostname(aOptions.mIp);
   NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateway);
   int type, prefix;
 
   type = getIpType(autoHostname.get());
   if (type != AF_INET && type != AF_INET6) {
     return EAFNOSUPPORT;