From fd6e2954f7a6cc66ff46a7eda8acbd7aa8173ae0 Mon Sep 17 00:00:00 2001 From: lilia Date: Mon, 9 Mar 2015 13:20:01 -0700 Subject: [PATCH] Curtail over-zealous websocket reconnects Closes #173 Previously, in the event of a failed websocket auth, we would attempt to reconnect once a second ad infinitum. This changeset ensures that we only reconnect automatically if the socket closed 'normally' as indicated by the code on the socket's CloseEvent. Otherwise, show a 'Websocket closed' error on the inbox view. Ideally we would show a more contextual error (ie, 'Unauthorized'), but unfortunately the actual server response code is not available to our code. It can be observed in the console output from the background page, but programmatically, we only receive the WebSocket CloseEvent codes listed here: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes The websocket error message is displayed by a normally-hidden but ever present socket status element. Clicking this element will immediately refresh the background page, which will try again to open the websocket connection. --- images/error_red.png | Bin 0 -> 433 bytes images/refresh.png | Bin 0 -> 299 bytes index.html | 1 + js/background.js | 20 ++++++++++++- js/chromium.js | 4 +++ js/libtextsecure.js | 56 ++++++++++++++++++++++--------------- js/views/inbox_view.js | 36 ++++++++++++++++++++++++ libtextsecure/websocket.js | 56 ++++++++++++++++++++++--------------- stylesheets/_index.scss | 25 +++++++++++++++++ stylesheets/manifest.css | 17 +++++++++++ 10 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 images/error_red.png create mode 100644 images/refresh.png diff --git a/images/error_red.png b/images/error_red.png new file mode 100644 index 0000000000000000000000000000000000000000..069e12c282063964706527211fd536649d6b3c81 GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0y~yU=RXf4rT@hhD3q7R0akHg=CK)Uj~LMH3o);76yi2 z3=9knFBlj~4Hy_+B``2p&0t^SS$>Y5)FCV&AuChQ7{?pFeKRoN8n+hwI2e&f9;c8~(`JH4lR*O$H(^7gr}VSS(Z! zJj(d;|9>~}i!-LU>id`K-)o+!Gc9u7dLO|BYu2cEzCQa+*}C=3-m6_n419I0lT@l% zw2rDUn`ovw%)az;e$;~CNo)m~uX(<3-sC=DdhaR&&-VFuYQVtoDuIE)Y6b&?c)^@qfi?^b3`|Mh?k)`f+xyS#XJBC9EbxddW?BRYud5 z4;PCa9xX_%y1|iaxM9NmA9n9Q)UiCQIuskA6SzXA#@nT7OXn}ICq^?Zx7}N@I3hE> zAgi)yL8s6;4}+z>Tt6i}ceZHV{Q5G_$@~4Q ys;~VpF#g;*wcR_tv#0jw*Yt;>-|xkL$nSd@Tyb@)unH(R7(8A5T-G@yGywpu%W*{j literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 8d57e41c6..808b4e71b 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ New Message +
diff --git a/js/background.js b/js/background.js index bf6b84885..8eea93b11 100644 --- a/js/background.js +++ b/js/background.js @@ -16,6 +16,7 @@ ;(function() { 'use strict'; + var socket; var conversations = new Whisper.ConversationCollection(); var messages = new Whisper.MessageCollection(); @@ -29,11 +30,20 @@ } extension.on('registration_done', init); + window.getSocketStatus = function() { + if (socket) { + return socket.getStatus(); + } else { + return WebSocket.CONNECTING; + } + }; + function init() { if (!textsecure.registration.isDone()) { return; } // initialize the socket and start listening for messages - var socket = textsecure.api.getMessageWebsocket(); + socket = textsecure.api.getMessageWebsocket(); + new WebSocketResource(socket, function(request) { // TODO: handle different types of requests. for now we only expect // PUT /messages @@ -58,6 +68,14 @@ }); extension.browserAction(window.openInbox); + + // refresh views + var views = extension.windows.getViews(); + for (var i = 0; i < views.length; ++i) { + if (views[i] !== window) { + views[i].location.reload(); + } + } } function onMessageReceived(pushMessage) { diff --git a/js/chromium.js b/js/chromium.js index 38b26d2ef..c7414f99e 100644 --- a/js/chromium.js +++ b/js/chromium.js @@ -74,6 +74,10 @@ getBackground: function() { return chrome.extension.getBackgroundPage(); + }, + + getViews: function() { + return chrome.extension.getViews(); } }; diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 4ce8f80ee..77893689a 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -15905,14 +15905,16 @@ window.axolotl.sessions = { */ window.textsecure.websocket = function (url) { - var socketWrapper = { - onmessage : function() {}, - ondisconnect : function() {}, - }; - var socket; var keepAliveTimer; var reconnectSemaphore = 0; var reconnectTimeout = 1000; + var socket; + var socketWrapper = { + onmessage : function() {}, + onclose : function() {}, + onerror : function() {}, + getStatus : function() { return socket.readyState; } + }; function resetKeepAliveTimer() { clearTimeout(keepAliveTimer); @@ -15928,10 +15930,27 @@ window.axolotl.sessions = { }, 15000); }; - function reconnect(e) { - reconnectSemaphore--; - setTimeout(connect, reconnectTimeout); - socketWrapper.ondisconnect(e); + function onclose(e) { + if (e.code === 1000) { // CLOSE_NORMAL + reconnectSemaphore--; + setTimeout(connect, reconnectTimeout); + } else { + console.log('websocket closed', e.code); + } + socketWrapper.onclose(e); + }; + + function onerror(e) { + socketWrapper.onerror(e); + }; + + function onmessage(response) { + socketWrapper.onmessage(response); + resetKeepAliveTimer(); + }; + + function send(msg) { + socket.send(msg); }; function connect() { @@ -15941,19 +15960,12 @@ window.axolotl.sessions = { if (socket) { socket.close(); } socket = new WebSocket(url); - socket.onerror = reconnect; - socket.onclose = reconnect; - socket.onopen = resetKeepAliveTimer; - - socket.onmessage = function(response) { - socketWrapper.onmessage(response); - resetKeepAliveTimer(); - }; - - socketWrapper.send = function(msg) { - socket.send(msg); - } - } + socket.onopen = resetKeepAliveTimer; + socket.onerror = onerror + socket.onclose = onclose; + socket.onmessage = onmessage; + socketWrapper.send = send; + }; connect(); return socketWrapper; diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index cbe87ffa6..8f43a257d 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -19,6 +19,40 @@ window.Whisper = window.Whisper || {}; var bg = extension.windows.getBackground(); + var SocketView = Whisper.View.extend({ + className: 'status', + initialize: function() { + setInterval(function() { + var className, message = ''; + switch(bg.getSocketStatus && bg.getSocketStatus()) { + case WebSocket.CONNECTING: + className = 'connecting'; + break; + case WebSocket.OPEN: + className = 'open'; + break; + case WebSocket.CLOSING: + className = 'closing'; + break; + case WebSocket.CLOSED: + className = 'closed'; + message = 'Websocket closed'; + break; + } + if (!this.$el.hasClass(className)) { + this.$el.attr('class', className); + this.$el.text(message); + } + }.bind(this), 1000); + }, + events: { + 'click': 'reloadBackgroundPage' + }, + reloadBackgroundPage: function() { + bg.location.reload(); + } + }); + Whisper.InboxView = Backbone.View.extend({ initialize: function () { this.$gutter = $('#gutter'); @@ -34,6 +68,8 @@ collection : bg.inbox }).render(); + new SocketView().render().$el.appendTo(this.$el.find('.socket-status')); + window.addEventListener('beforeunload', function () { this.inbox.stopListening(); }.bind(this)); diff --git a/libtextsecure/websocket.js b/libtextsecure/websocket.js index a1c88b578..f6a50f873 100644 --- a/libtextsecure/websocket.js +++ b/libtextsecure/websocket.js @@ -25,14 +25,16 @@ */ window.textsecure.websocket = function (url) { - var socketWrapper = { - onmessage : function() {}, - ondisconnect : function() {}, - }; - var socket; var keepAliveTimer; var reconnectSemaphore = 0; var reconnectTimeout = 1000; + var socket; + var socketWrapper = { + onmessage : function() {}, + onclose : function() {}, + onerror : function() {}, + getStatus : function() { return socket.readyState; } + }; function resetKeepAliveTimer() { clearTimeout(keepAliveTimer); @@ -48,10 +50,27 @@ }, 15000); }; - function reconnect(e) { - reconnectSemaphore--; - setTimeout(connect, reconnectTimeout); - socketWrapper.ondisconnect(e); + function onclose(e) { + if (e.code === 1000) { // CLOSE_NORMAL + reconnectSemaphore--; + setTimeout(connect, reconnectTimeout); + } else { + console.log('websocket closed', e.code); + } + socketWrapper.onclose(e); + }; + + function onerror(e) { + socketWrapper.onerror(e); + }; + + function onmessage(response) { + socketWrapper.onmessage(response); + resetKeepAliveTimer(); + }; + + function send(msg) { + socket.send(msg); }; function connect() { @@ -61,19 +80,12 @@ if (socket) { socket.close(); } socket = new WebSocket(url); - socket.onerror = reconnect; - socket.onclose = reconnect; - socket.onopen = resetKeepAliveTimer; - - socket.onmessage = function(response) { - socketWrapper.onmessage(response); - resetKeepAliveTimer(); - }; - - socketWrapper.send = function(msg) { - socket.send(msg); - } - } + socket.onopen = resetKeepAliveTimer; + socket.onerror = onerror + socket.onclose = onclose; + socket.onmessage = onmessage; + socketWrapper.send = send; + }; connect(); return socketWrapper; diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 5f9f20cb3..9ca4e78ed 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -13,6 +13,31 @@ // TODO: spinner } +.socket-status { + float: left; + padding: 6px; + + * { + cursor: pointer; + padding-left: 20px; + border-radius: $header-height; + min-height: 20px; + + &:hover { + background: $blue url('/images/refresh.png') center; + } + } + .connecting .icon { + background-color: $blue; + } + .closing { + background-color: $blue_l; + } + .closed { + background: url('/images/error_red.png') no-repeat left center; + } +} + .contact { .number, .checkbox { display: none; diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index 713dbc995..ce5ef48bd 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -143,6 +143,23 @@ button.back { #contacts { overflow: auto; } +.socket-status { + float: left; + padding: 6px; } + .socket-status * { + cursor: pointer; + padding-left: 20px; + border-radius: 36px; + min-height: 20px; } + .socket-status *:hover { + background: #2a92e7 url("/images/refresh.png") center; } + .socket-status .connecting .icon { + background-color: #2a92e7; } + .socket-status .closing { + background-color: #a2d2f4; } + .socket-status .closed { + background: url("/images/error_red.png") no-repeat left center; } + .contact .number, .contact .checkbox { display: none; }