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 | ;
|
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 | }());
|