OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @fileoverview | |
7 * Class handling creation and teardown of a remoting client session. | |
8 * | |
9 * The ClientSession class controls lifetime of the client plugin | |
10 * object and provides the plugin with the functionality it needs to | |
11 * establish connection, e.g. delivers incoming/outgoing signaling | |
12 * messages. | |
13 * | |
14 * This class should not access the plugin directly, instead it should | |
15 * do it through ClientPlugin class which abstracts plugin version | |
16 * differences. | |
17 */ | |
18 | |
19 'use strict'; | |
20 | |
21 /** @suppress {duplicate} */ | |
22 var remoting = remoting || {}; | |
23 | |
24 /** | |
25 * @param {remoting.ClientPlugin} plugin | |
26 * @param {remoting.SignalStrategy} signalStrategy Signal strategy. | |
27 * @param {remoting.ClientSession.EventHandler} listener | |
28 * | |
29 * @constructor | |
30 * @extends {base.EventSourceImpl} | |
31 * @implements {base.Disposable} | |
32 * @implements {remoting.ClientPlugin.ConnectionEventHandler} | |
33 */ | |
34 remoting.ClientSession = function(plugin, signalStrategy, listener) { | |
35 base.inherits(this, base.EventSourceImpl); | |
36 | |
37 /** @private */ | |
38 this.state_ = remoting.ClientSession.State.INITIALIZING; | |
39 | |
40 /** @private {!remoting.Error} */ | |
41 this.error_ = remoting.Error.none(); | |
42 | |
43 /** @private {remoting.Host} */ | |
44 this.host_ = null; | |
45 | |
46 /** @private {remoting.CredentialsProvider} */ | |
47 this.credentialsProvider_ = null; | |
48 | |
49 /** @private */ | |
50 this.sessionId_ = ''; | |
51 | |
52 /** @private */ | |
53 this.listener_ = listener; | |
54 | |
55 /** @private */ | |
56 this.hasReceivedFrame_ = false; | |
57 | |
58 /** @private */ | |
59 this.logToServer_ = new remoting.LogToServer(signalStrategy); | |
60 | |
61 /** @private */ | |
62 this.signalStrategy_ = signalStrategy; | |
63 base.debug.assert(this.signalStrategy_.getState() == | |
64 remoting.SignalStrategy.State.CONNECTED); | |
65 this.signalStrategy_.setIncomingStanzaCallback( | |
66 this.onIncomingMessage_.bind(this)); | |
67 | |
68 /** @private {remoting.FormatIq} */ | |
69 this.iqFormatter_ = null; | |
70 | |
71 /** | |
72 * Allow host-offline error reporting to be suppressed in situations where it | |
73 * would not be useful, for example, when using a cached host JID. | |
74 * | |
75 * @type {boolean} @private | |
76 */ | |
77 this.logHostOfflineErrors_ = true; | |
78 | |
79 /** @private {remoting.ClientPlugin} */ | |
80 this.plugin_ = plugin; | |
81 plugin.setConnectionEventHandler(this); | |
82 | |
83 /** @private */ | |
84 this.connectedDisposables_ = new base.Disposables(); | |
85 | |
86 this.defineEvents(Object.keys(remoting.ClientSession.Events)); | |
87 }; | |
88 | |
89 /** @enum {string} */ | |
90 remoting.ClientSession.Events = { | |
91 videoChannelStateChanged: 'videoChannelStateChanged' | |
92 }; | |
93 | |
94 /** | |
95 * @interface | |
96 * [START]-------> [onConnected] ------> [onDisconnected] | |
97 * | | |
98 * |-----> [OnConnectionFailed] | |
99 * | |
100 */ | |
101 remoting.ClientSession.EventHandler = function() {}; | |
102 | |
103 /** | |
104 * Called when the connection failed before it is connected. | |
105 * | |
106 * @param {!remoting.Error} error | |
107 */ | |
108 remoting.ClientSession.EventHandler.prototype.onConnectionFailed = | |
109 function(error) {}; | |
110 | |
111 /** | |
112 * Called when a new session has been connected. The |connectionInfo| will be | |
113 * valid until onDisconnected() is called. | |
114 * | |
115 * @param {!remoting.ConnectionInfo} connectionInfo | |
116 */ | |
117 remoting.ClientSession.EventHandler.prototype.onConnected = | |
118 function(connectionInfo) {}; | |
119 | |
120 /** | |
121 * Called when the current session has been disconnected. | |
122 * | |
123 * @param {!remoting.Error} reason Reason that the session is disconnected. | |
124 * Set to remoting.Error.none() if there is no error. | |
125 */ | |
126 remoting.ClientSession.EventHandler.prototype.onDisconnected = | |
127 function(reason) {}; | |
128 | |
129 // Note that the positive values in both of these enums are copied directly | |
130 // from connection_to_host.h and must be kept in sync. Code in | |
131 // chromoting_instance.cc converts the C++ enums into strings that must match | |
132 // the names given here. | |
133 // The negative values represent state transitions that occur within the | |
134 // web-app that have no corresponding plugin state transition. | |
135 /** @enum {number} */ | |
136 remoting.ClientSession.State = { | |
137 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting. | |
138 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error. | |
139 CREATED: -1, | |
140 UNKNOWN: 0, | |
141 INITIALIZING: 1, | |
142 CONNECTING: 2, | |
143 // We don't currently receive AUTHENTICATED from the host - it comes through | |
144 // as 'CONNECTING' instead. | |
145 // TODO(garykac) Update chromoting_instance.cc to send this once we've | |
146 // shipped a webapp release with support for AUTHENTICATED. | |
147 AUTHENTICATED: 3, | |
148 CONNECTED: 4, | |
149 CLOSED: 5, | |
150 FAILED: 6 | |
151 }; | |
152 | |
153 /** | |
154 * @param {string} state The state name. | |
155 * @return {remoting.ClientSession.State} The session state enum value. | |
156 */ | |
157 remoting.ClientSession.State.fromString = function(state) { | |
158 if (!remoting.ClientSession.State.hasOwnProperty(state)) { | |
159 throw "Invalid ClientSession.State: " + state; | |
160 } | |
161 return remoting.ClientSession.State[state]; | |
162 }; | |
163 | |
164 /** @enum {number} */ | |
165 remoting.ClientSession.ConnectionError = { | |
166 UNKNOWN: -1, | |
167 NONE: 0, | |
168 HOST_IS_OFFLINE: 1, | |
169 SESSION_REJECTED: 2, | |
170 INCOMPATIBLE_PROTOCOL: 3, | |
171 NETWORK_FAILURE: 4, | |
172 HOST_OVERLOAD: 5 | |
173 }; | |
174 | |
175 /** | |
176 * @param {string} error The connection error name. | |
177 * @return {remoting.ClientSession.ConnectionError} The connection error enum. | |
178 */ | |
179 remoting.ClientSession.ConnectionError.fromString = function(error) { | |
180 if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) { | |
181 console.error('Unexpected ClientSession.ConnectionError string: ', error); | |
182 return remoting.ClientSession.ConnectionError.UNKNOWN; | |
183 } | |
184 return remoting.ClientSession.ConnectionError[error]; | |
185 } | |
186 | |
187 /** | |
188 * Type used for performance statistics collected by the plugin. | |
189 * @constructor | |
190 */ | |
191 remoting.ClientSession.PerfStats = function() {}; | |
192 /** @type {number} */ | |
193 remoting.ClientSession.PerfStats.prototype.videoBandwidth; | |
194 /** @type {number} */ | |
195 remoting.ClientSession.PerfStats.prototype.videoFrameRate; | |
196 /** @type {number} */ | |
197 remoting.ClientSession.PerfStats.prototype.captureLatency; | |
198 /** @type {number} */ | |
199 remoting.ClientSession.PerfStats.prototype.encodeLatency; | |
200 /** @type {number} */ | |
201 remoting.ClientSession.PerfStats.prototype.decodeLatency; | |
202 /** @type {number} */ | |
203 remoting.ClientSession.PerfStats.prototype.renderLatency; | |
204 /** @type {number} */ | |
205 remoting.ClientSession.PerfStats.prototype.roundtripLatency; | |
206 | |
207 // Keys for connection statistics. | |
208 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth'; | |
209 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate'; | |
210 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency'; | |
211 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency'; | |
212 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency'; | |
213 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency'; | |
214 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency'; | |
215 | |
216 /** | |
217 * Set of capabilities for which hasCapability() can be used to test. | |
218 * | |
219 * @enum {string} | |
220 */ | |
221 remoting.ClientSession.Capability = { | |
222 // When enabled this capability causes the client to send its screen | |
223 // resolution to the host once connection has been established. See | |
224 // this.plugin_.notifyClientResolution(). | |
225 SEND_INITIAL_RESOLUTION: 'sendInitialResolution', | |
226 | |
227 // Let the host know that we're interested in knowing whether or not it | |
228 // rate limits desktop-resize requests. | |
229 // TODO(kelvinp): This has been supported since M-29. Currently we only have | |
230 // <1000 users on M-29 or below. Remove this and the capability on the host. | |
231 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests', | |
232 | |
233 // Indicates that host/client supports Google Drive integration, and that the | |
234 // client should send to the host the OAuth tokens to be used by Google Drive | |
235 // on the host. | |
236 GOOGLE_DRIVE: 'googleDrive', | |
237 | |
238 // Indicates that the client supports the video frame-recording extension. | |
239 VIDEO_RECORDER: 'videoRecorder', | |
240 | |
241 // Indicates that the client supports 'cast'ing the video stream to a | |
242 // cast-enabled device. | |
243 CAST: 'casting', | |
244 }; | |
245 | |
246 /** | |
247 * Connects to |host| using |credentialsProvider| as the credentails. | |
248 * | |
249 * @param {remoting.Host} host | |
250 * @param {remoting.CredentialsProvider} credentialsProvider | |
251 */ | |
252 remoting.ClientSession.prototype.connect = function(host, credentialsProvider) { | |
253 this.host_ = host; | |
254 this.credentialsProvider_ = credentialsProvider; | |
255 this.iqFormatter_ = | |
256 new remoting.FormatIq(this.signalStrategy_.getJid(), host.jabberId); | |
257 this.plugin_.connect(this.host_, this.signalStrategy_.getJid(), | |
258 credentialsProvider); | |
259 }; | |
260 | |
261 /** | |
262 * Disconnect the current session with a particular |error|. The session will | |
263 * raise a |stateChanged| event in response to it. The caller should then call | |
264 * dispose() to remove and destroy the <embed> element. | |
265 * | |
266 * @param {!remoting.Error} error The reason for the disconnection. Use | |
267 * remoting.Error.none() if there is no error. | |
268 * @return {void} Nothing. | |
269 */ | |
270 remoting.ClientSession.prototype.disconnect = function(error) { | |
271 if (this.isFinished()) { | |
272 // Do not send the session-terminate Iq if disconnect() is already called or | |
273 // if it is initiated by the host. | |
274 return; | |
275 } | |
276 | |
277 this.sendIq_( | |
278 '<cli:iq ' + | |
279 'to="' + this.host_.jabberId + '" ' + | |
280 'type="set" ' + | |
281 'id="session-terminate" ' + | |
282 'xmlns:cli="jabber:client">' + | |
283 '<jingle ' + | |
284 'xmlns="urn:xmpp:jingle:1" ' + | |
285 'action="session-terminate" ' + | |
286 'sid="' + this.sessionId_ + '">' + | |
287 '<reason><success/></reason>' + | |
288 '</jingle>' + | |
289 '</cli:iq>'); | |
290 | |
291 var state = error.isNone() ? | |
292 remoting.ClientSession.State.CLOSED : | |
293 remoting.ClientSession.State.FAILED; | |
294 this.error_ = error; | |
295 this.setState_(state); | |
296 }; | |
297 | |
298 /** | |
299 * Deletes the <embed> element from the container and disconnects. | |
300 * | |
301 * @return {void} Nothing. | |
302 */ | |
303 remoting.ClientSession.prototype.dispose = function() { | |
304 base.dispose(this.connectedDisposables_); | |
305 this.connectedDisposables_ = null; | |
306 base.dispose(this.plugin_); | |
307 this.plugin_ = null; | |
308 }; | |
309 | |
310 /** | |
311 * @return {remoting.ClientSession.State} The current state. | |
312 */ | |
313 remoting.ClientSession.prototype.getState = function() { | |
314 return this.state_; | |
315 }; | |
316 | |
317 /** | |
318 * @return {remoting.LogToServer}. | |
319 */ | |
320 remoting.ClientSession.prototype.getLogger = function() { | |
321 return this.logToServer_; | |
322 }; | |
323 | |
324 /** | |
325 * @return {!remoting.Error} The current error code. | |
326 */ | |
327 remoting.ClientSession.prototype.getError = function() { | |
328 return this.error_; | |
329 }; | |
330 | |
331 /** | |
332 * Called when the client receives its first frame. | |
333 * | |
334 * @return {void} Nothing. | |
335 */ | |
336 remoting.ClientSession.prototype.onFirstFrameReceived = function() { | |
337 this.hasReceivedFrame_ = true; | |
338 }; | |
339 | |
340 /** | |
341 * @return {boolean} Whether the client has received a video buffer. | |
342 */ | |
343 remoting.ClientSession.prototype.hasReceivedFrame = function() { | |
344 return this.hasReceivedFrame_; | |
345 }; | |
346 | |
347 /** | |
348 * Sends a signaling message. | |
349 * | |
350 * @param {string} message XML string of IQ stanza to send to server. | |
351 * @return {void} Nothing. | |
352 * @private | |
353 */ | |
354 remoting.ClientSession.prototype.sendIq_ = function(message) { | |
355 // Extract the session id, so we can close the session later. | |
356 var parser = new DOMParser(); | |
357 var iqNode = parser.parseFromString(message, 'text/xml').firstChild; | |
358 var jingleNode = iqNode.firstChild; | |
359 if (jingleNode) { | |
360 var action = jingleNode.getAttribute('action'); | |
361 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') { | |
362 this.sessionId_ = jingleNode.getAttribute('sid'); | |
363 } | |
364 } | |
365 | |
366 console.log(base.timestamp() + this.iqFormatter_.prettifySendIq(message)); | |
367 if (this.signalStrategy_.getState() != | |
368 remoting.SignalStrategy.State.CONNECTED) { | |
369 console.log("Message above is dropped because signaling is not connected."); | |
370 return; | |
371 } | |
372 | |
373 this.signalStrategy_.sendMessage(message); | |
374 }; | |
375 | |
376 /** | |
377 * @param {string} message XML string of IQ stanza to send to server. | |
378 */ | |
379 remoting.ClientSession.prototype.onOutgoingIq = function(message) { | |
380 this.sendIq_(message); | |
381 }; | |
382 | |
383 /** | |
384 * @param {string} msg | |
385 */ | |
386 remoting.ClientSession.prototype.onDebugMessage = function(msg) { | |
387 console.log('plugin: ' + msg.trimRight()); | |
388 }; | |
389 | |
390 /** | |
391 * @param {Element} message | |
392 * @private | |
393 */ | |
394 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) { | |
395 if (!this.plugin_) { | |
396 return; | |
397 } | |
398 var formatted = new XMLSerializer().serializeToString(message); | |
399 console.log(base.timestamp() + | |
400 this.iqFormatter_.prettifyReceiveIq(formatted)); | |
401 this.plugin_.onIncomingIq(formatted); | |
402 }; | |
403 | |
404 /** | |
405 * Callback that the plugin invokes to indicate that the connection | |
406 * status has changed. | |
407 * | |
408 * @param {remoting.ClientSession.State} status The plugin's status. | |
409 * @param {remoting.ClientSession.ConnectionError} error The plugin's error | |
410 * state, if any. | |
411 */ | |
412 remoting.ClientSession.prototype.onConnectionStatusUpdate = | |
413 function(status, error) { | |
414 if (status == remoting.ClientSession.State.FAILED) { | |
415 switch (error) { | |
416 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE: | |
417 this.error_ = new remoting.Error( | |
418 remoting.Error.Tag.HOST_IS_OFFLINE); | |
419 break; | |
420 case remoting.ClientSession.ConnectionError.SESSION_REJECTED: | |
421 this.error_ = new remoting.Error( | |
422 remoting.Error.Tag.INVALID_ACCESS_CODE); | |
423 break; | |
424 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL: | |
425 this.error_ = new remoting.Error( | |
426 remoting.Error.Tag.INCOMPATIBLE_PROTOCOL); | |
427 break; | |
428 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE: | |
429 this.error_ = new remoting.Error( | |
430 remoting.Error.Tag.P2P_FAILURE); | |
431 break; | |
432 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD: | |
433 this.error_ = new remoting.Error( | |
434 remoting.Error.Tag.HOST_OVERLOAD); | |
435 break; | |
436 default: | |
437 this.error_ = remoting.Error.unexpected(); | |
438 } | |
439 } | |
440 this.setState_(status); | |
441 }; | |
442 | |
443 /** | |
444 * Callback that the plugin invokes to indicate that the connection type for | |
445 * a channel has changed. | |
446 * | |
447 * @param {string} channel The channel name. | |
448 * @param {string} connectionType The new connection type. | |
449 * @private | |
450 */ | |
451 remoting.ClientSession.prototype.onRouteChanged = | |
452 function(channel, connectionType) { | |
453 console.log('plugin: Channel ' + channel + ' using ' + | |
454 connectionType + ' connection.'); | |
455 this.logToServer_.setConnectionType(connectionType); | |
456 }; | |
457 | |
458 /** | |
459 * Callback that the plugin invokes to indicate when the connection is | |
460 * ready. | |
461 * | |
462 * @param {boolean} ready True if the connection is ready. | |
463 */ | |
464 remoting.ClientSession.prototype.onConnectionReady = function(ready) { | |
465 // TODO(jamiewalch): Currently, the logic for determining whether or not the | |
466 // connection is available is based solely on whether or not any video frames | |
467 // have been received recently. which leads to poor UX on slow connections. | |
468 // Re-enable this once crbug.com/435315 has been fixed. | |
469 var ignoreVideoChannelState = true; | |
470 if (ignoreVideoChannelState) { | |
471 console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.'); | |
472 return; | |
473 } | |
474 | |
475 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged, | |
476 ready); | |
477 }; | |
478 | |
479 /** @return {boolean} */ | |
480 remoting.ClientSession.prototype.isFinished = function() { | |
481 var finishedStates = [ | |
482 remoting.ClientSession.State.CLOSED, | |
483 remoting.ClientSession.State.FAILED, | |
484 remoting.ClientSession.State.CONNECTION_CANCELED, | |
485 remoting.ClientSession.State.CONNECTION_DROPPED | |
486 ]; | |
487 return finishedStates.indexOf(this.getState()) !== -1; | |
488 }; | |
489 /** | |
490 * @param {remoting.ClientSession.State} newState The new state for the session. | |
491 * @return {void} Nothing. | |
492 * @private | |
493 */ | |
494 remoting.ClientSession.prototype.setState_ = function(newState) { | |
495 var oldState = this.state_; | |
496 this.state_ = this.translateState_(oldState, newState); | |
497 | |
498 if (newState == remoting.ClientSession.State.CONNECTED) { | |
499 this.connectedDisposables_.add( | |
500 new base.RepeatingTimer(this.reportStatistics.bind(this), 1000)); | |
501 } else if (this.isFinished()) { | |
502 base.dispose(this.connectedDisposables_); | |
503 this.connectedDisposables_ = null; | |
504 } | |
505 | |
506 this.notifyStateChanges_(oldState, this.state_); | |
507 this.logToServer_.logClientSessionStateChange(this.state_, this.error_); | |
508 }; | |
509 | |
510 /** | |
511 * @param {remoting.ClientSession.State} oldState The new state for the session. | |
512 * @param {remoting.ClientSession.State} newState The new state for the session. | |
513 * @private | |
514 */ | |
515 remoting.ClientSession.prototype.notifyStateChanges_ = | |
516 function(oldState, newState) { | |
517 /** @type {remoting.Error} */ | |
518 var error; | |
519 switch (this.state_) { | |
520 case remoting.ClientSession.State.CONNECTED: | |
521 console.log('Connection established.'); | |
522 var connectionInfo = new remoting.ConnectionInfo( | |
523 this.host_, this.credentialsProvider_, this, this.plugin_); | |
524 this.listener_.onConnected(connectionInfo); | |
525 break; | |
526 | |
527 case remoting.ClientSession.State.CONNECTING: | |
528 remoting.identity.getEmail().then(function(/** string */ email) { | |
529 console.log('Connecting as ' + email); | |
530 }); | |
531 break; | |
532 | |
533 case remoting.ClientSession.State.AUTHENTICATED: | |
534 console.log('Connection authenticated.'); | |
535 break; | |
536 | |
537 case remoting.ClientSession.State.INITIALIZING: | |
538 console.log('Connection initializing .'); | |
539 break; | |
540 | |
541 case remoting.ClientSession.State.CLOSED: | |
542 console.log('Connection closed.'); | |
543 this.listener_.onDisconnected(remoting.Error.none()); | |
544 break; | |
545 | |
546 case remoting.ClientSession.State.CONNECTION_CANCELED: | |
547 case remoting.ClientSession.State.FAILED: | |
548 error = this.getError(); | |
549 if (!error.isNone()) { | |
550 console.error('Connection failed: ' + error.toString()); | |
551 } | |
552 this.listener_.onConnectionFailed(error); | |
553 break; | |
554 | |
555 case remoting.ClientSession.State.CONNECTION_DROPPED: | |
556 error = this.getError(); | |
557 console.error('Connection dropped: ' + error.toString()); | |
558 this.listener_.onDisconnected(error); | |
559 break; | |
560 | |
561 default: | |
562 console.error('Unexpected client plugin state: ' + newState); | |
563 } | |
564 }; | |
565 | |
566 /** | |
567 * @param {remoting.ClientSession.State} previous | |
568 * @param {remoting.ClientSession.State} current | |
569 * @return {remoting.ClientSession.State} | |
570 * @private | |
571 */ | |
572 remoting.ClientSession.prototype.translateState_ = function(previous, current) { | |
573 var State = remoting.ClientSession.State; | |
574 if (previous == State.CONNECTING || previous == State.AUTHENTICATED) { | |
575 if (current == State.CLOSED) { | |
576 return remoting.ClientSession.State.CONNECTION_CANCELED; | |
577 } else if (current == State.FAILED && | |
578 this.error_.hasTag(remoting.Error.Tag.HOST_IS_OFFLINE) && | |
579 !this.logHostOfflineErrors_) { | |
580 // The application requested host-offline errors to be suppressed, for | |
581 // example, because this connection attempt is using a cached host JID. | |
582 console.log('Suppressing host-offline error.'); | |
583 return State.CONNECTION_CANCELED; | |
584 } | |
585 } else if (previous == State.CONNECTED && current == State.FAILED) { | |
586 return State.CONNECTION_DROPPED; | |
587 } | |
588 return current; | |
589 }; | |
590 | |
591 /** @private */ | |
592 remoting.ClientSession.prototype.reportStatistics = function() { | |
593 this.logToServer_.logStatistics(this.plugin_.getPerfStats()); | |
594 }; | |
595 | |
596 /** | |
597 * Enable or disable logging of connection errors due to a host being offline. | |
598 * For example, if attempting a connection using a cached JID, host-offline | |
599 * errors should not be logged because the JID will be refreshed and the | |
600 * connection retried. | |
601 * | |
602 * @param {boolean} enable True to log host-offline errors; false to suppress. | |
603 */ | |
604 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) { | |
605 this.logHostOfflineErrors_ = enable; | |
606 }; | |
607 | |
OLD | NEW |