/*jshint strict: false */ //////////////////////////////////////////////////////////////////////////////// /// @brief Cluster planning functionality /// /// @file /// /// DISCLAIMER /// /// Copyright 2014 triagens GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is triAGENS GmbH, Cologne, Germany /// /// @author Max Neunhoeffer /// @author Copyright 2014, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// // ----------------------------------------------------------------------------- // --SECTION-- Planner functionality // ----------------------------------------------------------------------------- var download = require("internal").download; var base64Encode = require("internal").base64Encode; var testPort = require("internal").testPort; // Our default configurations: var PlannerLocalDefaults = { "agencyPrefix" : "arango", "numberOfAgents" : 1, "numberOfDBservers" : 2, "startSecondaries" : false, "numberOfCoordinators" : 1, "DBserverIDs" : ["Pavel", "Perry", "Pancho", "Paul", "Pierre", "Pit", "Pia", "Pablo" ], "coordinatorIDs" : ["Claus", "Chantalle", "Claire", "Claudia", "Claas", "Clemens", "Chris" ], "dataPath" : "", // means configured in dispatcher "logPath" : "", // means configured in dispatcher "arangodPath" : "", // means configured in dispatcher "agentPath" : "", // means configured in dispatcher "agentExtPorts" : [4001], "agentIntPorts" : [7001], "DBserverPorts" : [8629], "coordinatorPorts" : [8530], "dispatchers" : {"me": {"endpoint": "tcp://127.0.0.1:"}}, // this means only we as a local instance "useSSLonDBservers" : false, "useSSLonCoordinators" : false, "valgrind" : "", "valgrindopts" : [], "valgrindXmlFileBase" : "", "valgrindTestname" : "", "valgrindHosts" : "", "extremeVerbosity" : false }; // Some helpers using underscore: var _ = require("underscore"); function objmap (o, f) { var r = {}; var k = _.keys(o); var i; for (i = 0;i < k.length; i++) { r[k[i]] = f(o[k[i]]); } return r; } function copy (o) { if (_.isArray(o)) { return _.map(o,copy); } if (_.isObject(o)) { return objmap(o,copy); } return o; } // A class to find free ports: function PortFinder (list, dispatcher) { if (!Array.isArray(list)) { throw new Error("need a list as first argument"); } if (typeof dispatcher !== "object" || !dispatcher.hasOwnProperty("endpoint") || !dispatcher.hasOwnProperty("id")) { throw new Error('need a dispatcher object as second argument'); } if (!dispatcher.hasOwnProperty("avoidPorts")) { dispatcher.avoidPorts = {}; } this.list = list; this.dispatcher = dispatcher; this.pos = 0; this.port = 0; } function getAuthorizationHeader (username, passwd) { return "Basic " + base64Encode(username + ":" + passwd); } function getAuthorization (dispatcher) { return getAuthorizationHeader(dispatcher.username, dispatcher.passwd); } function endpointToURL (endpoint) { if (endpoint.substr(0,6) === "ssl://") { return "https://" + endpoint.substr(6); } var pos = endpoint.indexOf("://"); if (pos === -1) { return "http://" + endpoint; } return "http" + endpoint.substr(pos); } PortFinder.prototype.next = function () { while (true) { // will be left by return when port is found if (this.pos < this.list.length) { this.port = this.list[this.pos++]; if (this.dispatcher.hasOwnProperty("portOverlapIndex")) { // add some value to the initial port if we had multiple // dispatchers on the same IP address. Otherwise, the // dispatchers will try to bind the exact same ports, and // this is prone to races this.port += this.dispatcher.portOverlapIndex * 20; } } else if (this.port === 0) { this.port = Math.floor(Math.random() * (65536 - 1024)) + 1024; } else { this.port++; if (this.port > 65535) { this.port = 1024; } } // Check that port is available: if (! this.dispatcher.avoidPorts.hasOwnProperty(this.port)) { var available = true; if ((this.dispatcher.endpoint === "tcp://localhost:") || (this.dispatcher.endpoint === "tcp://127.0.0.1:")) { available = testPort("tcp://0.0.0.0:" + this.port); } else { var url = endpointToURL(this.dispatcher.endpoint) + "/_admin/clusterCheckPort?port="+this.port; var hdrs = {}; if (this.dispatcher.username !== undefined && this.dispatcher.passwd !== undefined) { hdrs.Authorization = getAuthorization(this.dispatcher); } var r = download(url, "", {"method": "GET", "timeout": 5, "headers": hdrs }); if (r.code === 200) { available = JSON.parse(r.body); } else { throw new Error("Cannot check port on dispatcher "+this.dispatcher.endpoint); } } if (available) { this.dispatcher.avoidPorts[this.port] = true; // do not use it again return this.port; } } } }; function exchangeProtocol (endpoint, useSSL) { var pos = endpoint.indexOf("://"); if (pos !== -1) { return (useSSL ? "ssl" : "tcp") + endpoint.substr(3); } return (useSSL ? "ssl://" : "tcp://") + endpoint; } function exchangePort (endpoint, newport) { var pos = endpoint.lastIndexOf(":"); if (pos < 0) { return endpoint+":"+newport; } return endpoint.substr(0,pos+1)+newport; } function getAddrPort (endpoint) { var pos = endpoint.indexOf("://"); if (pos !== -1) { return endpoint.substr(pos+3); } return endpoint; } function getAddr (endpoint) { var addrPort = getAddrPort(endpoint); var pos = addrPort.indexOf(":"); if (pos !== -1) { return addrPort.substr(0,pos); } return addrPort; } function getPort (endpoint) { var pos = endpoint.lastIndexOf(":"); if (pos !== -1) { return parseInt(endpoint.substr(pos+1),10); } return 8529; } // The following function merges default configurations and user configuration. function fillConfigWithDefaults (config, defaultConfig) { var n; for (n in defaultConfig) { if (defaultConfig.hasOwnProperty(n)) { if (!config.hasOwnProperty(n)) { config[n] = copy(defaultConfig[n]); } } } } //////////////////////////////////////////////////////////////////////////////// /// @brief check if IP addresses of dispatchers are non-unique, and if yes, /// then set a flag for each dispatcher with non-unique address /// we need this to prevent race conditions when multiple dispatchers on the /// same physical server start coordinators or dbservers and compete for the /// same ports //////////////////////////////////////////////////////////////////////////////// function checkDispatcherIps (config) { if (typeof config.dispatchers !== "object") { return; } var d, dispatcherIps = { }; for (d in config.dispatchers) { if (config.dispatchers.hasOwnProperty(d)) { var ip = config.dispatchers[d].endpoint.replace(/:\d+$/, '').replace(/^(ssl|tcp):\/\//, ''); if (! dispatcherIps.hasOwnProperty(ip)) { dispatcherIps[ip] = [ ]; } dispatcherIps[ip].push(d); } } for (d in dispatcherIps) { if (dispatcherIps.hasOwnProperty(d)) { if (dispatcherIps[d].length > 1) { // more than one dispatcher for an IP address // need to ensure that the port ranges do not overlap for (var i = 0; i < dispatcherIps[d].length; ++i) { config.dispatchers[dispatcherIps[d][i]].portOverlapIndex = i; } } } } } //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_Cluster_Planner_Constructor /// /// *new require("@arangodb/cluster").Planner(userConfig)* /// /// This constructor builds a cluster planner object. The one and only /// argument is an object that can have the properties described below. /// The planner can plan clusters on a single machine (basically for /// testing purposes) and on multiple machines. The resulting "cluster plans" /// can be used by the kickstarter to start up the processes comprising /// the cluster, including the agency. To this end, there has to be one /// dispatcher on every machine participating in the cluster. A dispatcher /// is a simple instance of ArangoDB, compiled with the cluster extensions, /// but not running in cluster mode. This is why the configuration option /// *dispatchers* below is of central importance. /// /// - *dispatchers*: an object with a property for each dispatcher, /// the property name is the ID of the dispatcher and the value /// should be an object with at least the property *endpoint* /// containing the endpoint of the corresponding dispatcher. /// Further optional properties are: /// /// - *avoidPorts* which is an object /// in which all port numbers that should not be used are bound to /// *true*, default is empty, that is, all ports can be used /// - *arangodExtraArgs*, which is a list of additional /// command line arguments that will be given to DBservers and /// coordinators started by this dispatcher, the default is /// an empty list. These arguments will be appended to those /// produced automatically, such that one can overwrite /// things with this. /// - *allowCoordinators*, which is a boolean value indicating /// whether or not coordinators should be started on this /// dispatcher, the default is *true* /// - *allowDBservers*, which is a boolean value indicating /// whether or not DBservers should be started on this dispatcher, /// the default is *true* /// - *allowAgents*, which is a boolean value indicating whether or /// not agents should be started on this dispatcher, the default is /// *true* /// - *username*, which is a string that contains the user name /// for authentication with this dispatcher /// - *passwd*, which is a string that contains the password /// for authentication with this dispatcher, if not both /// *username* and *passwd* are set, then no authentication /// is used between dispatchers. Note that this will not work /// if the dispatchers are configured with authentication. /// /// If *.dispatchers* is empty (no property), then an entry for the /// local arangod itself is automatically added. Note that if the /// only configured dispatcher has endpoint *tcp://localhost:*, /// all processes are started in a special "local" mode and are /// configured to bind their endpoints only to the localhost device. /// In all other cases both agents and *arangod* instances bind /// their endpoints to all available network devices. /// - *numberOfAgents*: the number of agents in the agency, /// usually there is no reason to deviate from the default of 3. The /// planner distributes them amongst the dispatchers, if possible. /// - *agencyPrefix*: a string that is used as prefix for all keys of /// configuration data stored in the agency. /// - *numberOfDBservers*: the number of DBservers in the /// cluster. The planner distributes them evenly amongst the dispatchers. /// - *startSecondaries*: a boolean flag indicating whether or not /// secondary servers are started. In this version, this flag is /// silently ignored, since we do not yet have secondary servers. /// - *numberOfCoordinators*: the number of coordinators in the cluster, /// the planner distributes them evenly amongst the dispatchers. /// - *DBserverIDs*: a list of DBserver IDs (strings). If the planner /// runs out of IDs it creates its own ones using *DBserver* /// concatenated with a unique number. /// - *coordinatorIDs*: a list of coordinator IDs (strings). If the planner /// runs out of IDs it creates its own ones using *Coordinator* /// concatenated with a unique number. /// - *dataPath*: this is a string and describes the path under which /// the agents, the DBservers and the coordinators store their /// data directories. This can either be an absolute path (in which /// case all machines in the clusters must use the same path), or /// it can be a relative path. In the latter case it is relative /// to the directory that is configured in the dispatcher with the /// *cluster.data-path* option (command line or configuration file). /// The directories created will be called *data-PREFIX-ID* where /// *PREFIX* is replaced with the agency prefix (see above) and *ID* /// is the ID of the DBserver or coordinator. /// - *logPath*: this is a string and describes the path under which /// the DBservers and the coordinators store their log file. This can /// either be an absolute path (in which case all machines in the cluster /// must use the same path), or it can be a relative path. In the /// latter case it is relative to the directory that is configured /// in the dispatcher with the *cluster.log-path* option. /// - *arangodPath*: this is a string and describes the path to the /// actual executable *arangod* that will be started for the /// DBservers and coordinators. If this is an absolute path, it /// obviously has to be the same on all machines in the cluster /// as described for *dataPath*. If it is an empty string, the /// dispatcher uses the executable that is configured with the /// *cluster.arangod-path* option, which is by default the same /// executable as the dispatcher uses. /// - *agentPath*: this is a string and describes the path to the /// actual executable that will be started for the agents in the /// agency. If this is an absolute path, it obviously has to be /// the same on all machines in the cluster, as described for /// *arangodPath*. If it is an empty string, the dispatcher /// uses its *cluster.agent-path* option. /// - *agentExtPorts*: a list of port numbers to use for the external /// ports of the agents. When running out of numbers in this list, /// the planner increments the last one used by one for every port /// needed. Note that the planner checks availability of the ports /// during the planning phase by contacting the dispatchers on the /// different machines, and uses only ports that are free during /// the planning phase. Obviously, if those ports are connected /// before the actual startup, things can go wrong. /// - *agentIntPorts*: a list of port numbers to use for the internal /// ports of the agents. The same comments as for *agentExtPorts* /// apply. /// - *DBserverPorts*: a list of port numbers to use for the /// DBservers. The same comments as for *agentExtPorts* apply. /// - *coordinatorPorts*: a list of port numbers to use for the /// coordinators. The same comments as for *agentExtPorts* apply. /// - *useSSLonDBservers*: a boolean flag indicating whether or not /// we use SSL on all DBservers in the cluster /// - *useSSLonCoordinators*: a boolean flag indicating whether or not /// we use SSL on all coordinators in the cluster /// - *valgrind*: a string to contain the path of the valgrind binary /// if we should run the cluster components in it /// - *valgrindopts*: commandline options to the valgrind process /// - *valgrindXmlFileBase*: pattern for logfiles /// - *valgrindTestname*: name of test to add to the logfiles /// - *valgrindHosts*: which host classes should run in valgrind? /// Coordinator / DBServer /// - *extremeVerbosity* : if set to true, then there will be more test /// run output, especially for cluster tests. /// /// All these values have default values. Here is the current set of /// default values: /// /// ```js /// { /// "agencyPrefix" : "arango", /// "numberOfAgents" : 1, /// "numberOfDBservers" : 2, /// "startSecondaries" : false, /// "numberOfCoordinators" : 1, /// "DBserverIDs" : ["Pavel", "Perry", "Pancho", "Paul", "Pierre", /// "Pit", "Pia", "Pablo" ], /// "coordinatorIDs" : ["Claus", "Chantalle", "Claire", "Claudia", /// "Claas", "Clemens", "Chris" ], /// "dataPath" : "", // means configured in dispatcher /// "logPath" : "", // means configured in dispatcher /// "arangodPath" : "", // means configured as dispatcher /// "agentPath" : "", // means configured in dispatcher /// "agentExtPorts" : [4001], /// "agentIntPorts" : [7001], /// "DBserverPorts" : [8629], /// "coordinatorPorts" : [8530], /// "dispatchers" : {"me": {"endpoint": "tcp://localhost:"}}, /// // this means only we as a local instance /// "useSSLonDBservers" : false, /// "useSSLonCoordinators" : false /// }; /// ``` /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// function Planner (userConfig) { 'use strict'; if (typeof userConfig !== "object") { throw new Error("userConfig must be an object"); } this.config = copy(userConfig); checkDispatcherIps(this.config); fillConfigWithDefaults(this.config, PlannerLocalDefaults); this.commands = []; this.makePlan(); } //////////////////////////////////////////////////////////////////////////////// /// @brief the actual planning method //////////////////////////////////////////////////////////////////////////////// Planner.prototype.makePlan = function() { // This sets up the plan for the cluster according to the options var config = this.config; var dispatchers = this.dispatchers = copy(config.dispatchers); var id; for (id in dispatchers) { if (dispatchers.hasOwnProperty(id)) { dispatchers[id].id = id; if (!dispatchers[id].hasOwnProperty("avoidPorts")) { dispatchers[id].avoidPorts = {}; } if (!dispatchers[id].hasOwnProperty("arangodExtraArgs")) { dispatchers[id].arangodExtraArgs = []; } if (!dispatchers[id].hasOwnProperty("allowCoordinators")) { dispatchers[id].allowCoordinators = true; } if (!dispatchers[id].hasOwnProperty("allowDBservers")) { dispatchers[id].allowDBservers = true; } if (!dispatchers[id].hasOwnProperty("allowAgents")) { dispatchers[id].allowAgents = true; } } } // If no dispatcher is there, configure a local one (ourselves): if (Object.keys(dispatchers).length === 0) { dispatchers.me = { "id": "me", "endpoint": "tcp://localhost:", "avoidPorts": {}, "allowCoordinators": true, "allowDBservers": true, "allowAgents": true }; config.onlyLocalhost = true; } else { config.onlyLocalhost = false; var k = Object.keys(dispatchers); if (k.length === 1 && ((dispatchers[k[0]].endpoint.substr(0,16) === "tcp://localhost:") || (dispatchers[k[0]].endpoint.substr(0,16) === "tcp://127.0.0.1:") )){ config.onlyLocalhost = true; } } var dispList = Object.keys(dispatchers); var pf,pf2; // lists of port finder objects var i; // Distribute agents to dispatchers (round robin, choosing ports) var d = 0; var wrap = d; var agents = []; pf = []; // will be filled lazily pf2 = []; // will be filled lazily i = 0; var oldi = i; while (i < config.numberOfAgents) { if (dispatchers[dispList[d]].allowAgents) { // Find two ports: if (!pf.hasOwnProperty(d)) { pf[d] = new PortFinder(config.agentExtPorts, dispatchers[dispList[d]]); pf2[d] = new PortFinder(config.agentIntPorts, dispatchers[dispList[d]]); } agents.push({"dispatcher":dispList[d], "extPort":pf[d].next(), "intPort":pf2[d].next()}); i++; } if (++d >= dispList.length) { d = 0; } if (d === wrap && oldi === i) { break; } } // Distribute coordinators to dispatchers var coordinators = []; pf = []; d = 0; i = 0; wrap = d; oldi = i; while (i < config.numberOfCoordinators) { if (dispatchers[dispList[d]].allowCoordinators) { if (!pf.hasOwnProperty(d)) { pf[d] = new PortFinder(config.coordinatorPorts, dispatchers[dispList[d]]); } if (!config.coordinatorIDs.hasOwnProperty(i)) { config.coordinatorIDs[i] = "Coordinator"+i; } coordinators.push({"id":config.coordinatorIDs[i], "dispatcher":dispList[d], "port":pf[d].next()}); i++; } if (++d >= dispList.length) { d = 0; } if (d === wrap && oldi === i) { break; } } // Distribute DBservers to dispatchers (secondaries if wanted) var DBservers = []; pf = []; i = 0; wrap = d; oldi = i; while (i < config.numberOfDBservers) { if (dispatchers[dispList[d]].allowDBservers) { if (!pf.hasOwnProperty(d)) { pf[d] = new PortFinder(config.DBserverPorts, dispatchers[dispList[d]]); } if (!config.DBserverIDs.hasOwnProperty(i)) { config.DBserverIDs[i] = "Primary"+i; } DBservers.push({"id":config.DBserverIDs[i], "dispatcher":dispList[d], "port":pf[d].next()}); i++; } if (++d >= dispList.length) { d = 0; } if (d === wrap && oldi === i) { break; } } // Store this plan in object: this.coordinators = coordinators; this.DBservers = DBservers; this.agents = agents; var launchers = {}; for (i = 0; i < dispList.length; i++) { launchers[dispList[i]] = { "DBservers": [], "Coordinators": [] }; } // Set up agency data: var agencyData = this.agencyData = {}; var prefix = agencyData[config.agencyPrefix] = {}; var tmp; // First the Target, we collect Launchers information at the same time: tmp = prefix.Target = {}; tmp.Lock = '"UNLOCKED"'; tmp.Version = '"1"'; var dbs = tmp.DBServers = {}; tmp.MapLocalToEndpoint = {}; // will stay empty for now var map = tmp.MapIDToEndpoint = {}; var s; var ep; for (i = 0; i < DBservers.length; i++) { s = DBservers[i]; dbs[s.id] = '"none"'; ep = exchangePort(dispatchers[s.dispatcher].endpoint,s.port); ep = exchangeProtocol(ep, config.useSSLonDBservers); map[s.id] = '"'+ep+'"'; launchers[s.dispatcher].DBservers.push(s.id); } var coo = tmp.Coordinators = {}; for (i = 0; i < coordinators.length; i++) { s = coordinators[i]; coo[s.id] = '"none"'; ep = exchangePort(dispatchers[s.dispatcher].endpoint,s.port); ep = exchangeProtocol(ep, config.useSSLonCoordinators); map[s.id] = '"' + ep + '"'; launchers[s.dispatcher].Coordinators.push(s.id); } tmp.Databases = { "_system" : '{"name":"_system", "id":"1"}' }; tmp.Collections = { "_system" : {} }; // Now Plan: prefix.Plan = copy(tmp); delete prefix.Plan.MapIDToEndpoint; // Now Current: prefix.Current = { "Lock" : '"UNLOCKED"', "Version" : '"1"', "DBservers" : {}, "Coordinators" : {}, "Databases" : {"_system":{ "name": '"name"', "id": '"1"' }}, "Collections" : {"_system":{}}, "NewServers" : {}, "ServersRegistered": {"Version":'"1"'}, "ShardsCopied" : {} }; // Now Sync: prefix.Sync = { "ServerStates" : {}, "Problems" : {}, "LatestID" : '"1"', "Commands" : {}, "HeartbeatIntervalMs": '1000', "UserVersion" : '"1"' }; tmp = prefix.Sync.Commands; for (i = 0; i < DBservers; i++) { tmp[DBservers[i].id] = '"SERVE"'; } // Finally Launchers: prefix.Launchers = objmap(launchers, JSON.stringify); // make commands tmp = this.commands = []; var tmp2,j; for (i = 0; i < agents.length; i++) { tmp2 = { "action" : "startAgent", "dispatcher" : agents[i].dispatcher, "extPort" : agents[i].extPort, "intPort" : agents[i].intPort, "peers" : [], "agencyPrefix" : config.agencyPrefix, "dataPath" : config.dataPath, "logPath" : config.logPath, "agentPath" : config.agentPath, "onlyLocalhost" : config.onlyLocalhost, "valgrind" : config.valgrind, "valgrindopts" : config.valgrindopts, "valgrindXmlFileBase" : config.valgrindXmlFileBase, "valgrindTestname" : config.valgrindXmlFileBase, "valgrindHosts" : config.valgrindHosts, "extremeVerbosity" : config.extremeVerbosity }; for (j = 0; j < i; j++) { ep = dispatchers[agents[j].dispatcher].endpoint; tmp2.peers.push( exchangePort( ep, agents[j].intPort ) ); } tmp.push(tmp2); } var agencyPos = { "agencyPrefix": config.agencyPrefix, "endpoints": agents.map(function(a) { return exchangePort(dispatchers[a.dispatcher].endpoint, a.extPort);}) }; tmp.push({ "action": "sendConfiguration", "agency": agencyPos, "data": agencyData, "extremeVerbosity": config.extremeVerbosity }); for (i = 0; i < dispList.length; i++) { tmp.push( { "action" : "startServers", "dispatcher" : dispList[i], "DBservers" : copy(launchers[dispList[i]].DBservers), "Coordinators" : copy(launchers[dispList[i]].Coordinators), "name" : dispList[i], "dataPath" : config.dataPath, "logPath" : config.logPath, "arangodPath" : config.arangodPath, "onlyLocalhost" : config.onlyLocalhost, "agency" : copy(agencyPos), "useSSLonDBservers" : config.useSSLonDBservers, "useSSLonCoordinators" : config.useSSLonCoordinators, "valgrind" : config.valgrind, "valgrindopts" : config.valgrindopts, "valgrindXmlFileBase" : config.valgrindXmlFileBase, "valgrindTestname" : config.valgrindTestname, "valgrindHosts" : config.valgrindHosts, "extremeVerbosity" : config.extremeVerbosity } ); } var cc = []; var c; var e; for (i = 0; i < coordinators.length; i++) { c = coordinators[i]; e = exchangePort(dispatchers[c.dispatcher].endpoint, c.port); e = exchangeProtocol(e, config.useSSLonCoordinators); cc.push(endpointToURL(e)); } var dd = []; for (i = 0; i < DBservers.length; i++) { c = DBservers[i]; e = exchangePort(dispatchers[c.dispatcher].endpoint, c.port); e = exchangeProtocol(e, config.useSSLonDBservers); dd.push(endpointToURL(e)); } tmp.push({ "action": "bootstrapServers", "dbServers": dd, "coordinators": cc, "extremeVerbosity": config.extremeVerbosity }); this.myname = "me"; }; //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_Planner_prototype_getPlan /// /// `Planner.getPlan()` /// /// returns the cluster plan as a JavaScript object. The result of this /// method can be given to the constructor of a Kickstarter. /// @endDocuBlock //////////////////////////////////////////////////////////////////////////////// Planner.prototype.getPlan = function() { // Computes the dispatcher commands and returns them as JSON return { "dispatchers": this.dispatchers, "commands": this.commands }; }; exports.PortFinder = PortFinder; exports.Planner = Planner; exports.exchangePort = exchangePort; exports.exchangeProtocol = exchangeProtocol; exports.getAddrPort = getAddrPort; exports.getPort = getPort; exports.getAddr = getAddr; exports.endpointToURL = endpointToURL; // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- /// Local Variables: /// mode: outline-minor /// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint" /// End: