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