blob: 95ead6814eb723184a79451478b2e90d991cbb51 [file] [log] [blame] [edit]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Utilities for making FIDL calls to protocols. Contains both
* classes for interacting with FIDL definitions and for calling out to
* services.
*/
import * as fidl_internal from 'fidl_internal';
(function(global) {
// Classes and functions for managing FIDL IR and libraries
// TODO(jeremymanson): We really only want to look in the /pkg/ directory.
// This involves really turning the shell into a component, which we haven't
// done yet.
const IR_PATH = fidl_internal.irPath;
/**
* Programmatic representation of a library. Contains fidling IR.
*/
class Library {
constructor(ir) {
this.ir = ir;
this.kinds = {
bits: this.ir.bits_declarations,
const : this.ir.const_declarations,
enum: this.ir.enum_declarations,
interface: this.ir.interface_declarations,
service: this.ir.service_declarations,
struct: this.ir.struct_declarations,
table: this.ir.table_declarations,
union: this.ir.union_declarations,
xunion: this.ir.xunion_declarations,
type_alias: this.ir.type_alias_declarations,
}
}
/**
* Given a particular protocol name (as in FIDL IR), return the IR in this
* library that represents that protocol.
*/
lookupProtocol(name) {
const decl = this._findDeclaration(this.ir.interface_declarations, name);
if (decl === null) {
throw new Error(`No such protocol ${name}`);
}
return decl;
}
/**
* Given a particular identifier name (as in FIDL IR), return the IR in this
* library that represents that identifier.
*/
lookupIdentifier(ident) {
const kind = this.ir.declarations[ident];
return [kind, this._findDeclaration(this.kinds[kind], ident)];
}
_findDeclaration(decls, name) {
for (const decl of decls) {
if (decl.name === name) {
return decl;
}
}
return null;
}
};
const libraries = new Map();
// If we ever reassign this, we need to call close() to delete it.
var internalLibrary = null;
/**
* Converts the given string to JSON, converting 64-bit ordinals to
* strings.
*
* @param {String} str the string containing the IR to convert.
*/
function stringToJsonIr(str) {
// Hack: replace 64-bit ordinals with strings. Better support for BigInts would be better.
let regex = /("ordinal"\s*:\s*)([0-9]+)\s*,/gi;
str = str.replace(regex, '$1"$2",');
regex = /("generated_ordinal"\s*:\s*)([0-9]+)\s*,/gi;
str = str.replace(regex, '$1"$2",');
return JSON.parse(str);
}
/**
* Loads the IR for a given library.
* libraryName should be of the form library.protocol
*
* Libraries are expected to be in a directory structure such that the filename
* is the library name, with dots replaced by directory separators, ending with
* .fidl.json. This should be arranged by the build system.
*/
function loadIR(libraryName) {
if (internalLibrary == null) {
internalLibrary = fidl_internal.newLibrary();
}
let maybeSeparator = '';
if (IR_PATH[IR_PATH.length - 1] != '/') {
maybeSeparator = '/';
}
const irPath = IR_PATH + maybeSeparator + libraryName.replace(/\./g, '/') + '.fidl.json';
// Load for JavaScript-level operations, like tab-completion and exposing the fidling.
// We should probably move more of this to native code over time, so that we don't have to
// load libraries twice.
try {
const f = std.open(irPath, 'r');
let str = f.readAsString();
f.close();
let ir = stringToJsonIr(str);
libraries.set(ir.name, new Library(ir));
} catch {
throw 'FIDL definition for ' + libraryName + ' not found in ' + irPath;
}
// Load for C++ level operations, like encoding and decoding.
if (!internalLibrary.loadLibrary(irPath)) {
throw 'Internal error: Unable to load libraries for ' + libraryName;
}
}
/**
* Loads the IR for a given library.
* libraryName should be of the form library.protocol
*
* Libraries are expected to be in a directory structure such that the filename
* is the library name, with dots replaced by directory separators, ending with
* .fidl.json. This should be arranged by the build system.
*/
function loadLibrary(libraryName) {
if (!libraries.has(libraryName)) {
loadIR(libraryName);
}
if (!libraries.has(libraryName)) {
throw new Error(`Could not find IR for ${libraryName}`);
} else {
return libraries.get(libraryName);
}
}
/**
* Loads the given string containing FIDL JSON IR as a library.
*/
function loadLibraryIr(irString) {
let ir = stringToJsonIr(irString);
if (!libraries.has(ir.name)) {
libraries.set(ir.name, new Library(ir));
// Load for C++ level operations, like encoding and decoding.
if (internalLibrary == null) {
internalLibrary = fidl_internal.newLibrary();
}
if (!internalLibrary.loadLibraryFromString(ir.name, JSON.stringify(ir))) {
throw 'Internal error: Unable to load libraries for ' + ir.name;
}
}
}
function lookupIdentifier(ident) {
const [lib_name, _] = ident.split('/');
const lib = loadLibrary(lib_name);
return lib.lookupIdentifier(ident);
}
/**
* Returns the value of a given FIDL IR attribute.
* @param {*} decl The IR containing the attribute.
* @param {*} name The name of the attribute to return
*/
function attribute(decl, name) {
const attribs = decl.maybe_attributes;
if (typeof attribs === 'undefined') {
return;
}
for (const attrib of attribs) {
if (attrib.name == name) {
return attrib.value;
}
}
}
/**
* Returns the fidldoc for a given method.
* @param {*} method The IR of the method for which you want help.
*/
function methodHelp(method) {
let help = attribute(method, 'Doc') || '';
function typeHelp(type) {
switch (type.kind) {
case 'primitive':
return type.subtype;
case 'string':
return 'string' + (type.maybe_element_count ? `:${type.maybe_element_count}` : '') +
(type.nullable ? '?' : '');
case 'vector':
return `vector<${typeHelp(type.element_type)}>` +
(type.maybe_element_count ? `:${type.maybe_element_count}` : '') +
(type.nullable ? '?' : '');
case 'array':
return `array<${typeHelp(type.element_type)}>:${type.element_count}` +
(type.nullable ? '?' : '');
case 'identifier':
return type.identifier + (type.nullable ? '?' : '');
case 'request':
return `request<${type.subtype}>` + (type.nullable ? '?' : '');
case 'handle':
return (type.subtype === 'handle' ? 'handle' : `handle<${type.subtype}>`) +
(type.nullable ? '?' : '');
default:
return `UNKNOWN(${JSON.stringify(type)})`;
}
}
function argHelp(arg) {
return `${typeHelp(arg.type)} ${arg.name}`;
}
function argsHelp(args) {
return '(' + args.map(argHelp).join(', ') + ')';
}
help = help + method.name;
if (method.has_request) {
help += argsHelp(method.maybe_request);
}
if (method.has_response) {
help = help + ' -> ' + argsHelp(method.maybe_response);
}
return help + '\n';
}
// Classes and libraries for communicating with FIDL services.
const MSG_MAX_BYTES = 64 * 1024;
/**
* Represents a FIDL message header.
* Given bytes representing a FIDL message, MessageHeader will provide txid and ordinal values.
*/
class MessageHeader {
/**
* @param {ArrayBuffer} bytes The bytes of the message.
*/
constructor(bytes) {
this.buf = bytes;
this.view = new DataView(this.buf);
this.txid = this.view.getUint32(0, true);
this.ordinal = this.view.getBigInt64(8, true);
}
};
/**
* Represents the client side of a protocol.
*
* When you construct an object of this type, methods on the protocol will
* automatically be populated from the protocol definition. You can then call
* them.
*/
class ProtocolClient {
/**
* Constructor for ProtocolClient.
* @param {channel} A zx.Channel object OR an existing protocol to duplicate
* @param {protocol} A string or a fidling object representing the protocol.
*/
constructor(channel, protocol) {
// Not sure if protocolName is a string or a fidling.
if (typeof protocol === 'string' || protocol instanceof String) {
this._protocol_name = protocol;
let [library_name, _] = protocol.split('/');
this._decl = loadLibrary(library_name).lookupProtocol(protocol);
} else {
this._protocol_name = protocol.name;
this._decl = protocol;
}
// Not sure if we are being passed a channel or an existing protocol client to dup.
if (channel instanceof zx.Channel) {
this._impl = new ProtocolClientImpl(this._decl, channel);
} else if ('_impl' in channel) {
this._impl = channel._impl;
} else {
throw 'Illegal argument passed as channel to ProtocolClient constructor';
}
// Populate the ProtocolClient object with methods corresponding to the protocol methods.
const completions = [];
for (const method of this._decl.methods) {
let definedProperty = false;
if (method.has_request) {
definedProperty = true;
this[method.name] = (...args) => this._impl._invokeMethod(method, args);
Object.defineProperty(this[method.name], 'length', {value: method.maybe_request.length});
} else if (method.has_response) {
definedProperty = true;
this[method.name] = (fn) => this._impl._setEventProcessor(method, fn);
}
if (definedProperty) {
let help = methodHelp(method);
if (help) {
Object.defineProperty(this[method.name], Symbol.for('help'), {value: help});
}
completions.push(method.name);
}
}
this[Symbol.for('completions')] = completions;
}
/**
* Closes the given ProtocolClient instance.
*/
static close(protocolClient) {
protocolClient._impl._close();
}
};
// Something fidl_codec should do: when we improve the JSON output for it, this should be
// less necessary.
function parseFidlcatHandle(handleStr) {
var regex = /[A-Z_]*:([0-9a-f]+)(\(.*\))?/gi;
return parseInt(handleStr.replace(regex, '$1'), 16);
}
function convertResponseHandles(methodDecl, message) {
for (let i = 0; i < methodDecl.maybe_response.length; i++) {
if (methodDecl.maybe_response[i].type.kind == 'identifier') {
// TODO: Look up identifier and recurse.
continue;
}
if (methodDecl.maybe_response[i].type.kind == 'request') {
const handleValue = parseFidlcatHandle(message[methodDecl.maybe_response[i].name]);
message[methodDecl.maybe_response[i].name] = zx.Channel.fromValueDeprecated(handleValue);
}
if (methodDecl.maybe_response[i].type.kind == 'handle') {
const handleValue = parseFidlcatHandle(message[methodDecl.maybe_response[i].name]);
switch (methodDecl.maybe_response[i].type.subtype) {
case 'job':
message[methodDecl.maybe_response[i].name] = zx.Job.fromValueDeprecated(handleValue);
break;
case 'handle':
case 'bti':
case 'channel':
case 'debuglog':
case 'event':
case 'eventpair':
case 'exception':
case 'fifo':
case 'guest':
case 'interrupt':
case 'iommu':
case 'pager':
case 'pcidevice':
case 'pmt':
case 'port':
case 'process':
case 'profile':
case 'resource':
case 'socket':
case 'suspendtoken':
case 'thread':
case 'timer':
case 'vcpu':
case 'vmar':
case 'vmo':
throw 'Unsupported handle type ' + methodDecl.maybe_response[i].type.subtype;
default:
throw 'Unknown handle type ' + methodDecl.maybe_response[i].type.subtype;
}
}
}
return message;
}
/**
* The implementation class for the ProtocolClient.
*
* This prevents the messy implementation methods from being exposed to the
* user. Can be shared among multiple ProtocolClient objects (this is useful
* for protocol composition, where you might have multiple protocols exposed on
* the same channel).
*/
class ProtocolClientImpl {
/**
* @param {declaration} A fidling definition for the protocol.
* @param {channel} A zx.Channel object for communication.
*/
constructor(declaration, channel) {
this._decl = declaration;
this._channel = channel;
this._eventProcessors = new Map(); // String representing ordinal => callback on that event.
this._txid = 1;
this._pending = new Map();
}
/**
* Returns the next transaction id for communications over this channel.
*/
_nextTxId() {
const txid = this._txid;
this._txid++;
return txid;
}
/**
* Tells this client what to do when a given event is triggered.
* @param {method} The part of the fidling describing the event.
* @param {fn} The function to be invoked when the event happens.
* @returns a promise that will be resolved / rejected when the event is triggered.
*/
_setEventProcessor(method, fn) {
this._channel.waitAsync(
zx.ZX_CHANNEL_READABLE | zx.ZX_CHANNEL_PEER_CLOSED, () => this._readable(method));
return new Promise((resolve, reject) => {
if (typeof method.generated_ordinal != 'undefined') {
this._eventProcessors.set(method.generated_ordinal, (args) => {
try {
resolve(fn(args));
} catch (e) {
reject(e);
console.log(e);
}
});
}
this._eventProcessors.set(method.ordinal, (args) => {
try {
resolve(fn(args));
} catch (e) {
reject(e);
console.log(e);
}
});
});
}
/**
* The implementation of method invocation. The ProtocolClient methods are a
* thin decorator around this method. If the method takes a response, it will
* set up an event handler to deal with the response.
*/
_invokeMethod(method, args) {
return new Promise((resolve, reject) => {
try {
let txid = 0;
if (method.has_response) {
this._channel.waitAsync(
zx.ZX_CHANNEL_READABLE | zx.ZX_CHANNEL_PEER_CLOSED, () => this._readable(method));
txid = this._nextTxId();
}
const encoded = internalLibrary.encodeRequest(txid, method.ordinal, args);
if (method.has_response) {
this._pending.set(txid, [method, resolve, reject]);
}
this._channel.write(encoded.bytes, encoded.handles);
if (!method.has_response) {
resolve();
} // otherwise, gets resolved when we invoke _readable.
} catch (e) {
console.log(e);
console.log(e.stack);
reject(e);
}
});
}
/**
* Handles the response from a protocol method. It looks up what methods
* we're expecting a response from and tries to correlate it with the txid
* received.
* @param method is the FIDL IR for the method.
*/
_readable(method) {
const [bytes, handles] = this._channel.read();
const header = new MessageHeader(bytes);
let resolve = this._eventProcessors.get(header.ordinal.toString());
if (typeof resolve == 'undefined') {
// It's a response to a request.
const txid = header.txid;
if (!this._pending.has(txid)) {
console.log(`Unexpected transaction id ${txid} from ${this._decl.name}`);
return;
}
const [meth, resolve_, reject] = this._pending.get(txid);
resolve = resolve_;
this._pending.delete(txid);
}
if (typeof resolve == 'undefined') {
throw 'Unexpected message with ordinal ' + header.ordinal + ' received';
}
// TODO(jeremymanson): Make sure strings in decoded response are escaped properly
const responseString = internalLibrary.decodeResponse(bytes, handles);
const response = convertResponseHandles(method, JSON.parse(responseString));
resolve(response);
}
_close() {
this._channel.close();
}
}
/**
* A convenience class that allows a user to say something like:
*
* client = new Request(fidling.fuchsia.io.Directory).getProtocolClient();
*
* And use client appropriately.
*/
class Request {
constructor(protocolIr) {
this._protocolIr = protocolIr;
const channels = zx.Channel.create();
this._serverEndpoint = channels[1];
this._clientEndpoint = channels[0];
}
/**
* Generates a new ProtocolClient for this request
*/
getProtocolClient() {
return new fidl.ProtocolClient(this._clientEndpoint, this._protocolIr.name);
}
/**
* Returns a zx.Channel representing the server endpoint for this request.
*/
getChannelForServer() {
return this._serverEndpoint;
}
};
/**
* Returns a ProtocolClient that connects to the server at the given path. For example:
* client = connectToServiceAt('/svc/fuchsia.power.BatteryManager', 'fuchsia.power/BatteryManager');
*/
function connectToServiceAt(path, protocolName) {
const channel = new zx.Channel(fdio.serviceConnect(path));
return new ProtocolClient(channel, protocolName);
}
/**
* Connects to a service exposed via /svc. For example:
* client = connectToService('fuchsia.power.BatteryManager');
*/
function connectToService(serviceName) {
const idx = serviceName.lastIndexOf('.');
const name = serviceName.substr(idx + 1);
const libraryName = serviceName.substr(0, idx);
return connectToServiceAt(`/svc/${serviceName}`, `${libraryName}/${name}`);
}
global['fidl'] = {
connectToService,
loadLibrary,
loadLibraryIr,
ProtocolClient,
Request,
};
function constToValue(c) {
if (c.type.kind == 'primitive') {
switch (c.type.subtype) {
case 'int8':
case 'uint8':
case 'int16':
case 'uint16':
case 'int32':
case 'uint32':
case 'int64':
case 'uint64':
return parseInt(c.value.literal.value);
case 'float32':
case 'float64':
return parseFloat(c.value.literal.value);
default:
throw 'Unknown primitive type ' + JSON.stringify(c.type) + ' ' + c.type.kind + ' ' +
c.type.subtype;
}
}
}
// Make fidl definitions available on an object called `fidling`
// Asking for fidling.libraryName.CONST returns a const value.
// Asking for fidling.libraryName.Protocol returns the IR for the protocol.
var fidling_handler = {
get: function(obj, prop) {
let ret = undefined;
libraries.forEach(function(val, library, map) {
// mangle library names to be valid JS identifiers
const libraryName = library.replace(/\./g, '_');
if (prop == libraryName) {
ret = new Proxy({_name: library}, {
get: function(library_obj, lib_prop) {
const maybeName = library_obj._name + '/' + lib_prop;
for (let i = 0; i < val.ir.const_declarations.length; i++) {
let c = val.ir.const_declarations[i];
if (c.name == maybeName) {
return constToValue(c);
}
}
for (let i = 0; i < val.ir.interface_declarations.length; i++) {
let p = val.ir.interface_declarations[i];
if (p.name == maybeName) {
return p;
}
}
// And similarly for non-const decls
return undefined;
}
});
}
});
return ret;
}
};
const fidling = new Proxy({}, fidling_handler);
global['fidling'] = fidling;
})(globalThis);