summaryrefslogtreecommitdiffstats
path: root/remoting/webapp
diff options
context:
space:
mode:
authorkelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-01 23:48:55 +0000
committerkelvinp@chromium.org <kelvinp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-05-01 23:48:55 +0000
commitbf7e9521a599eb09fe7e207aeafdedf9c089bcdd (patch)
treeb85bf06f1a1c7342848808dd4d92dc5f6505a628 /remoting/webapp
parenta4f0d8897f27c9d8edfe31b3a99baec79934b6c3 (diff)
downloadchromium_src-bf7e9521a599eb09fe7e207aeafdedf9c089bcdd.zip
chromium_src-bf7e9521a599eb09fe7e207aeafdedf9c089bcdd.tar.gz
chromium_src-bf7e9521a599eb09fe7e207aeafdedf9c089bcdd.tar.bz2
Smart Reconnect
This CL serves to make the experience below better “I typically have to manually reconnect 20-40 times per day when working remotely, about 10 reconnects are due to wifi zone changes, and 20 are due to closing my laptop screen (Chromebook Pixel). “ - A customer Cause: There are three factors that causes the connection to drop 1. When the client is suspended, the host times out in 1 min. This is particular common when the user closes the lid of the laptop and walk to another room for meeting. (P1) 2. When the client is not suspended, but cannot reach the host (network changes, host_is_offline) a. If the timeout is less than 1 min, it will try to reconnect, however, the reconnect could takes up to 2 min. Most people won't have that kind of patience to wait for the reconnect. (P2) b. If the timeout is larger than 1 min, it will timeout. Fix: This change addresses (1) and (2b) by listening to the changes in connection state of the session. If it transits from CONNECTED to FAILED and the host is not offline, we reconnect. In order for this to work smoothly, the timeout of session initiation and session accept needs to be increased, which is included in part II. Review URL: https://codereview.chromium.org/245983003 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@267671 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting/webapp')
-rw-r--r--remoting/webapp/all_js_load.gtestjs1
-rw-r--r--remoting/webapp/client_screen.js21
-rw-r--r--remoting/webapp/client_session.js80
-rw-r--r--remoting/webapp/session_connector.js38
-rw-r--r--remoting/webapp/smart_reconnector.js143
5 files changed, 238 insertions, 45 deletions
diff --git a/remoting/webapp/all_js_load.gtestjs b/remoting/webapp/all_js_load.gtestjs
index 6eedd3e..9b26026 100644
--- a/remoting/webapp/all_js_load.gtestjs
+++ b/remoting/webapp/all_js_load.gtestjs
@@ -57,6 +57,7 @@ AllJsLoadTest.prototype = {
'remoting.js',
'session_connector.js',
'server_log_entry.js',
+ 'smart_reconnector.js',
'stats_accumulator.js',
'toolbar.js',
'ui_mode.js',
diff --git a/remoting/webapp/client_screen.js b/remoting/webapp/client_screen.js
index b870e8a..dd583b7 100644
--- a/remoting/webapp/client_screen.js
+++ b/remoting/webapp/client_screen.js
@@ -71,7 +71,7 @@ remoting.disconnect = function() {
} else {
remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
}
- remoting.clientSession.disconnect(true);
+ remoting.clientSession.disconnect(remoting.Error.NONE);
remoting.clientSession = null;
console.log('Disconnected.');
};
@@ -102,13 +102,12 @@ remoting.sendPrintScreen = function() {
/**
* Callback function called when the state of the client plugin changes. The
- * current state is available via the |state| member variable.
+ * current and previous states are available via the |state| member variable.
*
- * @param {number} oldState The previous state of the plugin.
- * @param {number} newState The current state of the plugin.
+ * @param {remoting.ClientSession.StateEvent} state
*/
-function onClientStateChange_(oldState, newState) {
- switch (newState) {
+function onClientStateChange_(state) {
+ switch (state.current) {
case remoting.ClientSession.State.CLOSED:
console.log('Connection closed by host');
if (remoting.clientSession.getMode() ==
@@ -129,14 +128,16 @@ function onClientStateChange_(oldState, newState) {
break;
default:
- console.error('Unexpected client plugin state: ' + newState);
+ console.error('Unexpected client plugin state: ' + state.current);
// This should only happen if the web-app and client plugin get out of
// sync, so MISSING_PLUGIN is a suitable error.
showConnectError_(remoting.Error.MISSING_PLUGIN);
break;
}
- remoting.clientSession.disconnect(false);
- remoting.clientSession.removePlugin();
+
+ remoting.clientSession.removeEventListener('stateChanged',
+ onClientStateChange_);
+ remoting.clientSession.cleanup();
remoting.clientSession = null;
}
@@ -332,7 +333,7 @@ remoting.connectMe2MeHostVersionAcknowledged_ = function(host) {
/** @param {remoting.ClientSession} clientSession */
remoting.onConnected = function(clientSession) {
remoting.clientSession = clientSession;
- remoting.clientSession.setOnStateChange(onClientStateChange_);
+ remoting.clientSession.addEventListener('stateChanged', onClientStateChange_);
setConnectionInterruptedButtonsText_();
var connectedTo = document.getElementById('connected-to');
connectedTo.innerText = remoting.connector.getHostDisplayName();
diff --git a/remoting/webapp/client_session.js b/remoting/webapp/client_session.js
index 92ded18..8a63998 100644
--- a/remoting/webapp/client_session.js
+++ b/remoting/webapp/client_session.js
@@ -44,6 +44,7 @@ var remoting = remoting || {};
* @param {string} clientPairedSecret For paired Me2Me connections, the
* paired secret for this client, as issued by the host.
* @constructor
+ * @extends {base.EventSource}
*/
remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
authenticationMethods,
@@ -89,9 +90,6 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
/** @private */
this.hasReceivedFrame_ = false;
this.logToServer = new remoting.LogToServer();
- /** @type {?function(remoting.ClientSession.State,
- remoting.ClientSession.State):void} */
- this.onStateChange_ = null;
/** @type {number?} @private */
this.notifyClientResolutionTimer_ = null;
@@ -116,7 +114,7 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
remoting.fullscreen);
/** @private */
- this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this);
+ this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
/** @private */
this.screenOptionsMenu_ = new remoting.MenuButton(
@@ -154,15 +152,15 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
'click', this.callSetScreenMode_, false);
this.fullScreenButton_.addEventListener(
'click', this.callToggleFullScreen_, false);
+ this.defineEvents(Object.keys(remoting.ClientSession.Events));
};
-/**
- * @param {?function(remoting.ClientSession.State,
- remoting.ClientSession.State):void} onStateChange
- * The callback to invoke when the session changes state.
- */
-remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) {
- this.onStateChange_ = onStateChange;
+base.extend(remoting.ClientSession, base.EventSource);
+
+/** @enum {string} */
+remoting.ClientSession.Events = {
+ stateChanged: 'stateChanged',
+ videoChannelStateChanged: 'videoChannelStateChanged'
};
/**
@@ -229,7 +227,20 @@ remoting.ClientSession.State.fromString = function(state) {
throw "Invalid ClientSession.State: " + state;
}
return remoting.ClientSession.State[state];
-}
+};
+
+/**
+ @constructor
+ @param {remoting.ClientSession.State} current
+ @param {remoting.ClientSession.State} previous
+*/
+remoting.ClientSession.StateEvent = function(current, previous) {
+ /** @type {remoting.ClientSession.State} */
+ this.previous = previous
+
+ /** @type {remoting.ClientSession.State} */
+ this.current = current;
+};
/** @enum {number} */
remoting.ClientSession.ConnectionError = {
@@ -561,19 +572,32 @@ remoting.ClientSession.prototype.removePlugin = function() {
};
/**
+ * Disconnect the current session with a particular |error|. The session will
+ * raise a |stateChanged| event in response to it. The caller should then call
+ * |cleanup| to remove and destroy the <embed> element.
+ *
+ * @param {remoting.Error} error The reason for the disconnection. Use
+ * remoting.Error.NONE if there is no error.
+ * @return {void} Nothing.
+ */
+remoting.ClientSession.prototype.disconnect = function(error) {
+ var state = (error == remoting.Error.NONE) ?
+ remoting.ClientSession.State.CLOSED :
+ remoting.ClientSession.State.FAILED;
+
+ // The plugin won't send a state change notification, so we explicitly log
+ // the fact that the connection has closed.
+ this.logToServer.logClientSessionStateChange(state, error, this.mode_);
+ this.error_ = error;
+ this.setState_(state);
+};
+
+/**
* Deletes the <embed> element from the container and disconnects.
*
- * @param {boolean} isUserInitiated True for user-initiated disconnects, False
- * for disconnects due to connection failures.
* @return {void} Nothing.
*/
-remoting.ClientSession.prototype.disconnect = function(isUserInitiated) {
- if (isUserInitiated) {
- // The plugin won't send a state change notification, so we explicitly log
- // the fact that the connection has closed.
- this.logToServer.logClientSessionStateChange(
- remoting.ClientSession.State.CLOSED, remoting.Error.NONE, this.mode_);
- }
+remoting.ClientSession.prototype.cleanup = function() {
remoting.wcsSandbox.setOnIq(null);
this.sendIq_(
'<cli:iq ' +
@@ -969,6 +993,9 @@ remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
} else {
this.plugin_.element().classList.remove("session-client-inactive");
}
+
+ this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
+ ready);
};
/**
@@ -1022,9 +1049,10 @@ remoting.ClientSession.prototype.setState_ = function(newState) {
if (this.state_ == remoting.ClientSession.State.CONNECTED) {
this.createGnubbyAuthHandler_();
}
- if (this.onStateChange_) {
- this.onStateChange_(oldState, newState);
- }
+
+ this.raiseEvent(remoting.ClientSession.Events.stateChanged,
+ new remoting.ClientSession.StateEvent(newState, oldState)
+ );
};
/**
@@ -1071,9 +1099,9 @@ remoting.ClientSession.prototype.onResize = function() {
*/
remoting.ClientSession.prototype.pauseVideo = function(pause) {
if (this.plugin_) {
- this.plugin_.pauseVideo(pause)
+ this.plugin_.pauseVideo(pause);
}
-}
+};
/**
* Requests that the host pause or resume audio.
diff --git a/remoting/webapp/session_connector.js b/remoting/webapp/session_connector.js
index 3c507d4..d3169bb 100644
--- a/remoting/webapp/session_connector.js
+++ b/remoting/webapp/session_connector.js
@@ -59,6 +59,19 @@ remoting.SessionConnector = function(pluginParent, onOk, onError,
*/
this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
+ /**
+ * @type {remoting.SmartReconnector}
+ * @private
+ */
+ this.reconnector_ = null;
+
+ /**
+ * @private
+ */
+ this.bound_ = {
+ onStateChange : this.onStateChange_.bind(this)
+ };
+
// Initialize/declare per-connection state.
this.reset();
};
@@ -393,7 +406,9 @@ remoting.SessionConnector.prototype.createSession_ = function() {
authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
- this.clientSession_.setOnStateChange(this.onStateChange_.bind(this));
+ this.clientSession_.addEventListener(
+ remoting.ClientSession.Events.stateChanged,
+ this.bound_.onStateChange);
this.clientSession_.createPluginAndConnect(this.pluginParent_,
this.onExtensionMessage_);
};
@@ -404,20 +419,24 @@ remoting.SessionConnector.prototype.createSession_ = function() {
* events). Errors that occur while connecting either trigger a reconnect
* or notify the onError handler.
*
- * @param {number} oldState The previous state of the plugin.
- * @param {number} newState The current state of the plugin.
+ * @param {remoting.ClientSession.StateEvent} event
* @return {void} Nothing.
* @private
*/
-remoting.SessionConnector.prototype.onStateChange_ =
- function(oldState, newState) {
- switch (newState) {
+remoting.SessionConnector.prototype.onStateChange_ = function(event) {
+ switch (event.current) {
case remoting.ClientSession.State.CONNECTED:
// When the connection succeeds, deregister for state-change callbacks
// and pass the session to the onOk callback. It is expected that it
// will register a new state-change callback to handle disconnect
// or error conditions.
- this.clientSession_.setOnStateChange(null);
+ this.clientSession_.removeEventListener(
+ remoting.ClientSession.Events.stateChanged,
+ this.bound_.onStateChange);
+
+ base.dispose(this.reconnector_);
+ this.reconnector_ =
+ new remoting.SmartReconnector(this, this.clientSession_);
this.onOk_(this.clientSession_);
break;
@@ -452,6 +471,7 @@ remoting.SessionConnector.prototype.onStateChange_ =
}
if (error == remoting.Error.HOST_IS_OFFLINE &&
this.refreshHostJidIfOffline_) {
+ // The plugin will be re-created when the host finished refreshing
remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
} else {
this.onError_(error);
@@ -459,7 +479,7 @@ remoting.SessionConnector.prototype.onStateChange_ =
break;
default:
- console.error('Unexpected client plugin state: ' + newState);
+ console.error('Unexpected client plugin state: ' + event.current);
// This should only happen if the web-app and client plugin get out of
// sync, and even then the version check should ensure compatibility.
this.onError_(remoting.Error.MISSING_PLUGIN);
@@ -512,4 +532,4 @@ remoting.SessionConnector.prototype.normalizeAccessCode_ =
function(accessCode) {
// Trim whitespace.
return accessCode.replace(/\s/g, '');
-};
+}; \ No newline at end of file
diff --git a/remoting/webapp/smart_reconnector.js b/remoting/webapp/smart_reconnector.js
new file mode 100644
index 0000000..55186b42
--- /dev/null
+++ b/remoting/webapp/smart_reconnector.js
@@ -0,0 +1,143 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * Class handling reconnecting the session when it is disconnected due to
+ * network failure.
+ *
+ * The SmartReconnector listens for changes in connection state of
+ * |clientSession| to determine if a reconnection is needed. It then calls into
+ * |connector| to reconnect the session.
+ */
+
+'use strict';
+
+/** @suppress {duplicate} */
+var remoting = remoting || {};
+
+/**
+ * @constructor
+ * @param {remoting.SessionConnector} connector This is used to reconnect the
+ * the session when necessary
+ * @param {remoting.ClientSession} clientSession This represents the current
+ * remote desktop connection. It is used to monitor the changes in
+ * connection state.
+ * @implements {base.Disposable}
+ */
+remoting.SmartReconnector = function(connector, clientSession) {
+ /** @private */
+ this.connector_ = connector;
+
+ /** @private */
+ this.clientSession_ = clientSession;
+
+ /** @private */
+ this.reconnectTimerId_ = null;
+
+ /** @private */
+ this.connectionTimeoutTimerId_ = null;
+
+ /** @private */
+ this.bound_ = {
+ reconnect: this.reconnect_.bind(this),
+ reconnectAsync: this.reconnectAsync_.bind(this),
+ startReconnectTimeout: this.startReconnectTimeout_.bind(this),
+ stateChanged: this.stateChanged_.bind(this),
+ videoChannelStateChanged: this.videoChannelStateChanged_.bind(this)
+ };
+
+ clientSession.addEventListener(
+ remoting.ClientSession.Events.stateChanged,
+ this.bound_.stateChanged);
+ clientSession.addEventListener(
+ remoting.ClientSession.Events.videoChannelStateChanged,
+ this.bound_.videoChannelStateChanged);
+};
+
+// The online event only means the network adapter is enabled, but
+// it doesn't necessarily mean that we have a working internet connection.
+// Therefore, delay the connection by |kReconnectDelay| to allow for the network
+// to connect.
+remoting.SmartReconnector.kReconnectDelay = 2000;
+
+// If no frames are received from the server for more than |kConnectionTimeout|,
+// disconnect the session.
+remoting.SmartReconnector.kConnectionTimeout = 10000;
+
+remoting.SmartReconnector.prototype = {
+ reconnect_: function() {
+ this.cancelPending_();
+ remoting.disconnect();
+ remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
+ this.connector_.reconnect();
+ },
+
+ reconnectAsync_: function() {
+ this.cancelPending_();
+ remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
+ this.reconnectTimerId_ = window.setTimeout(
+ this.bound_.reconnect, remoting.SmartReconnector.kReconnectDelay);
+ },
+
+ /**
+ * @param {remoting.ClientSession.StateEvent} event
+ */
+ stateChanged_: function(event) {
+ var State = remoting.ClientSession.State;
+ if (event.previous === State.CONNECTED && event.current === State.FAILED) {
+ this.cancelPending_();
+ if (navigator.onLine) {
+ this.reconnect_();
+ } else {
+ window.addEventListener('online', this.bound_.reconnectAsync, false);
+ }
+ }
+ },
+
+ /**
+ * @param {boolean} active This function is called if no frames are received
+ * on the client for more than 1 second.
+ */
+ videoChannelStateChanged_: function (active) {
+ this.cancelPending_();
+ if (!active) {
+ // If the channel becomes inactive due to a lack of network connection,
+ // wait for it to go online. The plugin will try to reconnect the video
+ // channel once it is online. If the video channels doesn't finish
+ // reconnecting within the timeout, tear down the session and reconnect.
+ if (navigator.onLine) {
+ this.reconnect_();
+ } else {
+ window.addEventListener(
+ 'online', this.bound_.startReconnectTimeout, false);
+ }
+ }
+ },
+
+ startReconnectTimeout_: function () {
+ this.cancelPending_();
+ this.connectionTimeoutTimerId_ = window.setTimeout(
+ this.bound_.reconnect, remoting.SmartReconnector.kConnectionTimeout);
+ },
+
+ cancelPending_: function() {
+ window.removeEventListener(
+ 'online', this.bound_.startReconnectTimeout, false);
+ window.removeEventListener('online', this.bound_.reconnectAsync, false);
+ window.clearTimeout(this.reconnectTimerId_);
+ window.clearTimeout(this.connectionTimeoutTimerId_);
+ this.reconnectTimerId_ = null;
+ this.connectionTimeoutTimerId_ = null;
+ },
+
+ dispose: function() {
+ this.clientSession_.removeEventListener(
+ remoting.ClientSession.Events.stateChanged,
+ this.bound_.stateChanged);
+ this.clientSession_.removeEventListener(
+ remoting.ClientSession.Events.videoChannelStateChanged,
+ this.bound_.videoChannelStateChanged);
+ }
+};