UNPKG

13.8 kBJavaScriptView Raw
1/*
2 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *
22 */
23
24(function () {
25 "use strict";
26
27 var util = require("util"),
28 domain = require("domain"),
29 ConnectionManager = require("./ConnectionManager");
30
31 /**
32 * @constructor
33 * DomainManager is a module/class that handles the loading, registration,
34 * and execution of all commands and events. It is a singleton, and is passed
35 * to a domain in its init() method.
36 */
37 var self = exports;
38
39 /**
40 * @private
41 * @type {object}
42 * Map of all the registered domains
43 */
44 var _domains = {};
45
46 /**
47 * @private
48 * @type {Array.<Module>}
49 * Array of all modules we have loaded. Used for avoiding duplicate loading.
50 */
51 var _initializedDomainModules = [];
52
53 /**
54 * @private
55 * @type {number}
56 * Used for generating unique IDs for events.
57 */
58 var _eventCount = 1;
59
60 /**
61 * @private
62 * @type {Array}
63 * JSON.stringify-able Array of the current API. In the format of
64 * Inspector.json. This is a cache that we invalidate every time the
65 * API changes.
66 */
67 var _cachedDomainDescriptions = null;
68
69 /**
70 * Returns whether a domain with the specified name exists or not.
71 * @param {string} domainName The domain name.
72 * @return {boolean} Whether the domain exists
73 */
74 function hasDomain(domainName) {
75 return !!_domains[domainName];
76 }
77
78 /**
79 * Returns a new empty domain. Throws error if the domain already exists.
80 * @param {string} domainName The domain name.
81 * @param {{major: number, minor: number}} version The domain version.
82 * The version has a format like {major: 1, minor: 2}. It is reported
83 * in the API spec, but serves no other purpose on the server. The client
84 * can make use of this.
85 */
86 function registerDomain(domainName, version) {
87 if (!hasDomain(domainName)) {
88 // invalidate the cache
89 _cachedDomainDescriptions = null;
90
91 _domains[domainName] = {version: version, commands: {}, events: {}};
92 } else {
93 console.error("[DomainManager] Domain " + domainName + " already registered");
94 }
95 }
96
97 /**
98 * Registers a new command with the specified domain. If the domain does
99 * not yet exist, it registers the domain with a null version.
100 * @param {string} domainName The domain name.
101 * @param {string} commandName The command name.
102 * @param {Function} commandFunction The callback handler for the function.
103 * The function is called with the arguments specified by the client in the
104 * command message. Additionally, if the command is asynchronous (isAsync
105 * parameter is true), the function is called with an automatically-
106 * constructed callback function of the form cb(err, result). The function
107 * can then use this to send a response to the client asynchronously.
108 * @param {boolean} isAsync See explanation for commandFunction param
109 * @param {?string} description Used in the API documentation
110 * @param {?Array.<{name: string, type: string, description:string}>} parameters
111 * Used in the API documentation.
112 * @param {?Array.<{name: string, type: string, description:string}>} returns
113 * Used in the API documentation.
114 */
115 function registerCommand(domainName, commandName, commandFunction, isAsync,
116 description, parameters, returns) {
117 // invalidate the cache
118 _cachedDomainDescriptions = null;
119
120 if (!hasDomain(domainName)) {
121 registerDomain(domainName, null);
122 }
123
124 if (!_domains[domainName].commands[commandName]) {
125 _domains[domainName].commands[commandName] = {
126 commandFunction: commandFunction,
127 isAsync: isAsync,
128 description: description,
129 parameters: parameters,
130 returns: returns
131 };
132 } else {
133 throw new Error("Command " + domainName + "." +
134 commandName + " already registered");
135 }
136 }
137
138 /**
139 * Executes a command by domain name and command name. Called by a connection's
140 * message parser. Sends response or error (possibly asynchronously) to the
141 * connection.
142 * @param {Connection} connection The requesting connection object.
143 * @param {number} id The unique command ID.
144 * @param {string} domainName The domain name.
145 * @param {string} commandName The command name.
146 * @param {Array} parameters The parameters to pass to the command function. If
147 * the command is asynchronous, will be augmented with a callback function.
148 * (see description in registerCommand documentation)
149 */
150 function executeCommand(connection, id, domainName,
151 commandName, parameters) {
152 var el, i;
153
154 for (i = 0; i < parameters.length; i++) {
155 el = parameters[i];
156 if (typeof el === "string") {
157 if (el.startsWith("/projects/")) {
158 parameters[i] = exports.projectsDir + el.substr("/projects".length);
159 } else if (el.startsWith("/samples/")) {
160 parameters[i] = exports.samplesDir + el.substr("/samples".length);
161 }
162 }
163 }
164
165 if (_domains[domainName] &&
166 _domains[domainName].commands[commandName]) {
167 var command = _domains[domainName].commands[commandName];
168 if (command.isAsync) {
169 var execDom = domain.create(),
170 callback = function (err, result) {
171 if (err) {
172 connection.sendCommandError(id, err);
173 } else {
174 connection.sendCommandResponse(id, result);
175 }
176 };
177
178 parameters.push(callback);
179
180 execDom.on("error", function(err) {
181 connection.sendCommandError(id, err.message);
182 execDom.dispose();
183 });
184
185 execDom.bind(command.commandFunction).apply(connection, parameters);
186 } else { // synchronous command
187 try {
188 connection.sendCommandResponse(
189 id,
190 command.commandFunction.apply(connection, parameters)
191 );
192 } catch (e) {
193 connection.sendCommandError(id, e.message);
194 }
195 }
196 } else {
197 connection.sendCommandError(id, "no such command: " +
198 domainName + "." + commandName);
199 }
200 }
201
202 /**
203 * Registers an event domain and name.
204 * @param {string} domainName The domain name.
205 * @param {string} eventName The event name.
206 * @param {?Array.<{name: string, type: string, description:string}>} parameters
207 * Used in the API documentation.
208 */
209 function registerEvent(domainName, eventName, parameters) {
210 // invalidate the cache
211 _cachedDomainDescriptions = null;
212
213 if (!hasDomain(domainName)) {
214 registerDomain(domainName, null);
215 }
216
217 if (!_domains[domainName].events[eventName]) {
218 _domains[domainName].events[eventName] = {
219 parameters: parameters
220 };
221 } else {
222 console.error("[DomainManager] Event " + domainName + "." +
223 eventName + " already registered");
224 }
225 }
226
227 /**
228 * Emits an event with the specified name and parameters to all connections.
229 *
230 * TODO: Future: Potentially allow individual connections to register
231 * for which events they want to receive. Right now, we have so few events
232 * that it's fine to just send all events to everyone and decide on the
233 * client side if the client wants to handle them.
234 *
235 * @param {string} domainName The domain name.
236 * @param {string} eventName The event name.
237 * @param {?Array} parameters The parameters. Must be JSON.stringify-able
238 */
239 function emitEvent(domainName, eventName, parameters) {
240 if (_domains[domainName] && _domains[domainName].events[eventName]) {
241 ConnectionManager.sendEventToAllConnections(
242 _eventCount++,
243 domainName,
244 eventName,
245 parameters
246 );
247 } else {
248 console.error("[DomainManager] No such event: " + domainName +
249 "." + eventName);
250 }
251 }
252
253 /**
254 * Loads and initializes domain modules using the specified paths. Checks to
255 * make sure that a module is not loaded/initialized more than once.
256 *
257 * @param {Array.<string>} paths The paths to load. The paths can be relative
258 * to the DomainManager or absolute. However, modules that aren't in core
259 * won't know where the DomainManager module is, so in general, all paths
260 * should be absolute.
261 * @return {boolean} Whether loading succeded. (Failure will throw an exception).
262 */
263 function loadDomainModulesFromPaths(paths) {
264 var pathArray = paths;
265 if (!util.isArray(paths)) {
266 pathArray = [paths];
267 }
268 pathArray.forEach(function (path) {
269 if (path.startsWith(exports.httpRoot)) {
270 path = "../../brackets-srv" + path.substr(exports.httpRoot.length);
271 } else if (path.startsWith("/support/extensions/user/")) {
272 if (exports.allowUserDomains) {
273 path = exports.supportDir + path.substr("/support".length);
274 } else {
275 console.error("ERROR: User domains are not allowed: " + path);
276 return false;
277 }
278 } else if (path !== "./BaseDomain") {
279 console.error("ERROR: Invalid domain path: " + path);
280 return false;
281 }
282
283 try {
284 var m = require(path);
285 if (m && m.init && _initializedDomainModules.indexOf(m) < 0) {
286 m.init(self);
287 _initializedDomainModules.push(m); // don't init more than once
288 }
289 } catch (err) {
290 console.error(err);
291 return false;
292 }
293 });
294 return true; // if we fail, an exception will be thrown
295 }
296
297 /**
298 * Returns a description of all registered domains in the format of WebKit's
299 * Inspector.json. Used for sending API documentation to clients.
300 *
301 * @return {Array} Array describing all domains.
302 */
303 function getDomainDescriptions() {
304 if (!_cachedDomainDescriptions) {
305 _cachedDomainDescriptions = [];
306
307 var domainNames = Object.keys(_domains);
308 domainNames.forEach(function (domainName) {
309 var d = {
310 domain: domainName,
311 version: _domains[domainName].version,
312 commands: [],
313 events: []
314 };
315 var commandNames = Object.keys(_domains[domainName].commands);
316 commandNames.forEach(function (commandName) {
317 var c = _domains[domainName].commands[commandName];
318 d.commands.push({
319 name: commandName,
320 description: c.description,
321 parameters: c.parameters,
322 returns: c.returns
323 });
324 });
325 var eventNames = Object.keys(_domains[domainName].events);
326 eventNames.forEach(function (eventName) {
327 d.events.push({
328 name: eventName,
329 parameters: _domains[domainName].events[eventName].parameters
330 });
331 });
332 _cachedDomainDescriptions.push(d);
333 });
334 }
335 return _cachedDomainDescriptions;
336 }
337
338 exports.hasDomain = hasDomain;
339 exports.registerDomain = registerDomain;
340 exports.registerCommand = registerCommand;
341 exports.executeCommand = executeCommand;
342 exports.registerEvent = registerEvent;
343 exports.emitEvent = emitEvent;
344 exports.loadDomainModulesFromPaths = loadDomainModulesFromPaths;
345 exports.getDomainDescriptions = getDomainDescriptions;
346}());