From 57b9036ba233eb466f9f55c791a13480865c28d5 Mon Sep 17 00:00:00 2001 From: Doug Turner Date: Thu, 28 Mar 2013 20:49:41 -0700 Subject: [PATCH] Bug 822712 - SimplePush - UDP Wakeup feature. r=jst, jlebar --- b2g/app/b2g.js | 4 + dom/push/src/PushService.js | 168 ++++++++++++++++++++++++++++++++++-- 2 files changed, 163 insertions(+), 9 deletions(-) diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 2ee1b4e63a9d..584b8a0be584 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -398,6 +398,10 @@ pref("services.push.retryBaseInterval", 5000); pref("services.push.maxRetryInterval", 1200000); // How long before a DOMRequest errors as timeout pref("services.push.requestTimeout", 10000); +// enable udp wakeup support +pref("services.push.udp.wakeupEnabled", true); +// port on which UDP server socket is bound +pref("services.push.udp.port", 2442); // NetworkStats #ifdef MOZ_B2G_RIL diff --git a/dom/push/src/PushService.js b/dom/push/src/PushService.js index a391e1fd068c..21832080efcc 100644 --- a/dom/push/src/PushService.js +++ b/dom/push/src/PushService.js @@ -11,6 +11,7 @@ function debug(s) { const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; +const Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); @@ -29,6 +30,10 @@ const kCONFLICT_RETRY_ATTEMPTS = 3; // If channelID registration says 409, how const kERROR_CHID_CONFLICT = 409; // Error code sent by push server if this // channel already exists on the server. +const kUDP_WAKEUP_WS_STATUS_CODE = 4774; // WebSocket Close status code sent + // by server to signal that it can + // wake client up using UDP. + const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister", "Push:Registrations"]; @@ -270,13 +275,17 @@ const STATE_READY = 3; PushService.prototype = { classID : Components.ID("{0ACE8D15-9B15-41F4-992F-C88820421DBF}"), - QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver]), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIUDPServerSocketListener]), observe: function observe(aSubject, aTopic, aData) { switch (aTopic) { case "app-startup": Services.obs.addObserver(this, "final-ui-startup", false); Services.obs.addObserver(this, "profile-change-teardown", false); + Services.obs.addObserver(this, + "network-interface-state-changed", + false); break; case "final-ui-startup": Services.obs.removeObserver(this, "final-ui-startup"); @@ -286,7 +295,22 @@ PushService.prototype = { Services.obs.removeObserver(this, "profile-change-teardown"); this._shutdown(); break; + case "network-interface-state-changed": + debug("network-interface-state-changed"); + if (this._udpServer) { + this._udpServer.close(); + } + + this._shutdownWS(); + + // Check to see if we need to do anything. + this._db.getAllChannelIDs(function(channelIDs) { + if (channelIDs.length > 0) { + this._beginWSSetup(); + } + }.bind(this)); + break; case "nsPref:changed": if (aData == "services.push.serverURL") { debug("services.push.serverURL changed! websocket. new value " + @@ -363,6 +387,18 @@ PushService.prototype = { _retryTimeoutTimer: null, _retryFailCount: 0, + /** + * According to the WS spec, servers should immediately close the underlying + * TCP connection after they close a WebSocket. This causes wsOnStop to be + * called with error NS_BASE_STREAM_CLOSED. Since the client has to keep the + * WebSocket up, it should try to reconnect. But if the server closes the + * WebSocket because it will wake up the client via UDP, then the client + * shouldn't re-establish the connection. If the server says that it will + * wake up the client over UDP, this is set to true in wsOnServerClose. It is + * checked in wsOnStop. + */ + _willBeWokenUpByUDP: false, + init: function() { debug("init()"); this._db = new PushDB(this); @@ -376,6 +412,8 @@ PushService.prototype = { this._requestTimeout = this._prefs.get("requestTimeout"); + this._udpPort = this._prefs.get("udp.port"); + this._db.getAllChannelIDs( function(channelIDs) { if (channelIDs.length > 0) { @@ -397,6 +435,7 @@ PushService.prototype = { _shutdownWS: function() { debug("shutdownWS()"); this._currentState = STATE_SHUT_DOWN; + this._willBeWokenUpByUDP = false; if (this._wsListener) this._wsListener._pushService = null; try { @@ -410,6 +449,10 @@ PushService.prototype = { this._db.close(); this._db = null; + if (this._udpServer) { + this._udpServer.close(); + } + // All pending requests (ideally none) are dropped at this point. We // shouldn't have any applications performing registration/unregistration // or receiving notifications. @@ -1032,6 +1075,20 @@ PushService.prototype = { if (this._UAID) data["uaid"] = this._UAID; + var networkState = this._getNetworkState(); + if (networkState.ip) { + // Hostport is apparently a thing. + data["wakeup_hostport"] = { + ip: networkState.ip, + port: this._udpPort + }; + + data["mobilenetwork"] = { + mcc: networkState.mcc, + mnc: networkState.mnc + }; + } + function sendHelloMessage(ids) { // On success, ids is an array, on error its not. data["channelIDs"] = ids.map ? @@ -1047,10 +1104,14 @@ PushService.prototype = { /** * This statusCode is not the websocket protocol status code, but the TCP * connection close status code. + * + * If we do not explicitly call ws.close() then statusCode is always + * NS_BASE_STREAM_CLOSED, even on a successful close. */ _wsOnStop: function(context, statusCode) { debug("wsOnStop()"); - if (statusCode != Components.results.NS_OK) { + if (statusCode != Cr.NS_OK && + !(statusCode == Cr.NS_BASE_STREAM_CLOSED && this._willBeWokenUpByUDP)) { debug("Socket error " + statusCode); this._socketError(statusCode); } @@ -1098,16 +1159,105 @@ PushService.prototype = { this[handler](reply); }, + /** + * The websocket should never be closed. Since we don't call ws.close(), + * _wsOnStop() receives error code NS_BASE_STREAM_CLOSED (see comment in that + * function), which calls socketError and re-establishes the WebSocket + * connection. + * + * If the server said it'll use UDP for wakeup, we set _willBeWokenUpByUDP + * and stop reconnecting in _wsOnStop(). + */ _wsOnServerClose: function(context, aStatusCode, aReason) { debug("wsOnServerClose() " + aStatusCode + " " + aReason); - // 1000 is the normal close - if (aStatusCode == 1000 && Object.keys(this._pendingRequests).length > 0) { - // This should never happen. A successful close cannot have pending - // requests since the server should've responded to them before the - // connection was closed. - this._shutdownWS(); - this._beginWSSetup(); + + // Switch over to UDP. + if (aStatusCode == kUDP_WAKEUP_WS_STATUS_CODE) { + debug("Server closed with promise to wake up"); + this._willBeWokenUpByUDP = true; + // TODO: there should be no pending requests + this._listenForUDPWakeup(); } + }, + + _listenForUDPWakeup: function() { + debug("listenForUDPWakeup()"); + + if (this._udpServer) { + debug("UDP Server already running"); + return; + } + + if (!this._getNetworkState().ip) { + debug("No IP"); + return; + } + + if (!this._prefs.get("udp.wakeupEnabled")) { + debug("UDP support disabled"); + return; + } + + this._udpServer = Cc["@mozilla.org/network/server-socket-udp;1"] + .createInstance(Ci.nsIUDPServerSocket); + this._udpServer.init(this._udpPort, false); + this._udpServer.asyncListen(this); + debug("listenForUDPWakeup listening on " + this._udpPort); + }, + + /** + * Called by UDP Server Socket. As soon as a ping is recieved via UDP, + * reconnect the WebSocket and get the actual data. + */ + onPacketReceived: function(aServ, aMessage) { + debug("Recv UDP datagram on port: " + this._udpPort); + this._beginWSSetup(); + }, + + /** + * Called by UDP Server Socket if the socket was closed for some reason. + * + * If this happens, we reconnect the WebSocket to not miss out on + * notifications. + */ + onStopListening: function(aServ, aStatus) { + debug("UDP Server socket was shutdown. Status: " + aStatus); + this._beginWSSetup(); + }, + + /** + * Get mobile network information to decide if the client is capable of being woken + * up by UDP (which currently just means having an mcc and mnc along with an + * IP). + */ + _getNetworkState: function() { + debug("getNetworkState()"); + + var networkManager = Cc["@mozilla.org/network/manager;1"] + .getService(Ci.nsINetworkManager); + if (networkManager.active && + networkManager.active.type == + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) { + debug("Running on mobile data"); + var mcp = Cc["@mozilla.org/ril/content-helper;1"] + .getService(Ci.nsIMobileConnectionProvider); + if (mcp.iccInfo) { + return { + mcc: mcp.iccInfo.mcc, + mnc: mcp.iccInfo.mnc, + ip: networkManager.active.ip + } + } + } + else { + debug("Running on wifi"); + } + + return { + mcc: 0, + mnc: 0, + ip: undefined + }; } }