/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensions/default/StaticServer/node/StaticServerDomain.js
477 строк
15 KB
ficristo
ESLint: enable strict rule
25 авг 2016, 23:24
25 авг 2016, 23:24
8370a08
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*eslint-env node */ /*jslint node: true */ "use strict"; var http = require("http"), pathJoin = require("path").join, connect = require("connect"), utils = require("connect/lib/utils"), mime = require("connect/node_modules/send/node_modules/mime"), parse = utils.parseUrl; var _domainManager; var FILTER_REQUEST_TIMEOUT = 5000; /** * @private * @type {number} * Used to assign unique identifiers to each filter request */ var _filterRequestCounter = 0; /** * @private * @type {number} * Duration to wait before passing a filtered request to the static file server. */ var _filterRequestTimeout = FILTER_REQUEST_TIMEOUT; /** * When Chrome has a css stylesheet replaced over live development, * it re-checks any image urls in the new css stylesheet. If it has * to hit the server to check them, this is asynchronous, so it causes * two re-layouts of the webpage, which causes flickering. By setting * a max age of five seconds, Chrome won't bother to hit the server * on each keystroke. So, flickers will happen at most once every five * seconds. * * @const * @type {number} */ var STATIC_CACHE_MAX_AGE = 5000; // 5 seconds /** * @private * @type {Object.<string, http.Server>} * A map from root paths to server instances. */ var _servers = {}; /** * @private * @type {Object.<string, {Object.<number, http.ServerResponse>}} * A map from a request identifier to its request/response mapping. */ var _requests = {}; /** * @private * @type {Object.<string, {Object.<string>}} * A map from root paths to relative paths to rewrite */ var _rewritePaths = {}; var PATH_KEY_PREFIX = "LiveDev_"; /** * @private * Removes trailing forward slash for the project root absolute path * @param {string} path Absolute path for a server * @returns {string} */ function normalizeRootPath(path) { return (path && path[path.length - 1] === "/") ? path.slice(0, -1) : path; } /** * @private * Generates a key based on a server's absolute path * @param {string} path Absolute path for a server * @returns {string} */ function getPathKey(path) { return PATH_KEY_PREFIX + normalizeRootPath(path); } /** * @private * Helper function to create a new server. * @param {string} path The absolute path that should be the document root * @param {function(?string, ?httpServer)} cb Callback function that receives * an error (or null if there was no error) and the server (or null if there * was an error). */ function _createServer(path, port, createCompleteCallback) { var server, app, address, pathKey = getPathKey(path); // create a new map for this server's requests _requests[pathKey] = {}; function requestRoot(server, cb) { address = server.address(); // Request the root file from the project in order to ensure that the // server is actually initialized. If we don't do this, it seems like // connect takes time to warm up the server. var req = http.get( {host: address.address, port: address.port}, function (res) { cb(null, res); } ); req.on("error", function (err) { cb(err, null); }); } function rewrite(req, res, next) { var location = {pathname: parse(req).pathname}, hasListener = _rewritePaths[pathKey] && _rewritePaths[pathKey][location.pathname], requestId = _filterRequestCounter++, timeoutId; // ignore most HTTP methods and files that we're not watching if (("GET" !== req.method && "HEAD" !== req.method) || !hasListener) { next(); return; } // pause the request and wait for listeners to possibly respond var pause = utils.pause(req); function resume(doNext) { // delete the callback after it's used or we hit the timeout. // if this path is requested again, a new callback is generated. delete _requests[pathKey][requestId]; // pass request to next middleware if (doNext) { next(); } pause.resume(); } // map request pathname to response callback _requests[pathKey][requestId] = function (resData) { // clear timeout immediately when this callback is called clearTimeout(timeoutId); // response data is optional if (resData.body) { // HTTP headers var type = mime.lookup(location.pathname), charset = mime.charsets.lookup(type); res.setHeader("Content-Type", type + (charset ? "; charset=" + charset : "")); // TODO (jasonsanjose): off-by-1 error here, why? // Chrome seems to handle the request without issues when Content-Length is not specified //res.setHeader("Content-Length", Buffer.byteLength(resData.body /* TODO encoding? */)); // response body res.end(resData.body); } // resume the HTTP ServerResponse, pass to next middleware if // no response data was passed resume(!resData.body); }; location.hostname = address.address; location.port = address.port; location.root = path; var request = { headers: req.headers, location: location, id: requestId }; // dispatch request event _domainManager.emitEvent("staticServer", "requestFilter", [request]); // set a timeout if custom responses are not returned timeoutId = setTimeout(function () { resume(true); }, _filterRequestTimeout); } app = connect(); app.use(rewrite); // JSLint complains if we use `connect.static` because static is a // reserved word. app.use(connect["static"](path, { maxAge: STATIC_CACHE_MAX_AGE })); app.use(connect.directory(path)); server = http.createServer(app); // Once the server is listening then verify we can handle requests // before calling the callback server.on("listening", function () { requestRoot( server, function (err, res) { if (err) { createCompleteCallback("Could not GET root after launching server", null); } else { createCompleteCallback(null, server); } } ); }); // If the given port/address is in use then use a random port server.on("error", function (e) { if (e.code === "EADDRINUSE") { server.listen(0, "127.0.0.1"); } else { throw e; } }); server.listen(port, "127.0.0.1"); } /** * @private * Handler function for the staticServer.getServer command. If a server * already exists for the given path, returns that, otherwise starts a new * one. * @param {string} path The absolute path that should be the document root * @param {function(?string, ?{address: string, family: string, * port: number})} cb Callback that should receive the address information * for the server. First argument is the error string (or null if no error), * second argument is the address object (or null if there was an error). * The "family" property of the address indicates whether the address is, * for example, IPv4, IPv6, or a UNIX socket. */ function _cmdGetServer(path, port, cb) { // Make sure the key doesn't conflict with some built-in property of Object. var pathKey = getPathKey(path); if (_servers[pathKey]) { cb(null, _servers[pathKey].address()); } else { _createServer(path, port, function (err, server) { if (err) { cb(err, null); } else { _servers[pathKey] = server; _rewritePaths[pathKey] = {}; cb(null, server.address()); } }); } } /** * @private * Handler function for the staticServer.closeServer command. If a server * exists for the given path, closes it, otherwise does nothing. Note that * this function doesn't wait for the actual socket to close, since the * server will actually wait for all client connections to close (which can * be awhile); but once it returns, you're guaranteed to get a different * server the next time you call getServer() on the same path. * * @param {string} path The absolute path whose server we should close. * @return {boolean} true if there was a server for that path, false otherwise */ function _cmdCloseServer(path, cba) { var pathKey = getPathKey(path); if (_servers[pathKey]) { var serverToClose = _servers[pathKey]; delete _servers[pathKey]; serverToClose.close(); return true; } return false; } /** * @private * Defines a set of paths from a server's root path to watch and fire "request" events for. * * @param {string} path The absolute path whose server we should watch * @param {Array.<string>} paths An array of root-relative paths to watch. * Each path should begin with a forward slash "/". */ function _cmdSetRequestFilterPaths(root, paths) { var pathKey = getPathKey(root), rewritePaths = {}; // reset list of filtered paths for each call to setRequestFilterPaths _rewritePaths[pathKey] = rewritePaths; paths.forEach(function (path) { rewritePaths[path] = pathJoin(root, path); }); } /** * @private * Overrides the server response from static middleware with the provided * response data. This should be called only in response to a filtered request. * * @param {string} path The absolute path of the server * @param {string} root The relative path of the file beginning with a forward slash "/" * @param {!Object} resData Response data to use */ function _cmdWriteFilteredResponse(root, path, resData) { var pathKey = getPathKey(root), callback = _requests[pathKey][resData.id]; if (callback) { callback(resData); } else { console.warn("writeFilteredResponse: Missing callback for %s. This command must only be called after a requestFilter event has fired for a path.", pathJoin(root, path)); } } /** * @private * Unit tests only. Set, or reset, timeout value for filtered requests. * * @param {number=} timeout Duration to wait before passing a filtered request to the static file server. * If omitted, timeout is reset to FILTER_REQUEST_TIMEOUT (5s). */ function _cmdSetRequestFilterTimeout(timeout) { timeout = (timeout === undefined) ? FILTER_REQUEST_TIMEOUT : timeout; _filterRequestTimeout = timeout; } /** * Initializes the StaticServer domain with its commands. * @param {DomainManager} domainManager The DomainManager for the server */ function init(domainManager) { _domainManager = domainManager; if (!domainManager.hasDomain("staticServer")) { domainManager.registerDomain("staticServer", {major: 0, minor: 1}); } _domainManager.registerCommand( "staticServer", "_setRequestFilterTimeout", _cmdSetRequestFilterTimeout, false, "Unit tests only. Set timeout value for filtered requests.", [{ name: "timeout", type: "number", description: "Duration to wait before passing a filtered request to the static file server." }], [] ); _domainManager.registerCommand( "staticServer", "getServer", _cmdGetServer, true, "Starts or returns an existing server for the given path.", [ { name: "path", type: "string", description: "Absolute filesystem path for root of server." }, { name: "port", type: "number", description: "Port number to use for HTTP server. Pass zero to assign a random port." } ], [{ name: "address", type: "{address: string, family: string, port: number}", description: "hostname (stored in 'address' parameter), port, and socket type (stored in 'family' parameter) for the server. Currently, 'family' will always be 'IPv4'." }] ); _domainManager.registerCommand( "staticServer", "closeServer", _cmdCloseServer, false, "Closes the server for the given path.", [{ name: "path", type: "string", description: "absolute filesystem path for root of server" }], [{ name: "result", type: "boolean", description: "indicates whether a server was found for the specific path then closed" }] ); _domainManager.registerCommand( "staticServer", "setRequestFilterPaths", _cmdSetRequestFilterPaths, false, "Defines a set of paths from a server's root path to watch and fire 'requestFilter' events for.", [ { name: "root", type: "string", description: "absolute filesystem path for root of server" }, { name: "paths", type: "Array", description: "path to notify" } ], [] ); _domainManager.registerCommand( "staticServer", "writeFilteredResponse", _cmdWriteFilteredResponse, false, "Overrides the server response from static middleware with the provided response data. This should be called only in response to a filtered request.", [ { name: "root", type: "string", description: "absolute filesystem path for root of server" }, { name: "path", type: "string", description: "path to rewrite" }, { name: "resData", type: "{body: string, headers: Array}", description: "TODO" } ], [] ); _domainManager.registerEvent( "staticServer", "requestFilter", [{ name: "location", type: "{hostname: string, pathname: string, port: number, root: string: id: number}", description: "request path" }] ); } exports.init = init;