| // 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. |
| |
| (function() { |
| var internal = mojo.internal; |
| |
| /** |
| * The state of |endpoint|. If both the endpoint and its peer have been |
| * closed, removes it from |endpoints_|. |
| * @enum {string} |
| */ |
| var EndpointStateUpdateType = { |
| ENDPOINT_CLOSED: 'endpoint_closed', |
| PEER_ENDPOINT_CLOSED: 'peer_endpoint_closed' |
| }; |
| |
| function check(condition, output) { |
| if (!condition) { |
| // testharness.js does not rethrow errors so the error stack needs to be |
| // included as a string in the error we throw for debugging layout tests. |
| throw new Error((new Error()).stack); |
| } |
| } |
| |
| function InterfaceEndpoint(router, interfaceId) { |
| this.router_ = router; |
| this.id = interfaceId; |
| this.closed = false; |
| this.peerClosed = false; |
| this.handleCreated = false; |
| this.disconnectReason = null; |
| this.client = null; |
| } |
| |
| InterfaceEndpoint.prototype.sendMessage = function(message) { |
| message.setInterfaceId(this.id); |
| return this.router_.connector_.accept(message); |
| }; |
| |
| function Router(handle, setInterfaceIdNamespaceBit) { |
| if (!(handle instanceof MojoHandle)) { |
| throw new Error("Router constructor: Not a handle"); |
| } |
| if (setInterfaceIdNamespaceBit === undefined) { |
| setInterfaceIdNamespaceBit = false; |
| } |
| |
| this.connector_ = new internal.Connector(handle); |
| |
| this.connector_.setIncomingReceiver({ |
| accept: this.accept.bind(this), |
| }); |
| this.connector_.setErrorHandler({ |
| onError: this.onPipeConnectionError.bind(this), |
| }); |
| |
| this.setInterfaceIdNamespaceBit_ = setInterfaceIdNamespaceBit; |
| // |cachedMessageData| caches infomation about a message, so it can be |
| // processed later if a client is not yet attached to the target endpoint. |
| this.cachedMessageData = null; |
| this.controlMessageHandler_ = new internal.PipeControlMessageHandler(this); |
| this.controlMessageProxy_ = |
| new internal.PipeControlMessageProxy(this.connector_); |
| this.nextInterfaceIdValue_ = 1; |
| this.encounteredError_ = false; |
| this.endpoints_ = new Map(); |
| } |
| |
| Router.prototype.associateInterface = function(handleToSend) { |
| if (!handleToSend.pendingAssociation()) { |
| return internal.kInvalidInterfaceId; |
| } |
| |
| var id = 0; |
| do { |
| if (this.nextInterfaceIdValue_ >= internal.kInterfaceIdNamespaceMask) { |
| this.nextInterfaceIdValue_ = 1; |
| } |
| id = this.nextInterfaceIdValue_++; |
| if (this.setInterfaceIdNamespaceBit_) { |
| id += internal.kInterfaceIdNamespaceMask; |
| } |
| } while (this.endpoints_.has(id)); |
| |
| var endpoint = new InterfaceEndpoint(this, id); |
| this.endpoints_.set(id, endpoint); |
| if (this.encounteredError_) { |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.PEER_ENDPOINT_CLOSED); |
| } |
| endpoint.handleCreated = true; |
| |
| if (!handleToSend.notifyAssociation(id, this)) { |
| // The peer handle of |handleToSend|, which is supposed to join this |
| // associated group, has been closed. |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.ENDPOINT_CLOSED); |
| |
| pipeControlMessageproxy.notifyPeerEndpointClosed(id, |
| handleToSend.disconnectReason()); |
| } |
| |
| return id; |
| }; |
| |
| Router.prototype.attachEndpointClient = function( |
| interfaceEndpointHandle, interfaceEndpointClient) { |
| check(internal.isValidInterfaceId(interfaceEndpointHandle.id())); |
| check(interfaceEndpointClient); |
| |
| var endpoint = this.endpoints_.get(interfaceEndpointHandle.id()); |
| check(endpoint); |
| check(!endpoint.client); |
| check(!endpoint.closed); |
| endpoint.client = interfaceEndpointClient; |
| |
| if (endpoint.peerClosed) { |
| setTimeout(endpoint.client.notifyError.bind(endpoint.client), 0); |
| } |
| |
| if (this.cachedMessageData && interfaceEndpointHandle.id() === |
| this.cachedMessageData.message.getInterfaceId()) { |
| setTimeout((function() { |
| if (!this.cachedMessageData) { |
| return; |
| } |
| |
| var targetEndpoint = this.endpoints_.get( |
| this.cachedMessageData.message.getInterfaceId()); |
| // Check that the target endpoint's client still exists. |
| if (targetEndpoint && targetEndpoint.client) { |
| var message = this.cachedMessageData.message; |
| var messageValidator = this.cachedMessageData.messageValidator; |
| this.cachedMessageData = null; |
| this.connector_.resumeIncomingMethodCallProcessing(); |
| var ok = endpoint.client.handleIncomingMessage(message, |
| messageValidator); |
| |
| // Handle invalid cached incoming message. |
| if (!internal.isTestingMode() && !ok) { |
| this.connector_.handleError(true, true); |
| } |
| } |
| }).bind(this), 0); |
| } |
| |
| return endpoint; |
| }; |
| |
| Router.prototype.detachEndpointClient = function( |
| interfaceEndpointHandle) { |
| check(internal.isValidInterfaceId(interfaceEndpointHandle.id())); |
| var endpoint = this.endpoints_.get(interfaceEndpointHandle.id()); |
| check(endpoint); |
| check(endpoint.client); |
| check(!endpoint.closed); |
| |
| endpoint.client = null; |
| }; |
| |
| Router.prototype.createLocalEndpointHandle = function( |
| interfaceId) { |
| if (!internal.isValidInterfaceId(interfaceId)) { |
| return new internal.InterfaceEndpointHandle(); |
| } |
| |
| // Unless it is the master ID, |interfaceId| is from the remote side and |
| // therefore its namespace bit is supposed to be different than the value |
| // that this router would use. |
| if (!internal.isMasterInterfaceId(interfaceId) && |
| this.setInterfaceIdNamespaceBit_ === |
| internal.hasInterfaceIdNamespaceBitSet(interfaceId)) { |
| return new internal.InterfaceEndpointHandle(); |
| } |
| |
| var endpoint = this.endpoints_.get(interfaceId); |
| |
| if (!endpoint) { |
| endpoint = new InterfaceEndpoint(this, interfaceId); |
| this.endpoints_.set(interfaceId, endpoint); |
| |
| check(!endpoint.handleCreated); |
| |
| if (this.encounteredError_) { |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.PEER_ENDPOINT_CLOSED); |
| } |
| } else { |
| // If the endpoint already exist, it is because we have received a |
| // notification that the peer endpoint has closed. |
| check(!endpoint.closed); |
| check(endpoint.peerClosed); |
| |
| if (endpoint.handleCreated) { |
| return new internal.InterfaceEndpointHandle(); |
| } |
| } |
| |
| endpoint.handleCreated = true; |
| return new internal.InterfaceEndpointHandle(interfaceId, this); |
| }; |
| |
| Router.prototype.accept = function(message) { |
| var messageValidator = new internal.Validator(message); |
| var err = messageValidator.validateMessageHeader(); |
| |
| var ok = false; |
| if (err !== internal.validationError.NONE) { |
| internal.reportValidationError(err); |
| } else if (message.deserializeAssociatedEndpointHandles(this)) { |
| if (internal.isPipeControlMessage(message)) { |
| ok = this.controlMessageHandler_.accept(message); |
| } else { |
| var interfaceId = message.getInterfaceId(); |
| var endpoint = this.endpoints_.get(interfaceId); |
| if (!endpoint || endpoint.closed) { |
| return true; |
| } |
| |
| if (!endpoint.client) { |
| // We need to wait until a client is attached in order to dispatch |
| // further messages. |
| this.cachedMessageData = {message: message, |
| messageValidator: messageValidator}; |
| this.connector_.pauseIncomingMethodCallProcessing(); |
| return true; |
| } |
| ok = endpoint.client.handleIncomingMessage(message, messageValidator); |
| } |
| } |
| return ok; |
| }; |
| |
| Router.prototype.close = function() { |
| this.connector_.close(); |
| // Closing the message pipe won't trigger connection error handler. |
| // Explicitly call onPipeConnectionError() so that associated endpoints |
| // will get notified. |
| this.onPipeConnectionError(); |
| }; |
| |
| Router.prototype.onPeerAssociatedEndpointClosed = function(interfaceId, |
| reason) { |
| var endpoint = this.endpoints_.get(interfaceId); |
| if (!endpoint) { |
| endpoint = new InterfaceEndpoint(this, interfaceId); |
| this.endpoints_.set(interfaceId, endpoint); |
| } |
| |
| if (reason) { |
| endpoint.disconnectReason = reason; |
| } |
| |
| if (!endpoint.peerClosed) { |
| if (endpoint.client) { |
| setTimeout(endpoint.client.notifyError.bind(endpoint.client, reason), |
| 0); |
| } |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.PEER_ENDPOINT_CLOSED); |
| } |
| return true; |
| }; |
| |
| Router.prototype.onPipeConnectionError = function() { |
| this.encounteredError_ = true; |
| |
| for (var endpoint of this.endpoints_.values()) { |
| if (endpoint.client) { |
| setTimeout( |
| endpoint.client.notifyError.bind( |
| endpoint.client, endpoint.disconnectReason), |
| 0); |
| } |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.PEER_ENDPOINT_CLOSED); |
| } |
| }; |
| |
| Router.prototype.closeEndpointHandle = function(interfaceId, reason) { |
| if (!internal.isValidInterfaceId(interfaceId)) { |
| return; |
| } |
| var endpoint = this.endpoints_.get(interfaceId); |
| check(endpoint); |
| check(!endpoint.client); |
| check(!endpoint.closed); |
| |
| this.updateEndpointStateMayRemove(endpoint, |
| EndpointStateUpdateType.ENDPOINT_CLOSED); |
| |
| if (!internal.isMasterInterfaceId(interfaceId) || reason) { |
| this.controlMessageProxy_.notifyPeerEndpointClosed(interfaceId, reason); |
| } |
| |
| if (this.cachedMessageData && interfaceId === |
| this.cachedMessageData.message.getInterfaceId()) { |
| this.cachedMessageData = null; |
| this.connector_.resumeIncomingMethodCallProcessing(); |
| } |
| }; |
| |
| Router.prototype.updateEndpointStateMayRemove = function(endpoint, |
| endpointStateUpdateType) { |
| if (endpointStateUpdateType === EndpointStateUpdateType.ENDPOINT_CLOSED) { |
| endpoint.closed = true; |
| } else { |
| endpoint.peerClosed = true; |
| } |
| if (endpoint.closed && endpoint.peerClosed) { |
| this.endpoints_.delete(endpoint.id); |
| } |
| }; |
| |
| internal.Router = Router; |
| })(); |