| // 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); |