From b64f2969fdcfe8addbf4e6caf904459d2e31b51a Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 6 Oct 2017 16:28:13 -0700 Subject: [PATCH] Better handling of network disconnection/reconnection (#1546) * Ensure that our preload.js setImmediate call finds right function FREEBIE * Our own socket close event, better logging, unregistration FREEBIE * Return CLOSED for NetworkStatusView if we've fully disconnected * background.js: Remove messageReceiver = null, log in connect() A null messageReciever makes the NetworkStatusView think we're online. FREEBIE --- Gruntfile.js | 2 +- js/background.js | 5 +- js/libtextsecure.js | 483 +++++++++++++++------------ libtextsecure/message_receiver.js | 40 ++- libtextsecure/websocket-resources.js | 59 +++- preload.js | 2 +- 6 files changed, 363 insertions(+), 228 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index e63a4bc28ca0..7b7772b77674 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -54,12 +54,12 @@ module.exports = function(grunt) { 'libtextsecure/storage/groups.js', 'libtextsecure/storage/unprocessed.js', 'libtextsecure/protobufs.js', - 'libtextsecure/websocket-resources.js', 'libtextsecure/helpers.js', 'libtextsecure/stringview.js', 'libtextsecure/event_target.js', 'libtextsecure/api.js', 'libtextsecure/account_manager.js', + 'libtextsecure/websocket-resources.js', 'libtextsecure/message_receiver.js', 'libtextsecure/outgoing_message.js', 'libtextsecure/sendmessage.js', diff --git a/js/background.js b/js/background.js index 993e04011945..40891308ab5b 100644 --- a/js/background.js +++ b/js/background.js @@ -69,7 +69,6 @@ Whisper.events.on('shutdown', function() { if (messageReceiver) { messageReceiver.close().then(function() { - messageReceiver = null; Whisper.events.trigger('shutdown-complete'); }); } else { @@ -147,7 +146,6 @@ messageReceiver.close().then(function() { Whisper.events.trigger('shutdown-complete'); }); - messageReceiver = null; } else { Whisper.events.trigger('shutdown-complete'); } @@ -155,6 +153,7 @@ var connectCount = 0; function connect(firstRun) { + console.log('connect'); window.removeEventListener('online', connect); window.addEventListener('offline', disconnect); @@ -163,7 +162,6 @@ if (messageReceiver) { messageReceiver.close(); - messageReceiver = null; } var USERNAME = storage.get('number_id'); @@ -485,7 +483,6 @@ console.log('offline'); if (messageReceiver) { messageReceiver.close(); - messageReceiver = null; } } diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 2f16f47783c9..7f310f66e1ff 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -37186,211 +37186,6 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ loadProtoBufs('DeviceMessages.proto'); })(); -/* - * vim: ts=4:sw=4:expandtab - */ -;(function(){ - 'use strict'; - - /* - * WebSocket-Resources - * - * Create a request-response interface over websockets using the - * WebSocket-Resources sub-protocol[1]. - * - * var client = new WebSocketResource(socket, function(request) { - * request.respond(200, 'OK'); - * }); - * - * client.sendRequest({ - * verb: 'PUT', - * path: '/v1/messages', - * body: '{ some: "json" }', - * success: function(message, status, request) {...}, - * error: function(message, status, request) {...} - * }); - * - * 1. https://github.com/WhisperSystems/WebSocket-Resources - * - */ - - var Request = function(options) { - this.verb = options.verb || options.type; - this.path = options.path || options.url; - this.body = options.body || options.data; - this.success = options.success; - this.error = options.error; - this.id = options.id; - - if (this.id === undefined) { - var bits = new Uint32Array(2); - window.crypto.getRandomValues(bits); - this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); - } - - if (this.body === undefined) { - this.body = null; - } - }; - - var IncomingWebSocketRequest = function(options) { - var request = new Request(options); - var socket = options.socket; - - this.verb = request.verb; - this.path = request.path; - this.body = request.body; - - this.respond = function(status, message) { - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: request.id, message: message, status: status } - }).encode().toArrayBuffer() - ); - }; - }; - - var outgoing = {}; - var OutgoingWebSocketRequest = function(options, socket) { - var request = new Request(options); - outgoing[request.id] = request; - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { - verb : request.verb, - path : request.path, - body : request.body, - id : request.id - } - }).encode().toArrayBuffer() - ); - }; - - window.WebSocketResource = function(socket, opts) { - opts = opts || {}; - var handleRequest = opts.handleRequest; - if (typeof handleRequest !== 'function') { - handleRequest = function(request) { - request.respond(404, 'Not found'); - }; - } - this.sendRequest = function(options) { - return new OutgoingWebSocketRequest(options, socket); - }; - - socket.onmessage = function(socketMessage) { - var blob = socketMessage.data; - var handleArrayBuffer = function(buffer) { - var message = textsecure.protobuf.WebSocketMessage.decode(buffer); - if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { - handleRequest( - new IncomingWebSocketRequest({ - verb : message.request.verb, - path : message.request.path, - body : message.request.body, - id : message.request.id, - socket : socket - }) - ); - } - else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) { - var response = message.response; - var request = outgoing[response.id]; - if (request) { - request.response = response; - var callback = request.error; - if (response.status >= 200 && response.status < 300) { - callback = request.success; - } - - if (typeof callback === 'function') { - callback(response.message, response.status, request); - } - } else { - throw 'Received response for unknown request ' + message.response.id; - } - } - }; - - if (blob instanceof ArrayBuffer) { - handleArrayBuffer(blob); - } else { - var reader = new FileReader(); - reader.onload = function() { - handleArrayBuffer(reader.result); - }; - reader.readAsArrayBuffer(blob); - } - }; - - if (opts.keepalive) { - var keepalive = new KeepAlive(this, { - path : opts.keepalive.path, - disconnect : opts.keepalive.disconnect - }); - var resetKeepAliveTimer = keepalive.reset.bind(keepalive); - socket.addEventListener('open', resetKeepAliveTimer); - socket.addEventListener('message', resetKeepAliveTimer); - socket.addEventListener('close', keepalive.stop.bind(keepalive)); - } - - this.close = function(code, reason) { - if (!code) { code = 3000; } - socket.close(code, reason); - }; - - }; - - function KeepAlive(websocketResource, opts) { - if (websocketResource instanceof WebSocketResource) { - opts = opts || {}; - this.path = opts.path; - if (this.path === undefined) { - this.path = '/'; - } - this.disconnect = opts.disconnect; - if (this.disconnect === undefined) { - this.disconnect = true; - } - this.wsr = websocketResource; - } else { - throw new TypeError('KeepAlive expected a WebSocketResource'); - } - } - - KeepAlive.prototype = { - constructor: KeepAlive, - stop: function() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - }, - reset: function() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - this.keepAliveTimer = setTimeout(function() { - console.log('Sending a keepalive message'); - this.wsr.sendRequest({ - verb: 'GET', - path: this.path, - success: this.reset.bind(this) - }); - if (this.disconnect) { - // automatically disconnect if server doesn't ack - this.disconnectTimer = setTimeout(function() { - clearTimeout(this.keepAliveTimer); - this.wsr.close(3001, 'No response to keepalive request'); - }.bind(this), 1000); - } else { - this.reset(); - } - }.bind(this), 55000); - }, - }; - -}()); - /* * vim: ts=4:sw=4:expandtab */ @@ -38365,6 +38160,244 @@ var TextSecureServer = (function() { }()); +/* + * vim: ts=4:sw=4:expandtab + */ +;(function(){ + 'use strict'; + + /* + * WebSocket-Resources + * + * Create a request-response interface over websockets using the + * WebSocket-Resources sub-protocol[1]. + * + * var client = new WebSocketResource(socket, function(request) { + * request.respond(200, 'OK'); + * }); + * + * client.sendRequest({ + * verb: 'PUT', + * path: '/v1/messages', + * body: '{ some: "json" }', + * success: function(message, status, request) {...}, + * error: function(message, status, request) {...} + * }); + * + * 1. https://github.com/WhisperSystems/WebSocket-Resources + * + */ + + var Request = function(options) { + this.verb = options.verb || options.type; + this.path = options.path || options.url; + this.body = options.body || options.data; + this.success = options.success; + this.error = options.error; + this.id = options.id; + + if (this.id === undefined) { + var bits = new Uint32Array(2); + window.crypto.getRandomValues(bits); + this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); + } + + if (this.body === undefined) { + this.body = null; + } + }; + + var IncomingWebSocketRequest = function(options) { + var request = new Request(options); + var socket = options.socket; + + this.verb = request.verb; + this.path = request.path; + this.body = request.body; + + this.respond = function(status, message) { + socket.send( + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, + response: { id: request.id, message: message, status: status } + }).encode().toArrayBuffer() + ); + }; + }; + + var outgoing = {}; + var OutgoingWebSocketRequest = function(options, socket) { + var request = new Request(options); + outgoing[request.id] = request; + socket.send( + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { + verb : request.verb, + path : request.path, + body : request.body, + id : request.id + } + }).encode().toArrayBuffer() + ); + }; + + window.WebSocketResource = function(socket, opts) { + opts = opts || {}; + var handleRequest = opts.handleRequest; + if (typeof handleRequest !== 'function') { + handleRequest = function(request) { + request.respond(404, 'Not found'); + }; + } + this.sendRequest = function(options) { + return new OutgoingWebSocketRequest(options, socket); + }; + + socket.onmessage = function(socketMessage) { + var blob = socketMessage.data; + var handleArrayBuffer = function(buffer) { + var message = textsecure.protobuf.WebSocketMessage.decode(buffer); + if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { + handleRequest( + new IncomingWebSocketRequest({ + verb : message.request.verb, + path : message.request.path, + body : message.request.body, + id : message.request.id, + socket : socket + }) + ); + } + else if (message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE ) { + var response = message.response; + var request = outgoing[response.id]; + if (request) { + request.response = response; + var callback = request.error; + if (response.status >= 200 && response.status < 300) { + callback = request.success; + } + + if (typeof callback === 'function') { + callback(response.message, response.status, request); + } + } else { + throw 'Received response for unknown request ' + message.response.id; + } + } + }; + + if (blob instanceof ArrayBuffer) { + handleArrayBuffer(blob); + } else { + var reader = new FileReader(); + reader.onload = function() { + handleArrayBuffer(reader.result); + }; + reader.readAsArrayBuffer(blob); + } + }; + + if (opts.keepalive) { + this.keepalive = new KeepAlive(this, { + path : opts.keepalive.path, + disconnect : opts.keepalive.disconnect + }); + var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); + socket.addEventListener('open', resetKeepAliveTimer); + socket.addEventListener('message', resetKeepAliveTimer); + socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive)); + } + + socket.addEventListener('close', function() { + this.closed = true; + }.bind(this)) + + this.close = function(code, reason) { + if (this.closed) { + return; + } + + console.log('WebSocketResource.close()'); + if (!code) { + code = 3000; + } + if (this.keepalive) { + this.keepalive.stop(); + } + + socket.close(code, reason); + socket.onmessage = null; + + // On linux the socket can wait a long time to emit its close event if we've + // lost the internet connection. On the order of minutes. This speeds that + // process up. + setTimeout(function() { + if (this.closed) { + return; + } + this.closed = true; + + console.log('Dispatching our own socket close event'); + var ev = new Event('close'); + ev.code = code; + ev.reason = reason; + this.dispatchEvent(ev); + }.bind(this), 10000); + }; + }; + window.WebSocketResource.prototype = new textsecure.EventTarget(); + + + function KeepAlive(websocketResource, opts) { + if (websocketResource instanceof WebSocketResource) { + opts = opts || {}; + this.path = opts.path; + if (this.path === undefined) { + this.path = '/'; + } + this.disconnect = opts.disconnect; + if (this.disconnect === undefined) { + this.disconnect = true; + } + this.wsr = websocketResource; + } else { + throw new TypeError('KeepAlive expected a WebSocketResource'); + } + } + + KeepAlive.prototype = { + constructor: KeepAlive, + stop: function() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + }, + reset: function() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + this.keepAliveTimer = setTimeout(function() { + if (this.disconnect) { + // automatically disconnect if server doesn't ack + this.disconnectTimer = setTimeout(function() { + clearTimeout(this.keepAliveTimer); + this.wsr.close(3001, 'No response to keepalive request'); + }.bind(this), 1000); + } else { + this.reset(); + } + console.log('Sending a keepalive message'); + this.wsr.sendRequest({ + verb: 'GET', + path: this.path, + success: this.reset.bind(this) + }); + }.bind(this), 55000); + }, + }; + +}()); + /* * vim: ts=4:sw=4:expandtab */ @@ -38395,8 +38428,11 @@ MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({ constructor: MessageReceiver, connect: function() { + this.hasConnected = true; + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.socket.close(); + this.wsr.close(); } // initialize the socket and start listening for messages this.socket = this.server.getMessageSocket(); @@ -38405,16 +38441,45 @@ MessageReceiver.prototype.extend({ this.socket.onopen = this.onopen.bind(this); this.wsr = new WebSocketResource(this.socket, { handleRequest: this.handleRequest.bind(this), - keepalive: { path: '/v1/keepalive', disconnect: true } + keepalive: { + path: '/v1/keepalive', + disconnect: true + } }); + // Because sometimes the socket doesn't properly emit its close event + this._onClose = this.onclose.bind(this) + this.wsr.addEventListener('close', this._onClose); + // Ensures that an immediate 'empty' event from the websocket will fire only after // all cached envelopes are processed. this.incoming = [this.pending]; }, + shutdown: function() { + if (this.socket) { + this.socket.onclose = null; + this.socket.onerror = null; + this.socket.onopen = null; + this.socket = null; + } + + if (this.wsr) { + this.wsr.removeEventListener('close', this._onClose); + this.wsr = null; + } + }, close: function() { + console.log('MessageReceiver.close()'); this.calledClose = true; - this.socket.close(3000, 'called close'); + + // Our WebSocketResource instance will close the socket and emit a 'close' event + // if the socket doesn't emit one quickly enough. + if (this.wsr) { + this.wsr.close(3000, 'called close'); + } + + this.shutdown(); + return this.drain(); }, onopen: function() { @@ -38435,6 +38500,8 @@ MessageReceiver.prototype.extend({ this.calledClose ); + this.shutdown(); + if (this.calledClose) { return; } @@ -38699,6 +38766,8 @@ MessageReceiver.prototype.extend({ getStatus: function() { if (this.socket) { return this.socket.readyState; + } else if (this.hasConnected) { + return WebSocket.CLOSED; } else { return -1; } diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 8aa4c48be2d2..cfb11c7f00c8 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -28,8 +28,11 @@ MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({ constructor: MessageReceiver, connect: function() { + this.hasConnected = true; + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.socket.close(); + this.wsr.close(); } // initialize the socket and start listening for messages this.socket = this.server.getMessageSocket(); @@ -38,16 +41,45 @@ MessageReceiver.prototype.extend({ this.socket.onopen = this.onopen.bind(this); this.wsr = new WebSocketResource(this.socket, { handleRequest: this.handleRequest.bind(this), - keepalive: { path: '/v1/keepalive', disconnect: true } + keepalive: { + path: '/v1/keepalive', + disconnect: true + } }); + // Because sometimes the socket doesn't properly emit its close event + this._onClose = this.onclose.bind(this) + this.wsr.addEventListener('close', this._onClose); + // Ensures that an immediate 'empty' event from the websocket will fire only after // all cached envelopes are processed. this.incoming = [this.pending]; }, + shutdown: function() { + if (this.socket) { + this.socket.onclose = null; + this.socket.onerror = null; + this.socket.onopen = null; + this.socket = null; + } + + if (this.wsr) { + this.wsr.removeEventListener('close', this._onClose); + this.wsr = null; + } + }, close: function() { + console.log('MessageReceiver.close()'); this.calledClose = true; - this.socket.close(3000, 'called close'); + + // Our WebSocketResource instance will close the socket and emit a 'close' event + // if the socket doesn't emit one quickly enough. + if (this.wsr) { + this.wsr.close(3000, 'called close'); + } + + this.shutdown(); + return this.drain(); }, onopen: function() { @@ -68,6 +100,8 @@ MessageReceiver.prototype.extend({ this.calledClose ); + this.shutdown(); + if (this.calledClose) { return; } @@ -332,6 +366,8 @@ MessageReceiver.prototype.extend({ getStatus: function() { if (this.socket) { return this.socket.readyState; + } else if (this.hasConnected) { + return WebSocket.CLOSED; } else { return -1; } diff --git a/libtextsecure/websocket-resources.js b/libtextsecure/websocket-resources.js index dd09ac51fdbb..239c9f7e1a46 100644 --- a/libtextsecure/websocket-resources.js +++ b/libtextsecure/websocket-resources.js @@ -138,22 +138,55 @@ }; if (opts.keepalive) { - var keepalive = new KeepAlive(this, { + this.keepalive = new KeepAlive(this, { path : opts.keepalive.path, disconnect : opts.keepalive.disconnect }); - var resetKeepAliveTimer = keepalive.reset.bind(keepalive); + var resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); socket.addEventListener('open', resetKeepAliveTimer); socket.addEventListener('message', resetKeepAliveTimer); - socket.addEventListener('close', keepalive.stop.bind(keepalive)); + socket.addEventListener('close', this.keepalive.stop.bind(this.keepalive)); } - this.close = function(code, reason) { - if (!code) { code = 3000; } - socket.close(code, reason); - }; + socket.addEventListener('close', function() { + this.closed = true; + }.bind(this)) + this.close = function(code, reason) { + if (this.closed) { + return; + } + + console.log('WebSocketResource.close()'); + if (!code) { + code = 3000; + } + if (this.keepalive) { + this.keepalive.stop(); + } + + socket.close(code, reason); + socket.onmessage = null; + + // On linux the socket can wait a long time to emit its close event if we've + // lost the internet connection. On the order of minutes. This speeds that + // process up. + setTimeout(function() { + if (this.closed) { + return; + } + this.closed = true; + + console.log('Dispatching our own socket close event'); + var ev = new Event('close'); + ev.code = code; + ev.reason = reason; + this.dispatchEvent(ev); + }.bind(this), 10000); + }; }; + window.WebSocketResource.prototype = new textsecure.EventTarget(); + function KeepAlive(websocketResource, opts) { if (websocketResource instanceof WebSocketResource) { @@ -182,12 +215,6 @@ clearTimeout(this.keepAliveTimer); clearTimeout(this.disconnectTimer); this.keepAliveTimer = setTimeout(function() { - console.log('Sending a keepalive message'); - this.wsr.sendRequest({ - verb: 'GET', - path: this.path, - success: this.reset.bind(this) - }); if (this.disconnect) { // automatically disconnect if server doesn't ack this.disconnectTimer = setTimeout(function() { @@ -197,6 +224,12 @@ } else { this.reset(); } + console.log('Sending a keepalive message'); + this.wsr.sendRequest({ + verb: 'GET', + path: this.path, + success: this.reset.bind(this) + }); }.bind(this), 55000); }, }; diff --git a/preload.js b/preload.js index 0ee10e872d13..6490e97bf73b 100644 --- a/preload.js +++ b/preload.js @@ -41,7 +41,7 @@ // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(function() { - setImmediate(function() {}); + window.nodeSetImmediate(function() {}); }, 1000); window.EmojiConvertor = require('emoji-js');