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.
This commit is contained in:
lilia 2015-03-09 13:20:01 -07:00
parent 1321a90667
commit fd6e2954f7
10 changed files with 170 additions and 45 deletions

BIN
images/error_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

BIN
images/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

View File

@ -18,6 +18,7 @@
<button class='back'></button>
<span class='title-text'>New Message</span>
</div>
<div class='socket-status'></div>
</div>
<div class='notifications'>
<div class='notification info'>

View File

@ -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 <encrypted IncomingPushMessageSignal>
@ -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) {

View File

@ -74,6 +74,10 @@
getBackground: function() {
return chrome.extension.getBackgroundPage();
},
getViews: function() {
return chrome.extension.getViews();
}
};

View File

@ -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;

View File

@ -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));

View File

@ -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;

View File

@ -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;

View File

@ -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; }