| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Reprents a message in the router's message queue that is ready for dispatch |
| * to an endpoint. This is needed because there are times when the router |
| * is not able to dispatch a message (e.g. if an interface client has not been |
| * bound yet). In those cases, the router must hold onto the message in its own |
| * message queue until dispatch is possible. |
| * |
| * @typedef {{ |
| * header: !mojo.internal.MessageHeader, |
| * buffer: !ArrayBuffer, |
| * handles: !Array<MojoHandle>, |
| * }} |
| */ |
| mojo.internal.interfaceSupport.RouterMessage; |
| |
| /** |
| * Owns a single message pipe handle and facilitates message sending and routing |
| * on behalf of all the pipe's local Endpoints. |
| */ |
| mojo.internal.interfaceSupport.Router = class { |
| /** |
| * @param {!MojoHandle} pipe |
| * @param {boolean} setNamespaceBit |
| * @public |
| */ |
| constructor(pipe, setNamespaceBit) { |
| /** @const {!MojoHandle} */ |
| this.pipe_ = pipe; |
| |
| /** @const {!Array<mojo.internal.interfaceSupport.RouterMessage>}*/ |
| this.messages_ = []; |
| /** |
| * Flag used to keep track of message dispatch. This is necessary because |
| * it is possible that a message dispatch could trigger additional message |
| * dispatch. The dispatch logic should check that this flag is not set |
| * before attempting to dispatch messages. |
| * |
| * @type {boolean} |
| */ |
| this.dispatchInProgress_ = false; |
| |
| /** @const {!mojo.internal.interfaceSupport.HandleReader} */ |
| this.reader_ = new mojo.internal.interfaceSupport.HandleReader(pipe); |
| this.reader_.onRead = this.onMessageReceived_.bind(this); |
| this.reader_.onError = this.onError_.bind(this); |
| |
| /** @const {!Map<number, !mojo.internal.interfaceSupport.Endpoint>} */ |
| this.endpoints_ = new Map(); |
| |
| /** @private {number} */ |
| this.nextInterfaceId_ = 1; |
| |
| /** @const {number} */ |
| this.interfaceIdNamespace_ = |
| setNamespaceBit ? mojo.internal.kInterfaceNamespaceBit : 0; |
| |
| /** @const {!mojo.internal.interfaceSupport.PipeControlMessageHandler} */ |
| this.pipeControlHandler_ = |
| new mojo.internal.interfaceSupport.PipeControlMessageHandler( |
| this, this.onPeerEndpointClosed_.bind(this)); |
| } |
| |
| /** @return {!MojoHandle} */ |
| get pipe() { |
| return this.pipe_; |
| } |
| |
| /** @return {number} */ |
| generateInterfaceId() { |
| return (this.nextInterfaceId_++ | this.interfaceIdNamespace_) >>> 0; |
| } |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {number} interfaceId |
| */ |
| addEndpoint(endpoint, interfaceId) { |
| if (interfaceId === 0) { |
| this.reader_.start(); |
| } |
| console.assert( |
| this.isReading(), 'adding a secondary endpoint with no primary'); |
| this.endpoints_.set(interfaceId, endpoint); |
| this.dispatchMessages_(); |
| } |
| |
| /** @param {number} interfaceId */ |
| removeEndpoint(interfaceId) { |
| this.endpoints_.delete(interfaceId); |
| if (interfaceId === 0) { |
| this.reader_.stop(); |
| } |
| } |
| |
| close() { |
| console.assert( |
| this.endpoints_.size === 0, |
| 'closing primary endpoint with secondary endpoints still bound'); |
| this.reader_.stopAndCloseHandle(); |
| } |
| |
| /** @param {number} interfaceId */ |
| closeEndpoint(interfaceId) { |
| this.removeEndpoint(interfaceId); |
| if (interfaceId === 0) { |
| this.close(); |
| } else { |
| this.pipeControlHandler_.notifyEndpointClosed(interfaceId); |
| } |
| } |
| |
| /** @return {boolean} */ |
| isReading() { |
| return !this.reader_.isStopped(); |
| } |
| |
| /** @param {!mojo.internal.Message} message */ |
| send(message) { |
| this.pipe_.writeMessage(message.buffer, message.handles); |
| } |
| |
| /** |
| * @param {!ArrayBuffer} buffer |
| * @param {!Array<MojoHandle>} handles |
| */ |
| onMessageReceived_(buffer, handles) { |
| if (!this.checkSize_(buffer)) { |
| return; |
| } |
| |
| const header = mojo.internal.deserializeMessageHeader(new DataView(buffer)); |
| if (this.pipeControlHandler_.maybeHandleMessage(header, buffer)) { |
| return; |
| } |
| |
| this.messages_.push({ |
| header: header, |
| buffer: buffer, |
| handles: handles, |
| }); |
| |
| this.dispatchMessages_(); |
| } |
| |
| /** |
| * Does a preliminary check to see if the buffer is a well formed mojo |
| * message. |
| * |
| * @param {ArrayBuffer} buffer |
| * @returns true if size is big enough to be a potential mojo message, |
| * false otherwise |
| */ |
| checkSize_(buffer) { |
| if (buffer.byteLength < mojo.internal.kMessageV0HeaderSize) { |
| console.error('Rejecting undersized message'); |
| this.onError_(); |
| return false; |
| } |
| |
| return true |
| } |
| |
| /** |
| * Dispatch all the messages currently in the router's message queue. |
| */ |
| dispatchMessages_() { |
| if (this.dispatchInProgress_) { |
| return; |
| } |
| |
| this.dispatchInProgress_ = true; |
| |
| while (this.messages_.length > 0) { |
| const msg = this.messages_[0]; |
| const successfullyDispatched = this.dispatch_(msg); |
| if (successfullyDispatched) { |
| this.messages_.shift(); |
| } else { |
| // Dispatch was unsuccessful, stop message dispatch and wait for |
| // another router event before re-attempting dispatch. |
| break; |
| } |
| } |
| |
| this.dispatchInProgress_ = false; |
| } |
| |
| /** |
| * Dispatches a single message to the endpoint. |
| * |
| * @param {mojo.internal.interfaceSupport.RouterMessage} msg |
| * @returns true if the message was successfully dispatched, false otherwise. |
| */ |
| dispatch_(msg) { |
| const endpoint = this.endpoints_.get(msg.header.interfaceId); |
| if (!endpoint) { |
| console.error( |
| `Received message for unknown endpoint ${msg.header.interfaceId}`); |
| |
| return false; |
| } |
| if (!endpoint.isStarted) { |
| // Not client bound for endpoint. This can happen for associated |
| // interface endpoint, where the client has not been bound yet. |
| return false; |
| } |
| |
| // Dispatch to the endpoint client. |
| endpoint.onMessageReceived(msg.header, msg.buffer, msg.handles); |
| return true; |
| } |
| |
| |
| onError_() { |
| for (const endpoint of this.endpoints_.values()) { |
| endpoint.onError(); |
| } |
| this.endpoints_.clear(); |
| } |
| |
| /** @param {number} id */ |
| onPeerEndpointClosed_(id) { |
| const endpoint = this.endpoints_.get(id); |
| if (endpoint) { |
| endpoint.onError(); |
| } |
| } |
| }; |
| |
| /** |
| * Something which can receive notifications from an Endpoint; generally this is |
| * the Endpoint's owner. |
| * @interface |
| */ |
| mojo.internal.interfaceSupport.EndpointClient = class { |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {!mojo.internal.MessageHeader} header |
| * @param {!ArrayBuffer} buffer |
| * @param {!Array<MojoHandle>} handles |
| */ |
| onMessageReceived(endpoint, header, buffer, handles) {} |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {string=} reason |
| */ |
| onError(endpoint, reason = undefined) {} |
| }; |
| |
| /** |
| * Encapsulates a single interface endpoint on a multiplexed Router object. This |
| * may be the primary (possibly only) endpoint on a pipe, or a secondary |
| * associated interface endpoint. |
| */ |
| mojo.internal.interfaceSupport.Endpoint = class { |
| /** |
| * @param {mojo.internal.interfaceSupport.Router=} router |
| * @param {number=} interfaceId |
| */ |
| constructor(router = null, interfaceId = 0) { |
| /** @private {mojo.internal.interfaceSupport.Router} */ |
| this.router_ = router; |
| |
| /** @private {number} */ |
| this.interfaceId_ = interfaceId; |
| |
| /** @private {mojo.internal.interfaceSupport.ControlMessageHandler} */ |
| this.controlMessageHandler_ = |
| new mojo.internal.interfaceSupport.ControlMessageHandler(this); |
| |
| /** @private {mojo.internal.interfaceSupport.EndpointClient} */ |
| this.client_ = null; |
| |
| /** @private {number} */ |
| this.nextRequestId_ = 0; |
| |
| /** @private {mojo.internal.interfaceSupport.Endpoint} */ |
| this.localPeer_ = null; |
| } |
| |
| /** |
| * @return {{ |
| * endpoint0: !mojo.internal.interfaceSupport.Endpoint, |
| * endpoint1: !mojo.internal.interfaceSupport.Endpoint, |
| * }} |
| */ |
| static createAssociatedPair() { |
| const endpoint0 = new mojo.internal.interfaceSupport.Endpoint(); |
| const endpoint1 = new mojo.internal.interfaceSupport.Endpoint(); |
| endpoint1.localPeer_ = endpoint0; |
| endpoint0.localPeer_ = endpoint1; |
| return {endpoint0, endpoint1}; |
| } |
| |
| /** @return {mojo.internal.interfaceSupport.Router} */ |
| get router() { |
| return this.router_; |
| } |
| |
| /** @return {boolean} */ |
| isPrimary() { |
| return this.router_ !== null && this.interfaceId_ === 0; |
| } |
| |
| /** @return {!MojoHandle} */ |
| releasePipe() { |
| console.assert(this.isPrimary(), 'secondary endpoint cannot release pipe'); |
| return this.router_.pipe; |
| } |
| |
| /** @return {boolean} */ |
| get isPendingAssociation() { |
| return this.localPeer_ !== null; |
| } |
| |
| /** |
| * @param {string} interfaceName |
| * @param {string} scope |
| */ |
| bindInBrowser(interfaceName, scope) { |
| console.assert( |
| this.isPrimary() && !this.router_.isReading(), |
| 'endpoint is either associated or already bound'); |
| Mojo.bindInterface(interfaceName, this.router_.pipe, scope); |
| } |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @return {number} |
| */ |
| associatePeerOfOutgoingEndpoint(endpoint) { |
| console.assert(this.router_, 'cannot associate with unbound endpoint'); |
| const peer = endpoint.localPeer_; |
| endpoint.localPeer_ = peer.localPeer_ = null; |
| |
| const id = this.router_.generateInterfaceId(); |
| peer.router_ = this.router_; |
| peer.interfaceId_ = id; |
| if (peer.client_) { |
| this.router_.addEndpoint(peer, id); |
| } |
| return id; |
| } |
| |
| /** @return {number} */ |
| generateRequestId() { |
| const id = this.nextRequestId_++; |
| if (this.nextRequestId_ > 0xffffffff) { |
| this.nextRequestId_ = 0; |
| } |
| return id; |
| } |
| |
| /** |
| * @param {number} ordinal |
| * @param {number} requestId |
| * @param {number} flags |
| * @param {!mojo.internal.MojomType} paramStruct |
| * @param {!Object} value |
| */ |
| send(ordinal, requestId, flags, paramStruct, value) { |
| const message = new mojo.internal.Message( |
| this, this.interfaceId_, flags, ordinal, requestId, |
| /** @type {!mojo.internal.StructSpec} */ (paramStruct.$.structSpec), |
| value); |
| console.assert( |
| this.router_, 'cannot send message on unassociated unbound endpoint'); |
| this.router_.send(message); |
| } |
| |
| /** @param {mojo.internal.interfaceSupport.EndpointClient} client */ |
| start(client) { |
| console.assert(!this.client_, 'endpoint already started'); |
| this.client_ = client; |
| if (this.router_) { |
| this.router_.addEndpoint(this, this.interfaceId_); |
| } |
| } |
| |
| /** @return {boolean} */ |
| get isStarted() { |
| return this.client_ !== null; |
| } |
| |
| stop() { |
| if (this.router_) { |
| this.router_.removeEndpoint(this.interfaceId_); |
| } |
| this.client_ = null; |
| this.controlMessageHandler_ = null; |
| } |
| |
| close() { |
| if (this.router_) { |
| this.router.closeEndpoint(this.interfaceId_); |
| } |
| this.client_ = null; |
| this.controlMessageHandler_ = null; |
| } |
| |
| async flushForTesting() { |
| return this.controlMessageHandler_.sendRunMessage({'flushForTesting': {}}); |
| } |
| |
| /** |
| * @param {!mojo.internal.MessageHeader} header |
| * @param {!ArrayBuffer} buffer |
| * @param {!Array<MojoHandle>} handles |
| */ |
| onMessageReceived(header, buffer, handles) { |
| console.assert(this.client_, 'endpoint has no client'); |
| const handled = |
| this.controlMessageHandler_.maybeHandleControlMessage(header, buffer); |
| if (handled) { |
| return; |
| } |
| |
| this.client_.onMessageReceived(this, header, buffer, handles); |
| } |
| |
| onError() { |
| if (this.client_) { |
| this.client_.onError(this); |
| } |
| } |
| }; |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {!ArrayBuffer} buffer |
| * @export |
| */ |
| mojo.internal.interfaceSupport.acceptBufferForTesting = function( |
| endpoint, buffer) { |
| endpoint.router_.onMessageReceived_(buffer, []); |
| }; |
| |
| /** |
| * Creates a new Endpoint wrapping a given pipe handle. |
| * |
| * @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} |
| * pipeOrEndpoint |
| * @param {boolean=} setNamespaceBit |
| * @return {!mojo.internal.interfaceSupport.Endpoint} |
| */ |
| mojo.internal.interfaceSupport.createEndpoint = function( |
| pipeOrEndpoint, setNamespaceBit = false) { |
| // `watch` is defined on MojoHandle but not Endpoint, so if it is not defined |
| // we know this is an Endpoint. |
| if (pipeOrEndpoint.watch === undefined) { |
| return /** @type {!mojo.internal.interfaceSupport.Endpoint} */( |
| pipeOrEndpoint); |
| } |
| return new mojo.internal.interfaceSupport.Endpoint( |
| new mojo.internal.interfaceSupport.Router( |
| /** @type {!MojoHandle} */(pipeOrEndpoint), setNamespaceBit), |
| 0); |
| }; |
| |
| /** |
| * Returns its input if given an existing Endpoint. If given a pipe handle, |
| * creates a new Endpoint to own it and returns that. This is a helper for |
| * generated PendingReceiver constructors since they can accept either type as |
| * input. |
| * |
| * @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} handle |
| * @return {!mojo.internal.interfaceSupport.Endpoint} |
| * @export |
| */ |
| mojo.internal.interfaceSupport.getEndpointForReceiver = function(handle) { |
| return mojo.internal.interfaceSupport.createEndpoint(handle); |
| }; |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {string} interfaceName |
| * @param {string} scope |
| * @export |
| */ |
| mojo.internal.interfaceSupport.bind = function(endpoint, interfaceName, scope) { |
| endpoint.bindInBrowser(interfaceName, scope); |
| }; |
| |
| mojo.internal.interfaceSupport.PipeControlMessageHandler = class { |
| /** |
| * @param {!mojo.internal.interfaceSupport.Router} router |
| * @param {function(number)} onDisconnect |
| */ |
| constructor(router, onDisconnect) { |
| /** @const {!mojo.internal.interfaceSupport.Router} */ |
| this.router_ = router; |
| |
| /** @const {function(number)} */ |
| this.onDisconnect_ = onDisconnect; |
| } |
| |
| /** |
| * @param {!mojo.pipeControl.RunOrClosePipeInput} input |
| */ |
| send(input) { |
| const message = new mojo.internal.Message( |
| null, 0xffffffff, 0, mojo.pipeControl.RUN_OR_CLOSE_PIPE_MESSAGE_ID, 0, |
| /** @type {!mojo.internal.StructSpec} */ |
| (mojo.pipeControl.RunOrClosePipeMessageParamsSpec.$.$.structSpec), |
| {'input': input}); |
| this.router_.send(message); |
| } |
| |
| /** |
| * @param {!mojo.internal.MessageHeader} header |
| * @param {!ArrayBuffer} buffer |
| * @return {boolean} |
| */ |
| maybeHandleMessage(header, buffer) { |
| if (header.ordinal !== mojo.pipeControl.RUN_OR_CLOSE_PIPE_MESSAGE_ID) { |
| return false; |
| } |
| |
| const data = new DataView(buffer, header.headerSize); |
| const decoder = new mojo.internal.Decoder(data, []); |
| const spec = /** @type {!mojo.internal.StructSpec} */ ( |
| mojo.pipeControl.RunOrClosePipeMessageParamsSpec.$.$.structSpec); |
| const input = decoder.decodeStructInline(spec)['input']; |
| if (input.hasOwnProperty('peerAssociatedEndpointClosedEvent')) { |
| this.onDisconnect_(input['peerAssociatedEndpointClosedEvent']['id']); |
| return true; |
| } |
| |
| return true; |
| } |
| |
| /**@param {number} interfaceId */ |
| notifyEndpointClosed(interfaceId) { |
| this.send({'peerAssociatedEndpointClosedEvent': {'id': interfaceId}}); |
| } |
| }; |
| |
| /** |
| * Handles incoming interface control messages on an interface endpoint. |
| */ |
| mojo.internal.interfaceSupport.ControlMessageHandler = class { |
| /** @param {!mojo.internal.interfaceSupport.Endpoint} endpoint */ |
| constructor(endpoint) { |
| /** @private {!mojo.internal.interfaceSupport.Endpoint} */ |
| this.endpoint_ = endpoint; |
| |
| /** @private {!Map<number, function()>} */ |
| this.pendingFlushResolvers_ = new Map; |
| } |
| |
| sendRunMessage(input) { |
| const requestId = this.endpoint_.generateRequestId(); |
| return new Promise(resolve => { |
| this.endpoint_.send( |
| mojo.interfaceControl.RUN_MESSAGE_ID, requestId, |
| mojo.internal.kMessageFlagExpectsResponse, |
| mojo.interfaceControl.RunMessageParamsSpec.$, {'input': input}); |
| this.pendingFlushResolvers_.set(requestId, resolve); |
| }); |
| } |
| |
| maybeHandleControlMessage(header, buffer) { |
| if (header.ordinal === mojo.interfaceControl.RUN_MESSAGE_ID) { |
| const data = new DataView(buffer, header.headerSize); |
| const decoder = new mojo.internal.Decoder(data, []); |
| if (header.flags & mojo.internal.kMessageFlagExpectsResponse) |
| return this.handleRunRequest_(header.requestId, decoder); |
| else |
| return this.handleRunResponse_(header.requestId, decoder); |
| } |
| |
| return false; |
| } |
| |
| handleRunRequest_(requestId, decoder) { |
| const input = decoder.decodeStructInline( |
| mojo.interfaceControl.RunMessageParamsSpec.$.$.structSpec)['input']; |
| if (input.hasOwnProperty('flushForTesting')) { |
| this.endpoint_.send( |
| mojo.interfaceControl.RUN_MESSAGE_ID, requestId, |
| mojo.internal.kMessageFlagIsResponse, |
| mojo.interfaceControl.RunResponseMessageParamsSpec.$, |
| {'output': null}); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| handleRunResponse_(requestId, decoder) { |
| const resolver = this.pendingFlushResolvers_.get(requestId); |
| if (!resolver) |
| return false; |
| |
| resolver(); |
| return true; |
| } |
| }; |
| |
| /** |
| * Captures metadata about a request which was sent by a remote, for which a |
| * response is expected. |
| * |
| * @typedef {{ |
| * requestId: number, |
| * ordinal: number, |
| * responseStruct: !mojo.internal.MojomType, |
| * resolve: !Function, |
| * reject: !Function, |
| * useResultResponse: boolean, |
| * }} |
| */ |
| mojo.internal.interfaceSupport.PendingResponse; |
| |
| /** |
| * Exposed by endpoints to allow observation of remote peer closure. Any number |
| * of listeners may be registered on a ConnectionErrorEventRouter, and the |
| * router will dispatch at most one event in its lifetime, whenever its endpoint |
| * detects peer closure. |
| * @export |
| */ |
| mojo.internal.interfaceSupport.ConnectionErrorEventRouter = class { |
| /** @public */ |
| constructor() { |
| /** @type {!Map<number, !Function>} */ |
| this.listeners = new Map; |
| |
| /** @private {number} */ |
| this.nextListenerId_ = 0; |
| } |
| |
| /** |
| * @param {!Function} listener |
| * @return {number} An ID which can be given to removeListener() to remove |
| * this listener. |
| * @export |
| */ |
| addListener(listener) { |
| const id = ++this.nextListenerId_; |
| this.listeners.set(id, listener); |
| return id; |
| } |
| |
| /** |
| * @param {number} id An ID returned by a prior call to addListener. |
| * @return {boolean} True iff the identified listener was found and removed. |
| * @export |
| */ |
| removeListener(id) { |
| return this.listeners.delete(id); |
| } |
| |
| /** |
| * Notifies all listeners of a connection error. |
| */ |
| dispatchErrorEvent() { |
| for (const listener of this.listeners.values()) |
| listener(); |
| } |
| }; |
| |
| /** |
| * @interface |
| * @export |
| */ |
| mojo.internal.interfaceSupport.PendingReceiver = class { |
| /** |
| * @return {!mojo.internal.interfaceSupport.Endpoint} |
| * @export |
| */ |
| get handle() {} |
| }; |
| |
| /** |
| * Generic helper used to implement all generated remote classes. Knows how to |
| * serialize requests and deserialize their replies, both according to |
| * declarative message structure specs. |
| * |
| * TODO(crbug.com/40102194): Use a bounded generic type instead of |
| * mojo.internal.interfaceSupport.PendingReceiver. |
| * @implements {mojo.internal.interfaceSupport.EndpointClient} |
| * @export |
| */ |
| mojo.internal.interfaceSupport.InterfaceRemoteBase = class { |
| /** |
| * @param {!function(new:mojo.internal.interfaceSupport.PendingReceiver, |
| * !mojo.internal.interfaceSupport.Endpoint)} requestType |
| * @param {MojoHandle|mojo.internal.interfaceSupport.Endpoint=} handle |
| * The pipe or endpoint handle to use as a remote endpoint. If omitted, |
| * this object must be bound with bindHandle before it can be used to send |
| * messages. |
| * @public |
| */ |
| constructor(requestType, handle = undefined) { |
| /** @private {mojo.internal.interfaceSupport.Endpoint} */ |
| this.endpoint_ = null; |
| |
| /** |
| * @private {!function(new:mojo.internal.interfaceSupport.PendingReceiver, |
| * !mojo.internal.interfaceSupport.Endpoint)} |
| */ |
| this.requestType_ = requestType; |
| |
| /** |
| * @private {!Map<number, !mojo.internal.interfaceSupport.PendingResponse>} |
| */ |
| this.pendingResponses_ = new Map; |
| |
| /** @const {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter} */ |
| this.connectionErrorEventRouter_ = |
| new mojo.internal.interfaceSupport.ConnectionErrorEventRouter; |
| |
| if (handle) { |
| this.bindHandle(handle); |
| } |
| } |
| |
| /** @return {mojo.internal.interfaceSupport.Endpoint} */ |
| get endpoint() { |
| return this.endpoint_; |
| } |
| |
| /** |
| * @return {!mojo.internal.interfaceSupport.PendingReceiver} |
| */ |
| bindNewPipeAndPassReceiver() { |
| let {handle0, handle1} = Mojo.createMessagePipe(); |
| this.bindHandle(handle0); |
| return new this.requestType_( |
| mojo.internal.interfaceSupport.createEndpoint(handle1)); |
| } |
| |
| /** |
| * @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} handle |
| * @export |
| */ |
| bindHandle(handle) { |
| console.assert(!this.endpoint_, 'already bound'); |
| handle = mojo.internal.interfaceSupport.createEndpoint( |
| handle, /* setNamespaceBit */ true); |
| this.endpoint_ = handle; |
| this.endpoint_.start(this); |
| this.pendingResponses_ = new Map; |
| } |
| |
| /** @export */ |
| associateAndPassReceiver() { |
| console.assert(!this.endpoint_, 'cannot associate when already bound'); |
| const {endpoint0, endpoint1} = |
| mojo.internal.interfaceSupport.Endpoint.createAssociatedPair(); |
| this.bindHandle(endpoint0); |
| return new this.requestType_(endpoint1); |
| } |
| |
| /** |
| * @return {?mojo.internal.interfaceSupport.Endpoint} |
| * @export |
| */ |
| unbind() { |
| if (!this.endpoint_) { |
| return null; |
| } |
| const endpoint = this.endpoint_; |
| this.endpoint_ = null; |
| endpoint.stop(); |
| return endpoint; |
| } |
| |
| /** @export */ |
| close() { |
| this.cleanupAndFlushPendingResponses_('Message pipe closed.'); |
| if (this.endpoint_) { |
| this.endpoint_.close(); |
| } |
| this.endpoint_ = null; |
| } |
| |
| /** |
| * @return {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter} |
| * @export |
| */ |
| getConnectionErrorEventRouter() { |
| return this.connectionErrorEventRouter_; |
| } |
| |
| /** |
| * @param {number} ordinal |
| * @param {!mojo.internal.MojomType} paramStruct |
| * @param {?mojo.internal.MojomType} maybeResponseStruct |
| * @param {!Array} args |
| * @param {boolean} useResultResponse |
| * @return {!Promise} |
| * @export |
| */ |
| sendMessage( |
| ordinal, paramStruct, maybeResponseStruct, args, useResultResponse) { |
| // The pipe has already been closed, so just drop the message. |
| if (maybeResponseStruct && (!this.endpoint_ || !this.endpoint_.isStarted)) { |
| return Promise.reject(new Error('The pipe has already been closed.')); |
| } |
| |
| // Turns a functions args into an object where each property corresponds to |
| // an argument. |
| // |
| // Each argument in `args` has a single corresponding field in `fields` |
| // except for optional numerics which map to two fields in `fields`. This |
| // means args' indexes don't exactly match `fields`'s. As we iterate |
| // over the fields we keep track of how many optional numeric args we've |
| // seen to get the right `args` index. |
| const value = {}; |
| let nullableValueKindFields = 0; |
| paramStruct.$.structSpec.fields.forEach((field, index) => { |
| const fieldArgsIndex = index - nullableValueKindFields; |
| if (!mojo.internal.isNullableValueKindField(field)) { |
| value[field.name] = args[fieldArgsIndex]; |
| return; |
| } |
| |
| const props = field.nullableValueKindProperties; |
| if (props.isPrimary) { |
| nullableValueKindFields++; |
| value[props.originalFieldName] = args[fieldArgsIndex]; |
| } |
| }); |
| |
| const requestId = this.endpoint_.generateRequestId(); |
| this.endpoint_.send( |
| ordinal, requestId, |
| maybeResponseStruct ? mojo.internal.kMessageFlagExpectsResponse : 0, |
| paramStruct, value); |
| if (!maybeResponseStruct) { |
| return Promise.resolve(); |
| } |
| |
| const responseStruct = |
| /** @type {!mojo.internal.MojomType} */ (maybeResponseStruct); |
| return new Promise((resolve, reject) => { |
| this.pendingResponses_.set(requestId, { |
| requestId, |
| ordinal, |
| responseStruct, |
| resolve, |
| reject, |
| useResultResponse |
| }); |
| }); |
| } |
| |
| /** |
| * @return {!Promise} |
| * @export |
| */ |
| flushForTesting() { |
| return this.endpoint_.flushForTesting(); |
| } |
| |
| /** @override */ |
| onMessageReceived(endpoint, header, buffer, handles) { |
| if (!(header.flags & mojo.internal.kMessageFlagIsResponse) || |
| header.flags & mojo.internal.kMessageFlagExpectsResponse) { |
| return this.onError(endpoint, 'Received unexpected request message'); |
| } |
| const pendingResponse = this.pendingResponses_.get(header.requestId); |
| this.pendingResponses_.delete(header.requestId); |
| if (!pendingResponse) |
| return this.onError(endpoint, 'Received unexpected response message'); |
| const decoder = new mojo.internal.Decoder( |
| new DataView(buffer, header.headerSize), handles, {endpoint}); |
| const responseValue = decoder.decodeStructInline( |
| /** @type {!mojo.internal.StructSpec} */ ( |
| pendingResponse.responseStruct.$.structSpec)); |
| if (!responseValue) |
| return this.onError(endpoint, 'Received malformed response message'); |
| if (header.ordinal !== pendingResponse.ordinal) |
| return this.onError(endpoint, 'Received malformed response message'); |
| |
| if (pendingResponse.useResultResponse) { |
| // Must use property access below to avoid closure name mangling. |
| const result = responseValue['result']; |
| if (result['success'] !== undefined) { |
| pendingResponse.resolve(result['success']); |
| } else { |
| pendingResponse.reject(result['failure']); |
| } |
| } else { |
| pendingResponse.resolve(responseValue); |
| } |
| } |
| |
| /** @override */ |
| onError(endpoint, reason = undefined) { |
| this.cleanupAndFlushPendingResponses_(reason); |
| this.connectionErrorEventRouter_.dispatchErrorEvent(); |
| } |
| |
| /** |
| * @param {string=} reason |
| * @private |
| */ |
| cleanupAndFlushPendingResponses_(reason = undefined) { |
| if (this.endpoint_) { |
| this.endpoint_.stop(); |
| } |
| for (const id of this.pendingResponses_.keys()) { |
| this.pendingResponses_.get(id).reject(new Error(reason)); |
| } |
| this.pendingResponses_ = new Map; |
| } |
| }; |
| |
| /** |
| * Wrapper around mojo.internal.interfaceSupport.InterfaceRemoteBase that |
| * exposes the subset of InterfaceRemoteBase's method that users are allowed |
| * to use. |
| * @template T |
| * @export |
| */ |
| mojo.internal.interfaceSupport.InterfaceRemoteBaseWrapper = class { |
| /** |
| * @param {!mojo.internal.interfaceSupport.InterfaceRemoteBase<T>} remote |
| * @public |
| */ |
| constructor(remote) { |
| /** @private {!mojo.internal.interfaceSupport.InterfaceRemoteBase<T>} */ |
| this.remote_ = remote; |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| bindNewPipeAndPassReceiver() { |
| return this.remote_.bindNewPipeAndPassReceiver(); |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| associateAndPassReceiver() { |
| return this.remote_.associateAndPassReceiver(); |
| } |
| |
| /** |
| * @return {boolean} |
| * @export |
| */ |
| isBound() { |
| return this.remote_.endpoint_ !== null; |
| } |
| |
| /** @export */ |
| close() { |
| this.remote_.close(); |
| } |
| |
| /** |
| * @return {!Promise} |
| * @export |
| */ |
| flushForTesting() { |
| return this.remote_.flushForTesting(); |
| } |
| } |
| |
| /** |
| * Helper used by generated EventRouter types to dispatch incoming interface |
| * messages as Event-like things. |
| * @export |
| */ |
| mojo.internal.interfaceSupport.CallbackRouter = class { |
| constructor() { |
| /** @type {!Map<number, !Function>} */ |
| this.removeCallbacks = new Map; |
| |
| /** @private {number} */ |
| this.nextListenerId_ = 0; |
| } |
| |
| /** @return {number} */ |
| getNextId() { |
| return ++this.nextListenerId_; |
| } |
| |
| /** |
| * @param {number} id An ID returned by a prior call to addListener. |
| * @return {boolean} True iff the identified listener was found and removed. |
| * @export |
| */ |
| removeListener(id) { |
| this.removeCallbacks.get(id)(); |
| return this.removeCallbacks.delete(id); |
| } |
| }; |
| |
| /** |
| * Helper used by generated CallbackRouter types to dispatch incoming interface |
| * messages to listeners. |
| * @export |
| */ |
| mojo.internal.interfaceSupport.InterfaceCallbackReceiver = class { |
| /** |
| * @public |
| * @param {!mojo.internal.interfaceSupport.CallbackRouter} callbackRouter |
| */ |
| constructor(callbackRouter) { |
| /** @private {!Map<number, !Function>} */ |
| this.listeners_ = new Map; |
| |
| /** @private {!mojo.internal.interfaceSupport.CallbackRouter} */ |
| this.callbackRouter_ = callbackRouter; |
| } |
| |
| /** |
| * @param {!Function} listener |
| * @return {number} A unique ID for the added listener. |
| * @export |
| */ |
| addListener(listener) { |
| const id = this.callbackRouter_.getNextId(); |
| this.listeners_.set(id, listener); |
| this.callbackRouter_.removeCallbacks.set(id, () => { |
| return this.listeners_.delete(id); |
| }); |
| return id; |
| } |
| |
| /** |
| * @param {boolean} expectsResponse |
| * @return {!Function} |
| * @export |
| */ |
| createReceiverHandler(expectsResponse) { |
| if (expectsResponse) |
| return this.dispatchWithResponse_.bind(this); |
| return this.dispatch_.bind(this); |
| } |
| |
| /** |
| * @param {...*} varArgs |
| * @private |
| */ |
| dispatch_(varArgs) { |
| const args = Array.from(arguments); |
| this.listeners_.forEach(listener => listener.apply(null, args)); |
| } |
| |
| /** |
| * @param {...*} varArgs |
| * @return {?Object} |
| * @private |
| */ |
| dispatchWithResponse_(varArgs) { |
| const args = Array.from(arguments); |
| const returnValues = Array.from(this.listeners_.values()) |
| .map(listener => listener.apply(null, args)); |
| |
| let returnValue; |
| for (const value of returnValues) { |
| if (value === undefined) |
| continue; |
| if (returnValue !== undefined) |
| throw new Error('Multiple listeners attempted to reply to a message'); |
| returnValue = value; |
| } |
| |
| return returnValue; |
| } |
| }; |
| |
| /** |
| * Wraps message handlers attached to an InterfaceReceiver. |
| * |
| * @typedef {{ |
| * paramStruct: !mojo.internal.MojomType, |
| * responseStruct: ?mojo.internal.MojomType, |
| * handler: !Function, |
| * useResultResponse: boolean, |
| * }} |
| */ |
| mojo.internal.interfaceSupport.MessageHandler; |
| |
| /** |
| * Generic helper that listens for incoming request messages on one or more |
| * endpoints of the same interface type, dispatching them to registered |
| * handlers. Handlers are registered against a specific ordinal message number. |
| * |
| * @template T |
| * @implements {mojo.internal.interfaceSupport.EndpointClient} |
| * @export |
| */ |
| mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal = class { |
| /** |
| * @param {!function(new:T, |
| * (!MojoHandle|!mojo.internal.interfaceSupport.Endpoint)=)} remoteType |
| * @public |
| */ |
| constructor(remoteType) { |
| /** @private {!Set<!mojo.internal.interfaceSupport.Endpoint>} endpoints */ |
| this.endpoints_ = new Set(); |
| |
| /** |
| * @private {!function(new:T, |
| * (!MojoHandle|!mojo.internal.interfaceSupport.Endpoint)=)} |
| */ |
| this.remoteType_ = remoteType; |
| |
| /** |
| * @private {!Map<number, !mojo.internal.interfaceSupport.MessageHandler>} |
| */ |
| this.messageHandlers_ = new Map; |
| |
| /** @const {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter} */ |
| this.connectionErrorEventRouter_ = |
| new mojo.internal.interfaceSupport.ConnectionErrorEventRouter; |
| } |
| |
| /** |
| * @param {number} ordinal |
| * @param {!mojo.internal.MojomType} paramStruct |
| * @param {?mojo.internal.MojomType} responseStruct |
| * @param {!Function} handler |
| * @param {boolean} useResultResponse |
| * @export |
| */ |
| registerHandler( |
| ordinal, paramStruct, responseStruct, handler, useResultResponse) { |
| this.messageHandlers_.set( |
| ordinal, {paramStruct, responseStruct, handler, useResultResponse}); |
| } |
| |
| /** |
| * @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} handle |
| * @export |
| */ |
| bindHandle(handle) { |
| handle = mojo.internal.interfaceSupport.createEndpoint(handle); |
| this.endpoints_.add(handle); |
| handle.start(this); |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| bindNewPipeAndPassRemote() { |
| let remote = new this.remoteType_(); |
| this.bindHandle(remote.$.bindNewPipeAndPassReceiver().handle); |
| return remote; |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| associateAndPassRemote() { |
| const {endpoint0, endpoint1} = |
| mojo.internal.interfaceSupport.Endpoint.createAssociatedPair(); |
| this.bindHandle(endpoint0); |
| return new this.remoteType_(endpoint1); |
| } |
| |
| /** @export */ |
| closeBindings() { |
| for (const endpoint of this.endpoints_) { |
| endpoint.close(); |
| } |
| this.endpoints_.clear(); |
| } |
| |
| /** |
| * @return {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter} |
| * @export |
| */ |
| getConnectionErrorEventRouter() { |
| return this.connectionErrorEventRouter_; |
| } |
| |
| /** |
| * @return {!Promise} |
| * @export |
| */ |
| async flush() { |
| for (let endpoint of this.endpoints_) { |
| await endpoint.flushForTesting(); |
| } |
| } |
| |
| /** @override */ |
| onMessageReceived(endpoint, header, buffer, handles) { |
| if (header.flags & mojo.internal.kMessageFlagIsResponse) |
| throw new Error('Received unexpected response on interface receiver'); |
| const handler = this.messageHandlers_.get(header.ordinal); |
| if (!handler) |
| throw new Error('Received unknown message'); |
| const decoder = new mojo.internal.Decoder( |
| new DataView(buffer, header.headerSize), handles, {endpoint}); |
| const request = decoder.decodeStructInline( |
| /** @type {!mojo.internal.StructSpec} */ ( |
| handler.paramStruct.$.structSpec)); |
| if (!request) |
| throw new Error('Received malformed message'); |
| |
| // Each field in `handler.paramStruct.$.structSpec.fields` corresponds to |
| // an argument, except for optional numerics where two fields correspond to |
| // a single argument. |
| const args = []; |
| for (const field of handler.paramStruct.$.structSpec.fields) { |
| if (!mojo.internal.isNullableValueKindField(field)) { |
| args.push(request[field.name]); |
| continue; |
| } |
| |
| const props = field.nullableValueKindProperties; |
| if (!props.isPrimary) { |
| continue; |
| } |
| args.push(request[props.originalFieldName]); |
| } |
| |
| if (handler.useResultResponse) { |
| this.handleResultResponseMessage_(endpoint, header, handler, args); |
| } else { |
| this.handleResponseMessage_(endpoint, header, handler, args); |
| } |
| } |
| |
| /** |
| * Handles result response messages. 'result' need more handling because there |
| * are more semantics around its type that are more consistent with js |
| * promise. |
| * |
| * Successful "handling" of the incoming message causes in a mojo message |
| * that signals a successful response with the associated value. A failure in |
| * the message handling would signal failure and propagate the error (if |
| * possible). |
| * |
| * Note that there is still a chance for irrecoverable error here if the type |
| * conversion cannot be successfully completed. The success path is mostly |
| * immune to this because of strong typing, but the error path is vulnerable |
| * to this type of runtime error. To avoid this, use the JsError mojo type for |
| * errors originating from javascript. |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {!mojo.internal.MessageHeader} header |
| * @param {!mojo.internal.interfaceSupport.MessageHandler} handler |
| * @param {!Array<*>} args |
| * @private |
| */ |
| handleResultResponseMessage_(endpoint, header, handler, args) { |
| try { |
| let result = handler.handler.apply(null, args); |
| |
| if (typeof result != 'object' || result.constructor.name != 'Promise') { |
| result = Promise.resolve(result); |
| } |
| |
| result |
| .then(value => { |
| endpoint.send( |
| header.ordinal, header.requestId, |
| mojo.internal.kMessageFlagIsResponse, |
| /** @type {!mojo.internal.MojomType} */ |
| (handler.responseStruct), {'result': {'success': value}}); |
| }) |
| .catch(error => { |
| endpoint.send( |
| header.ordinal, header.requestId, |
| mojo.internal.kMessageFlagIsResponse, |
| /** @type {!mojo.internal.MojomType} */ |
| (handler.responseStruct), {'result': {'failure': error}}); |
| }); |
| } catch (error) { |
| endpoint.send( |
| header.ordinal, header.requestId, |
| mojo.internal.kMessageFlagIsResponse, |
| /** @type {!mojo.internal.MojomType} */ |
| (handler.responseStruct), {'result': {'failure': error}}); |
| } |
| } |
| |
| /** |
| * @param {!mojo.internal.interfaceSupport.Endpoint} endpoint |
| * @param {!mojo.internal.MessageHeader} header |
| * @param {!mojo.internal.interfaceSupport.MessageHandler} handler |
| * @param {!Array<*>} args |
| * @private |
| */ |
| handleResponseMessage_(endpoint, header, handler, args) { |
| let result = handler.handler.apply(null, args); |
| |
| // If the message expects a response, the handler must return either a |
| // well-formed response object, or a Promise that will eventually yield one. |
| if (handler.responseStruct) { |
| if (result === undefined) { |
| this.onError(endpoint); |
| throw new Error( |
| 'Message expects a reply but its handler did not provide one.'); |
| } |
| |
| if (typeof result != 'object' || result.constructor.name != 'Promise') { |
| result = Promise.resolve(result); |
| } |
| |
| result |
| .then(value => { |
| endpoint.send( |
| header.ordinal, header.requestId, |
| mojo.internal.kMessageFlagIsResponse, |
| /** @type {!mojo.internal.MojomType} */ |
| (handler.responseStruct), value); |
| }) |
| .catch(() => { |
| // If the handler rejects, that means it didn't like the request's |
| // contents for whatever reason. We close the binding to prevent |
| // further messages from being received from that client. |
| this.onError(endpoint); |
| }); |
| } |
| } |
| |
| /** @override */ |
| onError(endpoint, reason = undefined) { |
| this.endpoints_.delete(endpoint); |
| endpoint.close(); |
| this.connectionErrorEventRouter_.dispatchErrorEvent(); |
| } |
| }; |
| |
| /** |
| * Generic helper used to perform operations related to the interface pipe e.g. |
| * bind the pipe, close it, flush it for testing, etc. Wraps |
| * mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal and exposes a |
| * subset of methods that meant to be used by users of a receiver class. |
| * |
| * @template T |
| * @export |
| */ |
| mojo.internal.interfaceSupport.InterfaceReceiverHelper = class { |
| /** |
| * @param {!mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal<T>} |
| * helper_internal |
| * @public |
| */ |
| constructor(helper_internal) { |
| /** |
| * @private {!mojo.internal.interfaceSupport.InterfaceReceiverHelperInternal<T>} |
| */ |
| this.helper_internal_ = helper_internal; |
| } |
| |
| /** |
| * Binds a new handle to this object. Messages which arrive on the handle will |
| * be read and dispatched to this object. |
| * |
| * @param {!MojoHandle|!mojo.internal.interfaceSupport.Endpoint} handle |
| * @export |
| */ |
| bindHandle(handle) { |
| this.helper_internal_.bindHandle(handle); |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| bindNewPipeAndPassRemote() { |
| return this.helper_internal_.bindNewPipeAndPassRemote(); |
| } |
| |
| /** |
| * @return {!T} |
| * @export |
| */ |
| associateAndPassRemote() { |
| return this.helper_internal_.associateAndPassRemote(); |
| } |
| |
| /** @export */ |
| close() { |
| this.helper_internal_.closeBindings(); |
| } |
| |
| /** |
| * @return {!Promise} |
| * @export |
| */ |
| flush() { |
| return this.helper_internal_.flush(); |
| } |
| } |
| |
| /** |
| * Watches a MojoHandle for readability or peer closure, forwarding either event |
| * to one of two callbacks on the reader. Used by both InterfaceRemoteBase and |
| * InterfaceReceiverHelperInternal to watch for incoming messages. |
| */ |
| mojo.internal.interfaceSupport.HandleReader = class { |
| /** |
| * @param {!MojoHandle} handle |
| * @private |
| */ |
| constructor(handle) { |
| /** @private {!MojoHandle} */ |
| this.handle_ = handle; |
| |
| /** @public {?function(!ArrayBuffer, !Array<MojoHandle>)} */ |
| this.onRead = null; |
| |
| /** @public {!Function} */ |
| this.onError = () => {}; |
| |
| /** @public {?MojoWatcher} */ |
| this.watcher_ = null; |
| } |
| |
| isStopped() { |
| return this.watcher_ === null; |
| } |
| |
| start() { |
| this.watcher_ = this.handle_.watch({readable: true}, this.read_.bind(this)); |
| } |
| |
| stop() { |
| if (!this.watcher_) { |
| return; |
| } |
| this.watcher_.cancel(); |
| this.watcher_ = null; |
| } |
| |
| stopAndCloseHandle() { |
| if (this.watcher_) { |
| this.stop(); |
| } |
| this.handle_.close(); |
| } |
| |
| /** @private */ |
| read_(result) { |
| for (;;) { |
| if (!this.watcher_) |
| return; |
| |
| const read = this.handle_.readMessage(); |
| |
| // No messages available. |
| if (read.result == Mojo.RESULT_SHOULD_WAIT) |
| return; |
| |
| // Remote endpoint has been closed *and* no messages available. |
| if (read.result == Mojo.RESULT_FAILED_PRECONDITION) { |
| this.onError(); |
| return; |
| } |
| |
| // Something terrible happened. |
| if (read.result != Mojo.RESULT_OK) |
| throw new Error('Unexpected error on HandleReader: ' + read.result); |
| |
| this.onRead(read.buffer, read.handles); |
| } |
| } |
| }; |