Skip to content

Commit af36b0d

Browse files
committed
Add support for HTTP Daemon
1 parent b726c47 commit af36b0d

File tree

4 files changed

+322
-4
lines changed

4 files changed

+322
-4
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = {
22
FunctionParser: require('./lib/parser/function_parser.js'),
3+
Daemon: require('./lib/daemon.js'),
34
Gateway: require('./lib/gateway.js'),
45
types: require('./lib/types.js')
56
};

lib/daemon.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
const cluster = require('cluster');
2+
const os = require('os');
3+
const http = require('http');
4+
const fs = require('fs');
5+
6+
const Gateway = require('./gateway.js');
7+
8+
/**
9+
* Multi-process HTTP Daemon that resets when files changed (in development)
10+
* @class
11+
*/
12+
class Daemon {
13+
14+
constructor (name, cpus) {
15+
16+
this.name = name || 'HTTP';
17+
18+
this._watchers = null;
19+
20+
this._error = null;
21+
this._server = null;
22+
this._port = null;
23+
24+
this.cpus = parseInt(cpus) || os.cpus().length;
25+
this.children = [];
26+
27+
process.on('exit', (code) => {
28+
29+
console.log(`[${this.name}.Daemon] Shutdown: Exited with code ${code}`);
30+
31+
});
32+
33+
}
34+
35+
/**
36+
* Starts the Daemon. If all application services fail, will launch a
37+
* dummy error app on the port provided.
38+
* @param {Number} port
39+
*/
40+
start(port) {
41+
42+
this._port = port || 3000;
43+
44+
console.log(`[${this.name}.Daemon] Startup: Initializing`);
45+
46+
if ((process.env.NODE_ENV || 'development') === 'development') {
47+
48+
this.watch('', (changes) => {
49+
50+
changes.forEach(change => {
51+
console.log(`[${this.name}.Daemon] ${change.event[0].toUpperCase()}${change.event.substr(1)}: ${change.path}`);
52+
});
53+
54+
this.children.forEach(child => child.send({invalidate: true}));
55+
this.children = [];
56+
!this.children.length && this.unwatch() && this.start();
57+
58+
});
59+
60+
}
61+
62+
this._server && this._server.close();
63+
this._server = null;
64+
65+
for (var i = 0; i < this.cpus; i++) {
66+
67+
let child = cluster.fork();
68+
this.children.push(child);
69+
70+
child.on('message', this.message.bind(this));
71+
child.on('exit', this.exit.bind(this, child));
72+
73+
}
74+
75+
console.log(`[${this.name}.Daemon] Startup: Spawning HTTP Workers`);
76+
77+
}
78+
79+
/**
80+
* Daemon failed to load, set it in idle state (accept connections, give dummy response)
81+
*/
82+
idle() {
83+
84+
let port = this._port || 3000;
85+
86+
this._server = http
87+
.createServer((req, res) => {
88+
this.error(req, res, this._error);
89+
req.connection.destroy();
90+
})
91+
.listen(port);
92+
93+
console.log(`[${this.name}.Daemon] Idle: Unable to spawn HTTP Workers, listening on port ${port}`);
94+
95+
}
96+
97+
error(req, res, error) {
98+
99+
res.writeHead(500, {'Content-Type': 'text/plain'});
100+
res.end(`Application Error:\n${error.stack}`);
101+
102+
}
103+
104+
message(data) {
105+
106+
data.error && this.logError(data.error);
107+
108+
}
109+
110+
/**
111+
* Shut down a child process given a specific exit code. (Reboot if clean shutdown.)
112+
* @param {child_process} child
113+
* @param {Number} code Exit status codes
114+
*/
115+
exit(child, code) {
116+
117+
let index = this.children.indexOf(child);
118+
119+
if (index === -1) {
120+
return;
121+
}
122+
123+
this.children.splice(index, 1);
124+
125+
if (code === 0) {
126+
child = cluster.fork();
127+
this.children.push(child);
128+
child.on('message', this.message.bind(this));
129+
child.on('exit', this.exit.bind(this, child));
130+
}
131+
132+
if (this.children.length === 0) {
133+
this.idle();
134+
}
135+
136+
}
137+
138+
/**
139+
* Log an error on the Daemon
140+
* @param {Error} error
141+
*/
142+
logError(error) {
143+
144+
this._error = error;
145+
this._server = null;
146+
console.log(`[${this.name}.Daemon] ${error.name}: ${error.message}`);
147+
console.log(error.stack);
148+
149+
}
150+
151+
/**
152+
* Stops watching a directory tree for changes
153+
*/
154+
unwatch() {
155+
156+
clearInterval(this._watchers.interval);
157+
this._watchers = null;
158+
return true;
159+
160+
}
161+
162+
/**
163+
* Watches a directory tree for changes
164+
* @param {string} path Directory tree to watch
165+
* @param {function} onChange Method to be executed when a change is detected
166+
*/
167+
watch(path, onChange) {
168+
169+
function watchDir(cwd, dirname, watchers) {
170+
171+
if (!watchers) {
172+
173+
watchers = Object.create(null);
174+
watchers.directories = Object.create(null);
175+
watchers.interval = null;
176+
177+
}
178+
179+
let path = [cwd, dirname].join('');
180+
let files = fs.readdirSync(path);
181+
182+
watchers.directories[path] = Object.create(null);
183+
184+
files.forEach(function(v) {
185+
186+
if (v === 'node_modules' || v.indexOf('.') === 0) {
187+
return;
188+
}
189+
190+
let filename = [dirname, v].join('/');
191+
let fullPath = [cwd, filename].join('/');
192+
193+
let stat = fs.statSync(fullPath);
194+
195+
if (stat.isDirectory()) {
196+
watchDir(cwd, filename, watchers);
197+
return;
198+
}
199+
200+
watchers.directories[path][v] = stat;
201+
202+
});
203+
204+
return watchers;
205+
206+
}
207+
208+
let watchers = watchDir(process.cwd(), path || '');
209+
let self = this;
210+
211+
watchers.iterate = function(changes) {
212+
213+
if (changes.length) {
214+
onChange.call(self, changes);
215+
}
216+
217+
};
218+
219+
watchers.interval = setInterval(function() {
220+
221+
let changes = [];
222+
223+
Object.keys(watchers.directories).forEach(function(dirPath) {
224+
225+
let dir = watchers.directories[dirPath];
226+
let files = fs.readdirSync(dirPath);
227+
let added = [];
228+
229+
let contents = Object.create(null);
230+
231+
files.forEach(function(v) {
232+
233+
if (v === 'node_modules' || v.indexOf('.') === 0) {
234+
return;
235+
}
236+
237+
let fullPath = [dirPath, v].join('/');
238+
let stat = fs.statSync(fullPath);
239+
240+
if (stat.isDirectory()) {
241+
return;
242+
}
243+
244+
if (!dir[v]) {
245+
added.push([v, stat]);
246+
changes.push({event: 'added', path: fullPath});
247+
return;
248+
}
249+
250+
if (stat.mtime.toString() !== dir[v].mtime.toString()) {
251+
dir[v] = stat;
252+
changes.push({event: 'modified', path: fullPath});
253+
}
254+
255+
contents[v] = true;
256+
257+
});
258+
259+
Object.keys(dir).forEach(function(v) {
260+
261+
let fullPath = [dirPath, v].join('/');
262+
263+
if (!contents[v]) {
264+
delete dir[v];
265+
changes.push({event: 'removed', path: fullPath});
266+
}
267+
268+
});
269+
270+
added.forEach(function(v) {
271+
dir[v[0]] = v[1];
272+
});
273+
274+
});
275+
276+
watchers.iterate(changes);
277+
278+
}, 1000);
279+
280+
return this._watchers = watchers;
281+
282+
}
283+
284+
}
285+
286+
class FunctionScriptDaemon extends Daemon {
287+
constructor (cpus) {
288+
super('FunctionScript', cpus);
289+
}
290+
}
291+
292+
FunctionScriptDaemon.Gateway = class FunctionScriptDaemonGateway extends Gateway {
293+
294+
constructor (cfg) {
295+
super(cfg);
296+
process.on('uncaughtException', e => {
297+
process.send({
298+
error: {
299+
name: e.name,
300+
message: e.message,
301+
stack: e.stack
302+
}
303+
});
304+
process.exit(1);
305+
});
306+
process.on('message', data => data.invalidate && process.exit(0));
307+
process.on('exit', code => console.log(`${this.formatName(this.name)} Shutdown: Exited with code ${code}`));
308+
}
309+
310+
listen () {
311+
super.listen();
312+
process.send({message: 'ready'});
313+
}
314+
315+
}
316+
317+
module.exports = FunctionScriptDaemon;

lib/gateway.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const types = require('./types.js');
1010
const background = require('./background.js');
1111

1212
const DEFAULT_PORT = 8170;
13-
const DEFAULT_NAME = 'FunctionScript Gateway';
13+
const DEFAULT_NAME = 'FunctionScript.Gateway';
1414
const DEFAULT_MAX_REQUEST_SIZE_MB = 128;
1515

1616
class Gateway extends EventEmitter {
@@ -43,7 +43,7 @@ class Gateway extends EventEmitter {
4343
}
4444

4545
formatName (name) {
46-
return `[${name}]`;
46+
return `[${name}.${process.pid}]`;
4747
}
4848

4949
formatRequest (req) {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "functionscript",
3-
"version": "1.0.5",
3+
"version": "1.0.6",
44
"description": "A language and specification for turning JavaScript functions into typed HTTP APIs",
55
"author": "Keith Horwood <keithwhor@gmail.com>",
66
"main": "index.js",
@@ -17,6 +17,6 @@
1717
"devDependencies": {
1818
"chai": "^3.5.0",
1919
"form-data": "^2.3.2",
20-
"mocha": "^3.2.0"
20+
"mocha": "^6.1.4"
2121
}
2222
}

0 commit comments

Comments
 (0)