From 9ce04d9fce1a804141085e550b681e94d9afd1b9 Mon Sep 17 00:00:00 2001 From: Andy Heimdahl Date: Sun, 9 Feb 2014 19:42:22 -0600 Subject: [PATCH 1/3] User can now specify a port for the live dev server --- .../default/StaticServer/StaticServer.js | 6 +- src/extensions/default/StaticServer/main.js | 14 +++- .../StaticServer/node/StaticServerDomain.js | 59 +++++++++++--- .../default/StaticServer/unittests.js | 76 +++++++++++++++---- 4 files changed, 126 insertions(+), 29 deletions(-) diff --git a/src/extensions/default/StaticServer/StaticServer.js b/src/extensions/default/StaticServer/StaticServer.js index 652d421ade4..570a10e819e 100644 --- a/src/extensions/default/StaticServer/StaticServer.js +++ b/src/extensions/default/StaticServer/StaticServer.js @@ -44,11 +44,13 @@ define(function (require, exports, module) { * pathResolver - Function to covert absolute native paths to project relative paths * root - Native path to the project root (and base URL) * nodeDomain - An initialized NodeDomain + * port - Optional port number to serve from */ function StaticServer(config) { this._nodeDomain = config.nodeDomain; this._onRequestFilter = this._onRequestFilter.bind(this); - + this._port = config.port; + BaseServer.call(this, config); } @@ -102,7 +104,7 @@ define(function (require, exports, module) { */ StaticServer.prototype.readyToServe = function () { var self = this; - return this._nodeDomain.exec("getServer", self._root) + return this._nodeDomain.exec("getServer", self._root, self._port) .done(function (address) { self._baseUrl = "http://" + address.address + ":" + address.port + "/"; }) diff --git a/src/extensions/default/StaticServer/main.js b/src/extensions/default/StaticServer/main.js index 6e0ee70e1d5..773e1f4ad09 100644 --- a/src/extensions/default/StaticServer/main.js +++ b/src/extensions/default/StaticServer/main.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), FileUtils = brackets.getModule("file/FileUtils"), LiveDevServerManager = brackets.getModule("LiveDevelopment/LiveDevServerManager"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"), BaseServer = brackets.getModule("LiveDevelopment/Servers/BaseServer").BaseServer, NodeDomain = brackets.getModule("utils/NodeDomain"), ProjectManager = brackets.getModule("project/ProjectManager"), @@ -49,6 +50,12 @@ define(function (require, exports, module) { * @type {NodeDomain} */ var _nodeDomain = new NodeDomain("staticServer", _domainPath); + + /** + * @private + * @type {NodeDomain} A user-specified port number for the live development server to use. + */ + var _port = 0; /** * @private @@ -59,13 +66,16 @@ define(function (require, exports, module) { var config = { nodeDomain : _nodeDomain, pathResolver : ProjectManager.makeProjectRelativeIfPossible, - root : ProjectManager.getProjectRoot().fullPath + root : ProjectManager.getProjectRoot().fullPath, + port : _port }; return new StaticServer(config); } - + AppInit.appReady(function () { + _port = PreferencesManager.get("liveDevPort") || 0; + LiveDevServerManager.registerServer({ create: _createStaticServer }, 5); }); diff --git a/src/extensions/default/StaticServer/node/StaticServerDomain.js b/src/extensions/default/StaticServer/node/StaticServerDomain.js index c4dd5611cb3..98cc23da632 100644 --- a/src/extensions/default/StaticServer/node/StaticServerDomain.js +++ b/src/extensions/default/StaticServer/node/StaticServerDomain.js @@ -98,7 +98,20 @@ function normalizeRootPath(path) { return (path && path[path.length - 1] === "/") ? path.slice(0, -1) : path; } - + + /** + * @private + * Converts given value to a valid port number or zero. + * A zero will cause a random port to be used. + * @param {number} port number to clean, can be string as well + * @return {number} a valid port number or zero if a value wasn't passed in or valid. + */ + function sanatizePort(port) { + port = parseInt(port, 10); + port = (port && !isNaN(port) && port > 0 && port < 65536) ? port : 0; + return port; + } + /** * @private * Generates a key based on a server's absolute path @@ -117,10 +130,11 @@ * an error (or null if there was no error) and the server (or null if there * was an error). */ - function _createServer(path, createCompleteCallback) { + function _createServer(path, port, createCompleteCallback) { var server, app, address, + portNum = sanatizePort(port), pathKey = getPathKey(path); // create a new map for this server's requests @@ -208,11 +222,11 @@ // 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 @@ -221,7 +235,10 @@ app.use(connect.directory(path)); server = http.createServer(app); - server.listen(0, "127.0.0.1", function () { + + // Once the server is listening then verify we can handle requests + // before calling the callback + server.on("listening", function () { requestRoot( server, function (err, res) { @@ -233,6 +250,17 @@ } ); }); + + // 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(portNum, "127.0.0.1"); } /** @@ -248,13 +276,13 @@ * The "family" property of the address indicates whether the address is, * for example, IPv4, IPv6, or a UNIX socket. */ - function _cmdGetServer(path, cb) { + 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, function (err, server) { + _createServer(path, port, function (err, server) { if (err) { cb(err, null); } else { @@ -371,11 +399,18 @@ _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: "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}", diff --git a/src/extensions/default/StaticServer/unittests.js b/src/extensions/default/StaticServer/unittests.js index bf7248fb47e..d94163c9642 100644 --- a/src/extensions/default/StaticServer/unittests.js +++ b/src/extensions/default/StaticServer/unittests.js @@ -93,7 +93,7 @@ define(function (require, exports, module) { it("should start a static server on the given folder", function () { var serverInfo, path = testFolder + "/folder1"; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -110,10 +110,59 @@ define(function (require, exports, module) { }); }); + it("should start the server on the given port", function () { + var serverInfo, + path = testFolder + "/folder1"; + + runs(function () { + nodeConnection.domains.staticServer.getServer(path, 54321) + .done(function (info) { + serverInfo = info; + }); + }); + + waitsFor(function () { return serverInfo; }, "waiting for static server to start"); + runs(function () { + expect(serverInfo.port).toBe(54321); + + waitsForDone(nodeConnection.domains.staticServer.closeServer(path), + "waiting for static server to close"); + }); + }); + + it("should start a static server using a random port when the given port is already in use", function () { + var serverInfo1, serverInfo2, + path1 = testFolder + "/folder1", path2 = testFolder + "/folder2"; + + runs(function () { + nodeConnection.domains.staticServer.getServer(path1, 54321) + .done(function (info) { + serverInfo1 = info; + }); + nodeConnection.domains.staticServer.getServer(path2, 54321) + .done(function (info) { + serverInfo2 = info; + }); + }); + + waitsFor(function () { return serverInfo1 && serverInfo2; }, "waiting for static servers to start"); + + runs(function () { + expect(serverInfo1.port).toBe(54321); + expect(serverInfo2.port).not.toBe(54321); + expect(serverInfo2.port).toBeGreaterThan(0); + + waitsForDone(nodeConnection.domains.staticServer.closeServer(path1), + "waiting for static server 1 to close"); + waitsForDone(nodeConnection.domains.staticServer.closeServer(path2), + "waiting for static server 2 to close"); + }); + }); + it("should serve the text of a file in the given folder", function () { var serverInfo, text, path = testFolder + "/folder1"; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -141,11 +190,11 @@ define(function (require, exports, module) { var serverInfo1, serverInfo2, path1 = testFolder + "/folder1", path2 = testFolder + "/folder2"; runs(function () { - nodeConnection.domains.staticServer.getServer(path1) + nodeConnection.domains.staticServer.getServer(path1, 0) .done(function (info) { serverInfo1 = info; }); - nodeConnection.domains.staticServer.getServer(path2) + nodeConnection.domains.staticServer.getServer(path2, 0) .done(function (info) { serverInfo2 = info; }); @@ -168,11 +217,11 @@ define(function (require, exports, module) { path1 = testFolder + "/folder1", path2 = testFolder + "/folder2", text1, text2; runs(function () { - nodeConnection.domains.staticServer.getServer(path1) + nodeConnection.domains.staticServer.getServer(path1, 0) .done(function (info) { serverInfo1 = info; }); - nodeConnection.domains.staticServer.getServer(path2) + nodeConnection.domains.staticServer.getServer(path2, 0) .done(function (info) { serverInfo2 = info; }); @@ -212,7 +261,7 @@ define(function (require, exports, module) { timeout = 500; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -268,7 +317,7 @@ define(function (require, exports, module) { requestId; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -318,7 +367,7 @@ define(function (require, exports, module) { requestId; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -365,7 +414,7 @@ define(function (require, exports, module) { requestId; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -427,7 +476,7 @@ define(function (require, exports, module) { }); runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -486,7 +535,7 @@ define(function (require, exports, module) { timeout = 500; runs(function () { - nodeConnection.domains.staticServer.getServer(path) + nodeConnection.domains.staticServer.getServer(path, 0) .done(function (info) { serverInfo = info; }); @@ -549,7 +598,8 @@ define(function (require, exports, module) { baseUrl: "http://localhost/", nodeDomain: mockNodeDomain, pathResolver: pathResolver, - root: projectPath + root: projectPath, + port: 0 }; it("should translate local paths to server paths", function () { From d89c84242a1da9ceebfa20d2d7e6af00c7c02c4d Mon Sep 17 00:00:00 2001 From: Andy Heimdahl Date: Wed, 12 Feb 2014 13:39:33 -0600 Subject: [PATCH 2/3] pull port pref at time of getServer instead of at brackets load --- .../default/StaticServer/StaticServer.js | 19 ++++++++++++++----- src/extensions/default/StaticServer/main.js | 12 +----------- .../StaticServer/node/StaticServerDomain.js | 4 ++-- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/extensions/default/StaticServer/StaticServer.js b/src/extensions/default/StaticServer/StaticServer.js index 570a10e819e..6270fb0e136 100644 --- a/src/extensions/default/StaticServer/StaticServer.js +++ b/src/extensions/default/StaticServer/StaticServer.js @@ -29,8 +29,17 @@ maxerr: 50, browser: true */ define(function (require, exports, module) { "use strict"; - var BaseServer = brackets.getModule("LiveDevelopment/Servers/BaseServer").BaseServer, - FileUtils = brackets.getModule("file/FileUtils"); + var BaseServer = brackets.getModule("LiveDevelopment/Servers/BaseServer").BaseServer, + FileUtils = brackets.getModule("file/FileUtils"), + PreferencesManager = brackets.getModule("preferences/PreferencesManager"); + + + /** + * @private + * + * Prefences manager for this extension + */ + var _prefs = PreferencesManager.getExtensionPrefs("staticserver"); /** * @constructor @@ -44,12 +53,10 @@ define(function (require, exports, module) { * pathResolver - Function to covert absolute native paths to project relative paths * root - Native path to the project root (and base URL) * nodeDomain - An initialized NodeDomain - * port - Optional port number to serve from */ function StaticServer(config) { this._nodeDomain = config.nodeDomain; this._onRequestFilter = this._onRequestFilter.bind(this); - this._port = config.port; BaseServer.call(this, config); } @@ -104,7 +111,9 @@ define(function (require, exports, module) { */ StaticServer.prototype.readyToServe = function () { var self = this; - return this._nodeDomain.exec("getServer", self._root, self._port) + var port = _prefs.get("port"); + + return this._nodeDomain.exec("getServer", self._root, port) .done(function (address) { self._baseUrl = "http://" + address.address + ":" + address.port + "/"; }) diff --git a/src/extensions/default/StaticServer/main.js b/src/extensions/default/StaticServer/main.js index 773e1f4ad09..fefab7c9aee 100644 --- a/src/extensions/default/StaticServer/main.js +++ b/src/extensions/default/StaticServer/main.js @@ -33,7 +33,6 @@ define(function (require, exports, module) { ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), FileUtils = brackets.getModule("file/FileUtils"), LiveDevServerManager = brackets.getModule("LiveDevelopment/LiveDevServerManager"), - PreferencesManager = brackets.getModule("preferences/PreferencesManager"), BaseServer = brackets.getModule("LiveDevelopment/Servers/BaseServer").BaseServer, NodeDomain = brackets.getModule("utils/NodeDomain"), ProjectManager = brackets.getModule("project/ProjectManager"), @@ -51,12 +50,6 @@ define(function (require, exports, module) { */ var _nodeDomain = new NodeDomain("staticServer", _domainPath); - /** - * @private - * @type {NodeDomain} A user-specified port number for the live development server to use. - */ - var _port = 0; - /** * @private * @return {StaticServerProvider} The singleton StaticServerProvider initialized @@ -66,16 +59,13 @@ define(function (require, exports, module) { var config = { nodeDomain : _nodeDomain, pathResolver : ProjectManager.makeProjectRelativeIfPossible, - root : ProjectManager.getProjectRoot().fullPath, - port : _port + root : ProjectManager.getProjectRoot().fullPath }; return new StaticServer(config); } AppInit.appReady(function () { - _port = PreferencesManager.get("liveDevPort") || 0; - LiveDevServerManager.registerServer({ create: _createStaticServer }, 5); }); diff --git a/src/extensions/default/StaticServer/node/StaticServerDomain.js b/src/extensions/default/StaticServer/node/StaticServerDomain.js index 98cc23da632..68d0860bf54 100644 --- a/src/extensions/default/StaticServer/node/StaticServerDomain.js +++ b/src/extensions/default/StaticServer/node/StaticServerDomain.js @@ -106,7 +106,7 @@ * @param {number} port number to clean, can be string as well * @return {number} a valid port number or zero if a value wasn't passed in or valid. */ - function sanatizePort(port) { + function sanitizePort(port) { port = parseInt(port, 10); port = (port && !isNaN(port) && port > 0 && port < 65536) ? port : 0; return port; @@ -134,7 +134,7 @@ var server, app, address, - portNum = sanatizePort(port), + portNum = sanitizePort(port), pathKey = getPathKey(path); // create a new map for this server's requests From 5df149dea61fd97cc2a9ffa146bbaffb9600764e Mon Sep 17 00:00:00 2001 From: Andy Heimdahl Date: Wed, 12 Feb 2014 16:21:30 -0600 Subject: [PATCH 3/3] Refactored to detect port changes between LiveDev launches. --- .../default/StaticServer/StaticServer.js | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/extensions/default/StaticServer/StaticServer.js b/src/extensions/default/StaticServer/StaticServer.js index 6270fb0e136..609f7954864 100644 --- a/src/extensions/default/StaticServer/StaticServer.js +++ b/src/extensions/default/StaticServer/StaticServer.js @@ -111,15 +111,46 @@ define(function (require, exports, module) { */ StaticServer.prototype.readyToServe = function () { var self = this; - var port = _prefs.get("port"); + var deferred = new $.Deferred(); - return this._nodeDomain.exec("getServer", self._root, port) + function sanitizePort(port) { + port = parseInt(port, 10); + port = (port && !isNaN(port) && port > 0 && port < 65536) ? port : 0; + return port; + } + + function onSuccess(address) { + self._baseUrl = "http://" + address.address + ":" + address.port + "/"; + deferred.resolve(); + } + + function onFailure() { + self._baseUrl = ""; + deferred.resolve(); + } + + var port = sanitizePort(_prefs.get("port")); + + this._nodeDomain.exec("getServer", self._root, port) .done(function (address) { - self._baseUrl = "http://" + address.address + ":" + address.port + "/"; + + // If the port returned wasn't what was requested, then the preference has + // changed. Close the current server, and open a new one with the new port. + if (address.port !== port && port > 0) { + return self._nodeDomain.exec("closeServer", self._root) + .done(function () { + return self._nodeDomain.exec("getServer", self._root, port) + .done(onSuccess) + .fail(onFailure); + }) + .fail(onFailure); + } + + onSuccess(address); }) - .fail(function () { - self._baseUrl = ""; - }); + .fail(onFailure); + + return deferred.promise(); }; /**