1 | 'use strict';
|
2 |
|
3 | var INHERIT = require('inherit'),
|
4 | Q = require('q'),
|
5 | QFS = require('q-io/fs'),
|
6 | QHTTP = require('q-io/http'),
|
7 | QS = require('querystring'),
|
8 | UTIL = require('util'),
|
9 | URL = require('url'),
|
10 | MIME = require('mime'),
|
11 | PATH = require('./path'),
|
12 | MAKE = require('./make'),
|
13 | LOGGER = require('./logger'),
|
14 | LEVEL = require('./level'),
|
15 | U = require('./util');
|
16 |
|
17 | var defaultDocument = 'index.html';
|
18 |
|
19 | exports.Server = INHERIT({
|
20 |
|
21 | __constructor: function(opts) {
|
22 | this.opts = opts || {};
|
23 | },
|
24 |
|
25 | start: function() {
|
26 |
|
27 | return this.rootExists(this.opts.root)()
|
28 | .then(this.showInfo())
|
29 | .then(this.initBuildRunner())
|
30 | .then(this.startServers())
|
31 | .then(function(servers) {
|
32 | return Q.all(servers.map(function(s) {
|
33 | return s.stopped;
|
34 | }));
|
35 | })
|
36 | .then(function() {
|
37 | LOGGER.info('Servers were stopped');
|
38 | });
|
39 |
|
40 | },
|
41 |
|
42 |
|
43 | showInfo: function() {
|
44 | var _this = this;
|
45 |
|
46 | return function() {
|
47 | LOGGER.finfo("Project root is '%s'", _this.opts.root);
|
48 | LOGGER.fverbose('Options are %j', _this.opts);
|
49 | };
|
50 | },
|
51 |
|
52 |
|
53 |
|
54 | rootExists: function(root) {
|
55 |
|
56 | return function() {
|
57 | return QFS.exists(root).then(function(exists) {
|
58 | if (!exists) return Q.reject(UTIL.format("Project root '%s' doesn't exist", root));
|
59 | });
|
60 | };
|
61 |
|
62 | },
|
63 |
|
64 |
|
65 | startServers: function() {
|
66 | var _this = this;
|
67 |
|
68 | return function(runner) {
|
69 | var requestHandler = _this.createRequestHandler(_this.opts.root, runner);
|
70 |
|
71 | return Q.when(_this.startServer.call(_this, requestHandler),
|
72 | function(servers) {
|
73 |
|
74 | return Q.all(servers).then(function(servers) {
|
75 |
|
76 |
|
77 | servers.forEach(function(server) {
|
78 | process.once('SIGINT', _this.stopServer(server));
|
79 | });
|
80 |
|
81 | return servers;
|
82 | });
|
83 |
|
84 | });
|
85 |
|
86 | };
|
87 |
|
88 | },
|
89 |
|
90 | startServer: function(requestHandler) {
|
91 | var _this = this,
|
92 | netServer = QHTTP.Server(requestHandler),
|
93 | started = [];
|
94 |
|
95 | netServer.node.on('error', this.errorHandler.bind(this));
|
96 |
|
97 |
|
98 | started.push(netServer.listen(_this.opts.port, _this.opts.host).then(function(listener) {
|
99 | LOGGER.finfo(
|
100 | 'Server is listening on port %s. Point your browser to http://%s:%s/',
|
101 | _this.opts.port,
|
102 | _this.opts.host || 'localhost',
|
103 | _this.opts.port
|
104 | );
|
105 |
|
106 | return listener;
|
107 | }));
|
108 |
|
109 | return started;
|
110 | },
|
111 |
|
112 | stopServer: function(server) {
|
113 | return function() {
|
114 | server.stop();
|
115 | };
|
116 | },
|
117 |
|
118 | errorHandler: function(port, error) {
|
119 | if (!error) {
|
120 | error = port;
|
121 | port = this.opts.port;
|
122 | }
|
123 |
|
124 | switch (error.code) {
|
125 | case 'EADDRINUSE':
|
126 | LOGGER.error('port ' + port + ' is in use. Specify a different port or stop the service which is using it.');
|
127 | break;
|
128 |
|
129 | case 'EACCES':
|
130 | LOGGER.error('insufficient permissions to listen port ' + port + '. Specify a different port.');
|
131 | break;
|
132 |
|
133 | default:
|
134 | LOGGER.error(error.message);
|
135 | break;
|
136 | }
|
137 | },
|
138 |
|
139 | initBuildRunner: function() {
|
140 | var _this = this;
|
141 |
|
142 | return function() {
|
143 | return Q.when(MAKE.createArch(_this.opts), function(arch) {
|
144 | return new MAKE.APW(arch, _this.opts.workers, {
|
145 | root: _this.opts.root,
|
146 | verbose: _this.opts.verbose,
|
147 | force: _this.opts.force
|
148 | });
|
149 | });
|
150 | };
|
151 | },
|
152 |
|
153 | _targetsInProcess: 0,
|
154 |
|
155 | createRequestHandler: function(root, runner) {
|
156 | var _this = this;
|
157 |
|
158 | return function(request, response) {
|
159 |
|
160 | var reqPath = URL.parse(request.path).pathname,
|
161 | relPath = QS.unescape(reqPath).replace(/^\/|\/$/g, ''),
|
162 | fullPath = PATH.join(root, relPath);
|
163 |
|
164 | if (PATH.dirSep === '\\') relPath = PATH.unixToOs(relPath);
|
165 |
|
166 | LOGGER.fverbose('*** trying to access %s', fullPath);
|
167 |
|
168 |
|
169 | LOGGER.fverbose('*** searching for node "%s"', relPath);
|
170 | return runner.findNode(relPath)
|
171 | .fail(function(err) {
|
172 | if (typeof err === 'string') {
|
173 | LOGGER.fverbose('*** node not found "%s"', relPath);
|
174 | return;
|
175 | }
|
176 | return Q.reject(err);
|
177 | })
|
178 | .then(function(id) {
|
179 | if (!id) return;
|
180 |
|
181 | _this._targetsInProcess++;
|
182 |
|
183 |
|
184 | LOGGER.fverbose('*** node found, building "%s"', id);
|
185 | LOGGER.fdebug('targets: %s', _this._targetsInProcess);
|
186 | LOGGER.time('[t] Build total for "%s"', id);
|
187 | return runner.process(id).fin(function() {
|
188 | _this._targetsInProcess--;
|
189 |
|
190 | if (_this._targetsInProcess === 0) LEVEL.resetLevelsCache();
|
191 |
|
192 | LOGGER.fdebug('targets: %s', _this._targetsInProcess);
|
193 | LOGGER.timeEnd('[t] Build total for "%s"', id);
|
194 | });
|
195 | })
|
196 |
|
197 |
|
198 | .then(_this.processPath(response, fullPath, root))
|
199 |
|
200 |
|
201 | .fail(_this.httpError(response, 500));
|
202 |
|
203 | };
|
204 |
|
205 | },
|
206 |
|
207 | processPath: function(response, path, root) {
|
208 | var _this = this;
|
209 |
|
210 | return function() {
|
211 |
|
212 | return QFS.exists(path).then(function(exists) {
|
213 |
|
214 |
|
215 | if (!exists) {
|
216 | return _this.httpError(response, 404)(path);
|
217 | }
|
218 |
|
219 |
|
220 | return QFS.isDirectory(path).then(function(isDir) {
|
221 |
|
222 | if (isDir) {
|
223 |
|
224 |
|
225 | var def = PATH.join(path, defaultDocument);
|
226 | return QFS.isFile(def).then(function(isFile) {
|
227 | if (isFile) return _this.streamFile(response, def)();
|
228 | return _this.processDirectory(response, path, root)();
|
229 | });
|
230 |
|
231 | }
|
232 |
|
233 | return _this.streamFile(response, path)();
|
234 |
|
235 | });
|
236 |
|
237 | });
|
238 |
|
239 | };
|
240 | },
|
241 |
|
242 | processDirectory: function(response, path, root) {
|
243 | var _this = this;
|
244 |
|
245 | return function() {
|
246 | response.status = 200;
|
247 | response.charset = 'utf8';
|
248 | response.headers = { 'content-type': 'text/html' };
|
249 |
|
250 | var body = response.body = [],
|
251 | base = '/' + PATH.relative(root, path);
|
252 |
|
253 | _this.pushFormatted(body,
|
254 | '<!DOCTYPE html><html><head><meta charset="utf-8"/><title>%s</title></head>' +
|
255 | '<body><b>Index of %s</b><ul style=\'list-style-type: none\'>',
|
256 | path, path);
|
257 |
|
258 | var files = [],
|
259 | dirs = ['..'];
|
260 |
|
261 | body.push(U.getDirsFiles(path, dirs, files).then(function() {
|
262 | var listing = [],
|
263 | pushListing = function(objs, str) {
|
264 | objs.sort();
|
265 | objs.forEach(function(o) {
|
266 | _this.pushFormatted(listing, str, PATH.join(base, o), o);
|
267 | });
|
268 | };
|
269 |
|
270 | pushListing(dirs, '<li><a href="%s">/%s</a></li>');
|
271 | pushListing(files, '<li><a href="%s">%s</a></li>');
|
272 |
|
273 | return listing.join('');
|
274 | }));
|
275 |
|
276 | body.push('</ul></body></html>');
|
277 |
|
278 | return response;
|
279 | };
|
280 | },
|
281 |
|
282 | streamFile: function(response, path) {
|
283 |
|
284 | return function() {
|
285 |
|
286 | response.status = 200;
|
287 | response.charset = 'binary';
|
288 | response.headers = { 'content-type': MIME.lookup(path) };
|
289 | response.body = QFS.open(path, 'b');
|
290 |
|
291 | return response;
|
292 | };
|
293 | },
|
294 |
|
295 | httpError: function(response, code) {
|
296 | return function(err) {
|
297 | LOGGER.fwarn('*** HTTP error: %s, %s', code, (err && err.stack) || err);
|
298 |
|
299 | response.status = code;
|
300 | response.charset = 'utf8';
|
301 | response.headers = { 'content-type': 'text/html' };
|
302 | response.body = ['<h1>HTTP error ' + code + '</h1>'].concat(err? ['<pre>', '' + (err.stack || err), '</pre>'] : []);
|
303 |
|
304 | return response;
|
305 | };
|
306 | },
|
307 |
|
308 | pushFormatted: function(arr/*, str*/) {
|
309 | arr.push(UTIL.format.apply(UTIL, Array.prototype.slice.call(arguments, 1)));
|
310 | return arr;
|
311 | }
|
312 | });
|