UNPKG

11.1 kBJavaScriptView Raw
1/**
2 * Copyright 2014 Skytap Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 **/
16
17var cluster = require('cluster'),
18 fs = require('fs'),
19 path = require('path'),
20 Express = require('express'),
21 _ = require('underscore'),
22 cookieParser = require('cookie-parser'),
23 errorHandler = require('errorhandler'),
24 compression = require('compression'),
25 staticFavicon = require('serve-favicon'),
26 bodyParser = require('body-parser'),
27 Promise = require('bluebird'),
28 Backhoe = require('backhoe'),
29 Config = require('./config'),
30 Controller = require('./controller'),
31 Environment = require('./environment'),
32 Router = require('./router'),
33 Logger = require('./logger'),
34 Template = require('./template'),
35 Filter = require('./filter'),
36 Path = require('./path'),
37 FileSystem = require('./filesystem'),
38 jadePlugin = require('./minorjs_template_jade');
39
40/**
41 * Module for creating an HTTP server.
42 **/
43var Minor = function(options) {
44 this.app = Express();
45 this.options = options;
46 this.closing = false;
47
48 // bind functions
49 this.listen = _.bind(this.listen, this);
50 this.initialize = _.bind(this.initialize, this);
51
52 if (!this.options.instance) {
53 this.options.instance = 1;
54 }
55
56 if (!this.options.controllerTimeout) {
57 //default timeout 3 minutes
58 this.options.controllerTimeout = 180000;
59 }
60
61 if (!this.options.hostname) {
62 this.options.hostname = '0.0.0.0';
63 }
64
65 this._initializeEnvironment();
66 this._initializeErrorHandling();
67};
68
69_.extend(Minor.prototype, {
70
71 //////////////////////////////////////////////////////////////////////
72 // Public methods ///////////////////////////////////////////////////
73 ////////////////////////////////////////////////////////////////////
74
75 /**
76 * Minor.initialize()
77 **/
78 initialize : function() {
79 var start = Date.now(),
80 defaultPromise,
81 environmentPromise;
82
83 this.start = start;
84
85 this.app.use(cookieParser());
86
87 defaultPromise = this._initializeDefaultConfiguration();
88
89 environmentPromise = this._isProduction()
90 ? this._initializeProductionConfig()
91 : this._initializeDevelopmentConfig();
92
93 // listen for a shutdown message
94 process.on('message', this._handleMessage.bind(this));
95
96 return Promise.all([defaultPromise, environmentPromise])
97 .then(function () {
98 Logger.profile('Initialize MinorJS', start);
99 });
100 },
101
102 /**
103 * Minor.listen() -> Object
104 **/
105 listen : function() {
106 var self = this,
107 start = Date.now();
108
109 return Router.load(this.app, this.options.basePath + "/lib/controllers", this.options)
110 .then(function startExpressListening() {
111 Logger.profile('Load controllers', start);
112
113 var port = self._getPort();
114 self.server = self.app.listen(port, self.options.hostname);
115
116 Logger.profile('Load MinorJS', self.start);
117
118 Logger.info("MinorJS HTTP server listening on " + self.options.hostname + ":" + port);
119 })
120 .catch(function (error) {
121 console.error('Error loading controllers and registering routes.\n\n', error.stack);
122 process.exit(1);
123 });
124 },
125
126 //////////////////////////////////////////////////////////////////////
127 // Psuedo-private methods ///////////////////////////////////////////
128 ////////////////////////////////////////////////////////////////////
129
130 /**
131 * Minor._getEnvironment() -> String
132 **/
133 _getEnvironment : function () {
134 return typeof process.env.NODE_ENV === 'undefined'
135 ? 'development'
136 : process.env.NODE_ENV;
137 },
138
139 /**
140 * Minor._getPort()
141 **/
142 _getPort : function() {
143 if (this.options.port) {
144 return this.options.port;
145 }
146
147 return Config.get('port');
148 },
149
150 /**
151 * Minor._initializeDefaultConfiguration()
152 **/
153 _initializeDefaultConfiguration : function() {
154 var self = this,
155 start = Date.now();
156
157 return Promise.resolve()
158 .then(function () {
159 self.app.use(compression());
160
161 self.app.set('views', self.options.basePath + '/lib/templates');
162
163 self.app.locals.escapeAttributes = true;
164
165 if (Array.isArray(self.options.templatePlugins)) {
166 for (var index in self.options.templatePlugins) {
167 var plugin = self.options.templatePlugins[index];
168 plugin.register(self.app);
169 }
170 } else {
171 jadePlugin.register(self.app);
172 }
173 })
174 .then(function () {
175 // load all the template mixins
176 return Template.loadMixins(self.options.basePath + '/lib/template_mixins');
177 })
178 .then(function () {
179 // load all the filters
180 return Filter.load(self.options.basePath + '/lib/filters');
181 })
182 .then(function () {
183 var faviconPath = self.options.basePath + '/public/favicon.ico';
184
185 if (fs.existsSync(faviconPath)) {
186 self.app.use(staticFavicon(faviconPath));
187 }
188
189 // parse application/x-www-form-urlencoded
190 self.app.use(bodyParser.urlencoded({ extended: false }));
191
192 // parse application/json
193 self.app.use(bodyParser.json());
194 })
195 .then(function () {
196 Logger.profile('Initialize default configuration', start);
197 });
198 },
199
200 /**
201 * Minor._initializeDevelopmentConfig()
202 **/
203 _initializeDevelopmentConfig : function() {
204 var self = this,
205 start = Date.now(),
206 noCacheDefaults = ['lib/controllers',
207 'lib/controller_helpers',
208 'lib/views',
209 'lib/templates'];
210
211 return Promise.resolve()
212 .then(function() {
213 var noCachePaths = self.options.noCache || noCacheDefaults;
214 Backhoe.noCache(
215 Environment.getBasePath(),
216 noCachePaths
217 );
218
219 // pretty print the HTML
220 self.app.locals.uglify = false;
221
222 self.app.use(
223 errorHandler({
224 dumpExceptions : true,
225 showStack : true
226 })
227 );
228
229 return self._loadMiddleware('development');
230 })
231 .then(function () {
232 Logger.profile('Initialize development configuration', start);
233 });
234 },
235
236 /**
237 * Minor._initializeProductionConfig()
238 **/
239 _initializeProductionConfig : function() {
240 var self = this,
241 start = Date.now();
242
243 return Promise.resolve()
244 .then(function() {
245 self.app.locals.uglify = true;
246 return self._loadMiddleware('production');
247 })
248 .then(function () {
249 Logger.profile('Initialize production configuration', start);
250 });
251 },
252
253 /**
254 * Minor._initializeEnvironment()
255 **/
256 _initializeEnvironment : function() {
257 // Initialize the environment and configs
258 Environment.initialize({
259 basePath : this.options.basePath,
260 environment : this.app.get('env'),
261 instance : this.options.instance,
262 loggers : this.options.loggers || [],
263 contextName : this.options.contextName
264 });
265 },
266
267 /**
268 * Minor._initializeErrorHandling()
269 **/
270 _initializeErrorHandling : function () {
271 process.on('uncaughtException', function handleException (error) {
272 console.log(error.stack)
273
274 Logger.error(error.stack);
275
276 if (Environment.isWorker()) {
277 cluster.worker.disconnect();
278 } else {
279 process.exit(1);
280 }
281 });
282
283 this.app.on('error', function handleError (error) {
284 Logger.error(error.stack);
285 });
286 },
287
288 /**
289 * Minor._isProduction() -> Boolean
290 **/
291 _isProduction : function () {
292 return this._getEnvironment() === 'production';
293 },
294
295 /**
296 * Minor._loadMiddleware()
297 *
298 * Load and register middleware.
299 **/
300 _loadMiddleware : function (environment) {
301 var self = this,
302 startMiddleware = Date.now(),
303 middlewarePath = path.join(this.options.basePath, 'lib', 'middleware');
304
305 if (!this.options.middleware || !this.options.middleware[environment]) {
306 return;
307 }
308
309 this.options.middleware[environment].forEach(function loadFile (file) {
310 var start = Date.now(),
311 middleware = require(path.join(middlewarePath, file));
312 middleware.process(self.app);
313 Logger.profile('Load middleware ' + file, start);
314 });
315
316 Logger.profile('Load middleware', startMiddleware);
317
318 return Promise.resolve();
319 },
320
321 /**
322 * Minor._handleMessage(message)
323 **/
324 _handleMessage : function (message) {
325 switch (message) {
326 case 'shutdown':
327 this._shutdown();
328 break;
329 }
330 },
331
332 /**
333 * Minor._shutdown()
334 **/
335 _shutdown : function () {
336 if (this.closing === true) {
337 // already shutting down the worker
338 return;
339 }
340
341 this.closing = true;
342
343 // stop accepting HTTP connections
344 this.server.close(function () {
345 // shut down the worker. the cluster manager will spawn a replacement worker.
346 cluster.worker.disconnect();
347 });
348 }
349});
350
351module.exports = Minor;
352
353// expose modules
354module.exports.Config = Config;
355module.exports.Controller = Controller;
356module.exports.Environment = Environment;
357module.exports.FileSystem = FileSystem;
358module.exports.Logger = Logger;
359module.exports.Path = Path;