| // Copyright 2015 The Vanadium 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 Server allows creation of services that can be invoked |
| * remotely via RPCs. |
| * |
| * Usage: |
| * var videoService = { |
| * play: { |
| * // Play video |
| * } |
| * }; |
| * |
| * var s = new server(proxyConnection); |
| * s.serve('mymedia/video', videoService); |
| * @private |
| */ |
| |
| var Deferred = require('./../lib/deferred'); |
| var Promise = require('./../lib/promise'); |
| var asyncCall = require('../lib/async-call'); |
| var InspectableFunction = require('../lib/inspectable-function'); |
| var leafDispatcher = require('./leaf-dispatcher'); |
| var vlog = require('./../lib/vlog'); |
| var inspector = require('./../lib/arg-inspector'); |
| var Invoker = require('./../invocation/invoker'); |
| var defaultAuthorizer = require('../security/default-authorizer'); |
| var actions = require('./../verror/actions'); |
| var makeError = require('../verror/make-errors'); |
| var ServerOption = require('./server-option'); |
| |
| var nextServerID = 1; // The ID for the next server. |
| |
| /** |
| * @summary |
| * Server defines the interface for managing a collection of services. |
| * @description |
| * <p>Private Constructor, use |
| * [Runtime#newServer]{@link module:vanadium~Runtime#newServer}.</p> |
| * @inner |
| * @constructor |
| * @memberof module:vanadium.rpc |
| */ |
| function Server(router, serverOption) { |
| if (!(this instanceof Server)) { |
| return new Server(router); |
| } |
| |
| this._router = router; |
| this._rootCtx = router._rootCtx; |
| this._handle = 0; |
| this.id = nextServerID++; |
| this.dispatcher = null; |
| this.serviceObjectHandles = {}; |
| this.serverOption = serverOption || new ServerOption(); |
| } |
| |
| // TODO(aghassemi) the serviceObject example needs to point to a "Guides" page |
| // on the website when we have it. https://github.com/vanadium/issues/issues/444 |
| /* jshint ignore:start */ |
| /** |
| * ServeOptions is a set of options that are passed to the |
| * [serve]{@link module:vanadium.rpc~Server#serve}. |
| * @typedef module:vanadium.rpc~Server~ServeOptions |
| * @property {module:vanadium.security.Authorize} authorizer An Authorizer |
| * that will handle the authorization for the method call. If null, then the |
| * default strict authorizer will be used. |
| */ |
| |
| /** |
| * <p>Serve associates object with name by publishing the address |
| * of this server with the mount table under the supplied name and using |
| * authorizer to authorize access to it.</p> |
| * <p>If name is an empty string, no attempt will made to publish that |
| * name to a mount table. It is an error to call |
| * {@link module:vanadium.rpc~Server#serve|serve} |
| * if either {@link module:vanadium.rpc~Server#serveDispatcher|serveDispatcher} |
| * or {@link module:vanadium.rpc~Server.serve|serve} has already been called. |
| * To serve the same object under multiple names, |
| * {@link module:vanadium.rpc~Server#addName|addName} can be used.</p> |
| * <p>To serve names of the form "mymedia/*" make the calls:</p> |
| * <pre> |
| * serve("mymedia", serviceObject, { |
| * authorizer: serviceAuthorizer // optional authorizer |
| * }); |
| * </pre> |
| * <p> |
| * serviceObject is simply a JavaScript object that implements service methods. |
| * </p> |
| * <p> |
| * <pre> |
| * var serviceObject = new MyService(); |
| * function MyService() {} |
| * </pre> |
| * <p> |
| * Each service method must take [ctx]{@link module:vanadium.context.Context} |
| * and [serverCall]{@link module:vanadium.rpc~ServerCall} as the |
| * first two parameters. |
| * </p> |
| * <p> |
| * The output arguments can be given in several forms - through direct return, |
| * return of a promise or calling a callback that is optionally the |
| * last parameter. |
| * </p> |
| * <pre> |
| * // Sync method that echoes the input text immediately. |
| * MyService.prototype.echo = function(ctx, serverCall, text) { |
| * return 'Echo: ' + text; |
| * }; |
| * </pre> |
| * <pre> |
| * // Async method that echoes the input text after 1 second, using Promises. |
| * MyService.prototype.delayedEcho = function(ctx, serverCall, text) { |
| * return new Promise(function(resolve, reject) { |
| * setTimeout(function() { |
| * resolve('Echo: ' + text); |
| * }, 1000); |
| * }); |
| * }; |
| *</pre> |
| *<pre> |
| * // Async method that echoes the input text after 1 second, using Callbacks. |
| * MyService.prototype.delayedEcho = function(ctx, serverCall, text, callback) { |
| * setTimeout(function() { |
| * // first argument to the callback is error, second argument is results |
| * callback(null, 'Echo: ' + text); |
| * }, 1000); |
| * }; |
| *</pre> |
| * |
| * @public |
| * @param {string} name Name to serve under. |
| * @param {object} serviceObject The service object that has a set of |
| * exported methods. |
| * @param {module:vanadium.rpc~Server~ServeOptions} options Options config. |
| * @param {module:vanadium~voidCb} [cb] If provided, the function |
| * will be called on completion. |
| * @return {Promise<void>} Promise to be called when serve completes or fails. |
| */ |
| /* jshint ignore:end */ |
| Server.prototype.serve = function(name, serviceObject, options, cb) { |
| if (typeof options === 'function') { |
| cb = options; |
| options = undefined; |
| } |
| |
| var authorizer; |
| |
| if (options) { |
| authorizer = options.authorizer; |
| } |
| |
| var dispatcher = leafDispatcher(serviceObject, authorizer); |
| |
| return this.serveDispatcher(name, dispatcher, cb); |
| }; |
| |
| /** |
| * @typedef module:vanadium.rpc~Server~DispatcherResponse |
| * @type {object} |
| * @property {object} service The Invoker that will handle |
| * method call. |
| * @property {module:vanadium.security.Authorize} authorizer An Authorizer that |
| * will handle the authorization for the method call. If null, then the default |
| * authorizer will be used. |
| */ |
| |
| /** |
| * A function that returns the service implementation for the object identified |
| * by the given suffix. |
| * @callback module:vanadium.rpc~Server~Dispatcher |
| * @param {string} suffix The suffix for the call. |
| * @param {module:vanadium.rpc~Server~Dispatcher-callback} cb |
| * The callback to call when the dispatch is complete. |
| * @return {Promise<module:vanadium.rpc~Server~DispatcherResponse>} |
| * Either the DispatcherResponse object to |
| * handle the method call or a Promise that will be resolved the service |
| * callback. |
| */ |
| |
| /** |
| * Callback passed into Dispatcher. |
| * @callback module:vanadium.rpc~Server~Dispatcher-callback |
| * @param {Error} err An error if one occurred. |
| * @param {object} object The object that will handle the method call. |
| */ |
| |
| /** |
| * <p>ServeDispatcher associates dispatcher with the portion of the mount |
| * table's name space for which name is a prefix, by publishing the |
| * address of this dispatcher with the mount table under the supplied name. |
| * RPCs invoked on the supplied name will be delivered to the supplied |
| * Dispatcher's lookup method which will in turn return the object. </p> |
| * |
| * <p>To serve names of the form "mymedia/*" make the calls: </p> |
| * |
| * <pre> |
| * serve("mymedia", dispatcher); |
| * </pre> |
| * |
| * <p>If name is an empty string, no attempt will made to publish that |
| * name to a mount table. </p> |
| * |
| * <p>It is an error to call |
| * {@link module:vanadium.rpc~Server#serveDispatcher|serveDispatcher} |
| * if {@link module:vanadium.rpc~Server#serve|serve} has already been called. |
| * It is also an error |
| * to call serveDispatcher multiple times.</p> |
| * To serve the same dispatcher under multiple names, |
| * {@link module:vanadium.rpc~Server#addName|addName} can be used. </p> |
| * |
| * @public |
| * @param {string} name Name to serve under. |
| * @param {module:vanadium.rpc~Server~Dispatcher} dispatcher A function that |
| * will take in the suffix and the method to be called and return the service |
| * object for that suffix. |
| * @param {module:vanadium~voidCb} [cb] If provided, the function |
| * will be called on completion. |
| * @return {Promise<void>} Promise to be called when serve completes or fails. |
| */ |
| Server.prototype.serveDispatcher = function(name, dispatcher, cb) { |
| this.dispatcher = dispatcher; |
| return this._router.serve(name, this, cb); |
| }; |
| |
| /** |
| * Stop gracefully stops all services on this Server. |
| * New calls are rejected, but any in-flight calls are allowed to complete. |
| * All published named are unmounted. |
| * @param {module:vanadium~voidCb} [cb] If provided, the function |
| * will be called on completion. |
| * @return {Promise<void>} Promise to be called when stop service completes or |
| * fails |
| */ |
| Server.prototype.stop = function(cb) { |
| return this._router.stopServer(this, cb); |
| }; |
| |
| /** |
| * Adds the specified name to the mount table for the object or dispatcher |
| * served by this server. It is an error to specify a name that was not |
| * previously added using |
| * [serve]{@link module:vanadium.rpc~Server#serve}/ |
| * [serveDispatcher]{@link module:vanadium.rpc~Server#serveDispatcher} |
| * or [addName]{@link module:vanadium.rpc~Server#addName}. |
| * @public |
| * @param {string} name Name to publish. |
| * @param {module:vanadium~voidCb} [cb] If provided, the function |
| * will be called on completion. |
| * @return {Promise<void>} Promise to be called when operation completes or |
| * fails |
| */ |
| Server.prototype.addName = function(name, cb) { |
| return this._router.addName(name, this, cb); |
| }; |
| |
| /** |
| * Removes the specified name from the mount table. It is an |
| * error to specify a name that was not previously added using |
| * [serve]{@link module:vanadium.rpc~Server#serve}/ |
| * [serveDispatcher]{@link module:vanadium.rpc~Server#serveDispatcher} |
| * or [addName]{@link module:vanadium.rpc~Server#addName}. |
| * @public |
| * @param {string} name Name to remove. |
| * @param {function} [cb] If provided, the function will be called on |
| * completion. The only argument is an error if there was one. |
| * @return {Promise<void>} Promise to be called when operation completes or |
| * fails. |
| */ |
| Server.prototype.removeName = function(name, cb) { |
| return this._router.removeName(name, this, cb); |
| }; |
| |
| /** |
| * @private |
| * @param {Number} handle The handle for the service. |
| * @return {Object} The invoker corresponding to the provided error. |
| */ |
| Server.prototype.getInvokerForHandle = function(handle) { |
| var result = this.serviceObjectHandles[handle]; |
| delete this.serviceObjectHandles[handle]; |
| |
| return result.invoker; |
| }; |
| |
| /** |
| * Handles the authorization for an RPC. |
| * @private |
| * @param {Number} handle The handle for the authorizer. |
| * @param {module:vanadium.context.Context} ctx The ctx of the |
| * call. |
| * @param {module:vanadium.security~SecurityCall} call The security call. |
| * @return {Promise} A promise that will be fulfilled with the result. |
| */ |
| Server.prototype.handleAuthorization = function(handle, ctx, call) { |
| var handler = this.serviceObjectHandles[handle]; |
| var authorizer = defaultAuthorizer; |
| if (handler && handler.authorizer) { |
| authorizer = handler.authorizer; |
| } |
| |
| var def = new Deferred(); |
| var inspectableAuthorizer = new InspectableFunction(authorizer); |
| asyncCall(ctx, null, inspectableAuthorizer, [], [ctx, call], |
| function(err) { |
| if (err) { |
| def.reject(err); |
| return; |
| } |
| def.resolve(); |
| }); |
| return def.promise; |
| }; |
| |
| var InvokeOnNonInvoker = makeError( |
| 'v.io/core/javascript.InvokeOnNonInvoker', actions.NO_RETRY, |
| '{1:}{2:} trying to invoke on a non-invoker{:_}'); |
| /** |
| * Handles the result of lookup and returns an error if there was any. |
| * @private |
| */ |
| Server.prototype._handleLookupResult = function(object) { |
| if (!object.hasOwnProperty('service')) { |
| // TODO(bjornick): Use the correct context here. |
| throw new InvokeOnNonInvoker(this._rootCtx); |
| } |
| object._handle = this._handle; |
| try { |
| object.invoker = new Invoker(object.service); |
| } catch(e) { |
| vlog.logger.error('lookup failed', e); |
| return e; |
| } |
| this.serviceObjectHandles[object._handle] = object; |
| this._handle++; |
| return null; |
| }; |
| |
| |
| |
| /* |
| * Perform the lookup call to the user code on the suffix and method passed in. |
| */ |
| Server.prototype._handleLookup = function(suffix) { |
| var self = this; |
| var def = new Deferred(); |
| |
| var argsNames = inspector(this.dispatcher).names; |
| var useCallback = argsNames.length >= 2; |
| var cb = function(err, val) { |
| if (err) { |
| def.reject(err); |
| } else { |
| def.resolve(val); |
| } |
| }; |
| |
| var result; |
| try { |
| result = this.dispatcher(suffix, cb); |
| } catch (e) { |
| def.reject(e); |
| vlog.logger.error(e); |
| return def.promise; |
| } |
| |
| if (!useCallback) { |
| if (result === undefined) { |
| return def.promise.then(handleResult); |
| } |
| |
| if (result instanceof Error) { |
| def.reject(result); |
| return def.promise; |
| } |
| |
| def.resolve(result); |
| } |
| |
| function handleResult(v) { |
| var err = self._handleLookupResult(v); |
| if (err) { |
| return Promise.reject(err); |
| } |
| return Promise.resolve(v); |
| } |
| |
| return def.promise.then(handleResult); |
| }; |
| |
| /** |
| * Export the module |
| */ |
| module.exports = Server; |