blob: 9e36eb1fbb0519a36cd2be6acd5747de50fa174a [file] [log] [blame]
// Copyright 2018 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.
'use strict';
goog.require('mojo.interfaceControl.kRunMessageId');
goog.require('mojo.interfaceControl.RunResponseMessageParamsSpec');
goog.require('mojo.internal');
goog.provide('mojo.internal.interfaceSupport');
/**
* Handles incoming interface control messages on a proxy or router endpoint.
*/
mojo.internal.interfaceSupport.ControlMessageHandler = class {
/** @param {!MojoHandle} handle */
constructor(handle) {
/** @private {!MojoHandle} */
this.handle_ = handle;
/** @private {!Map<number, function()>} */
this.pendingFlushResolvers_ = new Map;
}
sendRunMessage(requestId, input) {
return new Promise(resolve => {
mojo.internal.serializeAndSendMessage(
this.handle_, mojo.interfaceControl.kRunMessageId, requestId,
mojo.internal.kMessageFlagExpectsResponse,
mojo.interfaceControl.RunMessageParamsSpec.$, {'input': input});
this.pendingFlushResolvers_.set(requestId, resolve);
});
}
maybeHandleControlMessage(header, buffer) {
if (header.ordinal === mojo.interfaceControl.kRunMessageId) {
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')) {
mojo.internal.serializeAndSendMessage(
this.handle_, mojo.interfaceControl.kRunMessageId, 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 local proxy, for which
* a response is expected.
*/
mojo.internal.interfaceSupport.PendingResponse = class {
/**
* @param {number} requestId
* @param {number} ordinal
* @param {!mojo.internal.MojomType} responseStruct
* @param {!Function} resolve
* @param {!Function} reject
* @private
*/
constructor(requestId, ordinal, responseStruct, resolve, reject) {
/** @public {number} */
this.requestId = requestId;
/** @public {number} */
this.ordinal = ordinal;
/** @public {!mojo.internal.MojomType} */
this.responseStruct = responseStruct;
/** @public {!Function} */
this.resolve = resolve;
/** @public {!Function} */
this.reject = reject;
}
};
/**
* 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
* associated bindings 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();
}
};
/**
* Generic helper used to implement all generated proxy classes. Knows how to
* serialize requests and deserialize their replies, both according to
* declarative message structure specs.
* @template T
* @export
*/
mojo.internal.interfaceSupport.InterfaceProxyBase = class {
/**
* @param {!function(new:T, !MojoHandle)} requestType
* @param {MojoHandle=} opt_handle The message pipe handle to use as a proxy
* endpoint. If null, this object must be bound with bindHandle before
* it can be used to send any messages.
* @public
*/
constructor(requestType, opt_handle) {
/** @public {?MojoHandle} */
this.handle = null;
/** @private {!function(new:T, !MojoHandle)} */
this.requestType_ = requestType;
/** @private {?mojo.internal.interfaceSupport.HandleReader} */
this.reader_ = null;
/** @private {number} */
this.nextRequestId_ = 0;
/**
* @private {!Map<number, !mojo.internal.interfaceSupport.PendingResponse>}
*/
this.pendingResponses_ = new Map;
/** @private {mojo.internal.interfaceSupport.ControlMessageHandler} */
this.controlMessageHandler_ = null;
/** @private {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter} */
this.connectionErrorEventRouter_ =
new mojo.internal.interfaceSupport.ConnectionErrorEventRouter;
if (opt_handle instanceof MojoHandle)
this.bindHandle(opt_handle);
}
/**
* @return {!T}
*/
createRequest() {
let {handle0, handle1} = Mojo.createMessagePipe();
this.bindHandle(handle0);
return new this.requestType_(handle1);
}
/**
* @param {!MojoHandle} handle
* @export
*/
bindHandle(handle) {
if (this.handle)
throw new Error('Proxy already bound.');
this.handle = handle;
const reader = new mojo.internal.interfaceSupport.HandleReader(handle);
reader.onRead = this.onMessageReceived_.bind(this);
reader.onError = this.onError_.bind(this);
reader.start();
this.controlMessageHandler_ =
new mojo.internal.interfaceSupport.ControlMessageHandler(handle);
this.reader_ = reader;
this.nextRequestId_ = 0;
this.pendingResponses_ = new Map;
}
/** @export */
unbind() {
if (this.reader_)
this.reader_.stop();
}
/** @export */
close() {
this.cleanupAndFlushPendingResponses_('Message pipe closed.');
}
/**
* @return {!mojo.internal.interfaceSupport.ConnectionErrorEventRouter}
* @export
*/
getConnectionErrorEventRouter() {
return this.connectionErrorEventRouter_;
}
/**
* @param {number} ordinal
* @param {!mojo.internal.MojomType} paramStruct
* @param {?mojo.internal.MojomType} responseStruct
* @param {!Array} args
* @return {!Promise}
* @export
*/
sendMessage(ordinal, paramStruct, responseStruct, args) {
if (!this.handle) {
throw new Error(
'Attempting to use an unbound proxy. Try $.createRequest() first.')
}
// The pipe has already been closed, so just drop the message.
if (!this.reader_ || this.reader_.isStopped())
return Promise.reject(new Error('The pipe has already been closed.'));
const requestId = this.nextRequestId_++;
const value = {};
paramStruct.$.structSpec.fields.forEach(
(field, index) => value[field.name] = args[index]);
mojo.internal.serializeAndSendMessage(
this.handle, ordinal, requestId,
responseStruct ? mojo.internal.kMessageFlagExpectsResponse : 0,
paramStruct, value);
if (!responseStruct)
return Promise.resolve();
return new Promise((resolve, reject) => {
this.pendingResponses_.set(
requestId,
new mojo.internal.interfaceSupport.PendingResponse(
requestId, ordinal,
/** @type {!mojo.internal.MojomType} */ (responseStruct), resolve,
reject));
});
}
/**
* @return {!Promise}
* @export
*/
flushForTesting() {
return this.controlMessageHandler_.sendRunMessage(
this.nextRequestId_++, {'flushForTesting': {}});
}
/**
* @param {!ArrayBuffer} buffer
* @param {!Array<MojoHandle>} handles
* @private
*/
onMessageReceived_(buffer, handles) {
const data = new DataView(buffer);
const header = mojo.internal.deserializeMessageHeader(data);
if (this.controlMessageHandler_.maybeHandleControlMessage(header, buffer))
return;
if (!(header.flags & mojo.internal.kMessageFlagIsResponse) ||
header.flags & mojo.internal.kMessageFlagExpectsResponse) {
return this.onError_('Received unexpected request message');
}
const pendingResponse = this.pendingResponses_.get(header.requestId);
this.pendingResponses_.delete(header.requestId);
if (!pendingResponse)
return this.onError_('Received unexpected response message');
const decoder = new mojo.internal.Decoder(
new DataView(buffer, header.headerSize), handles);
const responseValue = decoder.decodeStructInline(
/** @type {!mojo.internal.StructSpec} */ (
pendingResponse.responseStruct.$.structSpec));
if (!responseValue)
return this.onError_('Received malformed response message');
if (header.ordinal !== pendingResponse.ordinal)
return this.onError_('Received malformed response message');
pendingResponse.resolve(responseValue);
}
/**
* @param {string=} opt_reason
* @private
*/
onError_(opt_reason) {
this.cleanupAndFlushPendingResponses_(opt_reason);
this.connectionErrorEventRouter_.dispatchErrorEvent();
}
/**
* @param {string=} opt_reason
* @private
*/
cleanupAndFlushPendingResponses_(opt_reason) {
this.reader_.stopAndCloseHandle();
this.reader_ = null;
for (const id of this.pendingResponses_.keys())
this.pendingResponses_.get(id).reject(new Error(opt_reason));
this.pendingResponses_ = new Map;
}
};
/**
* Wrapper around mojo.internal.interfaceSupport.InterfaceProxyBase that
* exposes the subset of InterfaceProxyBase's method that users are allowed
* to use.
* @template T
* @export
*/
mojo.internal.interfaceSupport.InterfaceProxyBaseWrapper = class {
/**
* @param {!mojo.internal.interfaceSupport.InterfaceProxyBase<T>} proxy
* @public
*/
constructor(proxy) {
/** @private {!mojo.internal.interfaceSupport.InterfaceProxyBase<T>} */
this.proxy_ = proxy;
}
/**
* @return {!T}
* @export
*/
createRequest() {
return this.proxy_.createRequest();
}
/** @export */
close() {
this.proxy_.close();
}
/**
* @return {!Promise}
* @export
*/
flushForTesting() {
return this.proxy_.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.InterfaceCallbackTarget = 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
*/
createTargetHandler(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 InterfaceTarget.
*/
mojo.internal.interfaceSupport.MessageHandler = class {
/**
* @param {!mojo.internal.MojomType} paramStruct
* @param {?mojo.internal.MojomType} responseStruct
* @param {!Function} handler
* @private
*/
constructor(paramStruct, responseStruct, handler) {
/** @public {!mojo.internal.MojomType} */
this.paramStruct = paramStruct;
/** @public {?mojo.internal.MojomType} */
this.responseStruct = responseStruct;
/** @public {!Function} */
this.handler = handler;
}
};
/**
* Listens for incoming request messages on a message pipe, dispatching them to
* any registered handlers. Handlers are registered against a specific ordinal
* message number.
* @export
*/
mojo.internal.interfaceSupport.InterfaceTarget = class {
/** @public */
constructor() {
/**
* @private {!Map<MojoHandle,
* !mojo.internal.interfaceSupport.HandleReader>}
*/
this.readers_ = new Map;
/**
* @private {!Map<number, !mojo.internal.interfaceSupport.MessageHandler>}
*/
this.messageHandlers_ = new Map;
/** @private {mojo.internal.interfaceSupport.ControlMessageHandler} */
this.controlMessageHandler_ = null;
}
/**
* @param {number} ordinal
* @param {!mojo.internal.MojomType} paramStruct
* @param {?mojo.internal.MojomType} responseStruct
* @param {!Function} handler
* @export
*/
registerHandler(ordinal, paramStruct, responseStruct, handler) {
this.messageHandlers_.set(
ordinal,
new mojo.internal.interfaceSupport.MessageHandler(
paramStruct, responseStruct, handler));
}
/**
* @param {!MojoHandle} handle
* @export
*/
bindHandle(handle) {
const reader = new mojo.internal.interfaceSupport.HandleReader(handle);
this.readers_.set(handle, reader);
reader.onRead = this.onMessageReceived_.bind(this, handle);
reader.onError = this.onError_.bind(this, handle);
reader.start();
this.controlMessageHandler_ =
new mojo.internal.interfaceSupport.ControlMessageHandler(handle);
}
/** @export */
closeBindings() {
for (const reader of this.readers_.values())
reader.stopAndCloseHandle();
this.readers_.clear();
}
/**
* @param {!MojoHandle} handle
* @param {!ArrayBuffer} buffer
* @param {!Array<MojoHandle>} handles
* @private
*/
onMessageReceived_(handle, buffer, handles) {
const data = new DataView(buffer);
const header = mojo.internal.deserializeMessageHeader(data);
if (this.controlMessageHandler_.maybeHandleControlMessage(header, buffer))
return;
if (header.flags & mojo.internal.kMessageFlagIsResponse)
throw new Error('Received unexpected response on interface target');
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);
const request = decoder.decodeStructInline(
/** @type {!mojo.internal.StructSpec} */ (
handler.paramStruct.$.structSpec));
if (!request)
throw new Error('Received malformed message');
let result = handler.handler.apply(
null,
handler.paramStruct.$.structSpec.fields.map(
field => request[field.name]));
// 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_(handle);
throw new Error(
'Message expects a reply but its handler did not provide one.');
}
if (!(result instanceof Promise))
result = Promise.resolve(result);
result
.then(value => {
mojo.internal.serializeAndSendMessage(
handle, 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_(handle);
});
}
}
/**
* @param {!MojoHandle} handle
* @private
*/
onError_(handle) {
const reader = this.readers_.get(handle);
if (!reader)
return;
reader.stopAndCloseHandle();
this.readers_.delete(handle);
}
};
/**
* Watches a MojoHandle for readability or peer closure, forwarding either event
* to one of two callbacks on the reader. Used by both InterfaceProxyBase and
* InterfaceTarget 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() {
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);
}
}
};