UNPKG

21.1 kBJavaScriptView Raw
1
2/**
3 * sgmore!
4 *
5 * More functions for SG, but bigger and more complex than those in sg.js.
6 */
7
8exports.load = function(sg, _) {
9
10 var path = require('path');
11 var spawn = require('child_process').spawn;
12
13 var fs = sg.extlibs.fs = require('fs-extra');
14 var glob = sg.extlibs.glob = require('glob');
15
16 var verbose = sg.verbose;
17
18 var dogStats;
19
20 var dogStatsClosure = function() {
21 var server = process.env.SG_STATSD_SERVER || process.env.STATSD_SERVER || process.env.DOG_STATSD_SERVER || 'localhost';
22 try {
23 var dogStatsD = require('dogstatsd').StatsD;
24 dogStats = new dogStatsD(server, 8125);
25 } catch(e) {}
26 };
27 dogStatsClosure();
28
29 sg.StatsD = function() {
30 var self = this;
31
32 self.increment = function(name, value, tags) {
33 if (!dogStats) { return; }
34 var args = parseParams.apply(self, arguments);
35
36 if ('value' in args) { dogStats.incrementBy(args.name, args.value, dogTags(args.tags)); }
37 else { dogStats.increment(args.name, dogTags(args.tags)); }
38 };
39
40 self.decrement = function(name, value, tags) {
41 if (!dogStats) { return; }
42 var args = parseParams.apply(self, arguments);
43
44 if ('value' in args) { dogStats.decrementBy(args.name, args.value, dogTags(args.tags)); }
45 else { dogStats.decrement(args.name, dogTags(args.tags)); }
46 };
47
48 self.gauge = function(name, value, tags) {
49 if (!dogStats) { return; }
50 var args = parseParams.apply(self, arguments);
51
52 if ('value' in args) { dogStats.gauge(args.name, args.value, dogTags(args.tags)); }
53 else { dogStats.gauge(args.name, dogTags(args.tags)); }
54 };
55
56 self.histogram = function(name, value, tags) {
57 if (!dogStats) { return; }
58 var args = parseParams.apply(self, arguments);
59
60 dogStats.histogram(args.name, args.value, dogTags(args.tags));
61 };
62
63 self.timing = function(name, value, tags) {
64 if (!dogStats) { return; }
65 var args = parseParams.apply(self, arguments);
66
67 dogStats.timing(args.name, args.value, dogTags(args.tags));
68 };
69
70 self.set = function(name, value, tags) {
71 if (!dogStats) { return; }
72 var args = parseParams.apply(self, arguments);
73
74 if ('value' in args) { dogStats.set(args.name, args.value, dogTags(args.tags)); }
75 else { dogStats.set(args.name, dogTags(args.tags)); }
76 };
77
78 var parseParams = function(name /*, value, tags*/) {
79 var args = _.rest(arguments);
80 var tags = _.isArray(_.last(args)) ? args.pop() : null;
81 var value = args.shift();
82
83 var result = { name : name };
84 if (value) { result.value = value; }
85 if (tags) { result.tags = tags; }
86
87 return result;
88 };
89
90 var dogTags = function(userTags) {
91 var result = [];
92 if (userTags) {
93 result = result.concat(userTags);
94 }
95
96 _.each(userTags, function(value, key) {
97 if (value === true) {
98 result.push(key);
99 } else {
100 result.push(key + ':' + value);
101 }
102 });
103
104 //_.each(initTags, function(value, key) {
105 // result.push(key + ':' + value);
106 //});
107
108 //_.each(keyValueTags, function(value, key) {
109 // result.push(key + ':' + value);
110 //});
111
112 //_.each(tags, function(value, key) {
113 // result.push(key);
114 //});
115
116 return result;
117 };
118 };
119
120
121 var stringForHttpCode = function(code) {
122 // First, specific codes
123 if (code === 301) { return 'Moved Permanently'; }
124 else if (code === 302) { return 'Found'; }
125 else if (code === 304) { return 'Not Modified'; }
126 else if (code === 400) { return 'Bad Request'; }
127 else if (code === 401) { return 'Unauthorized'; }
128 else if (code === 403) { return 'Permission Denied'; }
129 else if (code === 404) { return 'Not Found'; }
130 else if (code === 418) { return "I'm a teapot"; }
131 else if (code === 429) { return 'Too Many Requests'; }
132 else if (code === 500) { return 'Internal Error'; }
133 else if (code === 501) { return 'Not Implemented'; }
134 else if (code === 521) { return 'Web server is down'; }
135
136 // Ranges
137 else if (200 <= code && code < 300) { return 'OK'; }
138 else if (300 <= code && code < 400) { return 'Follow Location'; }
139 else if (400 <= code && code < 500) { return 'Client Error'; }
140 else if (code < 600) { return 'Server Error'; }
141 return '';
142 };
143
144 var mkResponseObject = function(code, str) {
145
146 if (code === 301) { return { Location: str}; }
147 else if (code === 302) { return { Location: str}; }
148
149 return {msg: str};
150 };
151
152 sg.jsonResponse = function(req, res, code, content_, headers_, debugInfo_) {
153 var content = content_ || {code: code, msg: stringForHttpCode(code)};
154 var headers = headers_ || {};
155 var debugInfo = null;
156
157 if (process.env.NODE_ENV !== 'production') {
158 if (code >= 400) {
159 debugInfo = debugInfo_ || {};
160 }
161 }
162
163 // No debug for any permission denied code
164 if (code === 401 || code === 403) {
165 debugInfo = null;
166 }
167
168 // If debugInfo_ came in as null, make sure no debug info
169 if (debugInfo_ === null) {
170 debugInfo = null;
171 }
172
173 if (_.isString(content)) {
174 content = mkResponseObject(code, content);
175 }
176
177 // Remember the passed-in content
178 var origContent = sg.extend(content);
179
180 if (_.isObject(content)) {
181 if (debugInfo) {
182 content.debug = debugInfo;
183 }
184
185 content = JSON.stringify(content);
186 }
187
188 headers['Content-Type'] = headers['Content-Type'] || 'application/json';
189 headers['Content-Length'] = content.length;
190
191 if ((code === 301 || code === 302) && origContent.Location) {
192 headers.Location = origContent.Location;
193 }
194
195 res.writeHead(code, headers);
196 res.write(content);
197 return res.end();
198 };
199
200 sg._200 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 200, content, headers, debugInfo); };
201 sg._301 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 301, content, headers, debugInfo); };
202 sg._302 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 302, content, headers, debugInfo); };
203 sg._304 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 304, content, headers, debugInfo); };
204 sg._400 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 400, content, headers, debugInfo); };
205 sg._401 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 401, content, headers, debugInfo); };
206 sg._403 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 403, content, headers, debugInfo); };
207 sg._404 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 404, content, headers, debugInfo); };
208 sg._418 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 418, content, headers, debugInfo); };
209 sg._429 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 429, content, headers, debugInfo); };
210 sg._500 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 500, content, headers, debugInfo); };
211 sg._501 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 501, content, headers, debugInfo); };
212 sg._521 = function(req, res, content, debugInfo, headers) { return sg.jsonResponse(req, res, 521, content, headers, debugInfo); };
213
214 sg.sendResponseChunks = function(req, res, contentType, chunks) {
215
216 var headers = {};
217
218 headers['Content-Type'] = contentType;
219
220 res.writeHead(200, headers);
221 _.each(chunks, function(chunk) {
222 res.write(chunk);
223 });
224 return res.end();
225 };
226
227 var findFiles = sg.findFiles = function(pattern, options, callback) {
228 return glob(pattern, options, function(err, filenames_) {
229 if (err) { return callback(err); }
230
231 /* otherwise */
232 var filenames = [];
233 return __eachll(filenames_, function(filename, next) {
234 return fs.stat(filename, function(err, stats) {
235 if (!err && stats.isFile()) {
236 filenames.push(filename);
237 }
238 return next();
239 });
240
241 }, function(errs) {
242 return callback(null, filenames);
243 });
244 });
245 };
246
247 var eachLine = sg.eachLine = function(pattern, options_, eachCallback, finalCallback) {
248 var options = _.defaults({}, options_ || {}, {cwd: process.cwd()});
249 var total = 0;
250 var files = [];
251
252 var eachLineOfOneFile = function(filename, next) {
253 return fs.readFile(filename, 'utf8', function(err, contents) {
254 if (err) { return next(err); }
255
256 var lines = contents.split('\n');
257 if (options.lineFilter) {
258 lines = _.filter(lines, options.lineFilter);
259 }
260
261 var i = 0, l = lines.length;
262 var oneLine = function() {
263 total++;
264
265 var result = eachCallback(lines[i], i, filename, total);
266 if (result === 'SG.nextFile') {
267 return next();
268 }
269
270 if (result === 'SG.done') {
271 return finalCallback(null, total, files.length);
272 }
273
274 i += 1;
275 if (i < l) {
276 if (result === 'SG.breathe') {
277 //console.log('Breathing');
278 return setTimeout(oneLine, 500);
279 }
280
281 if (i % 200 === 0) {
282 return process.nextTick(oneLine);
283 }
284
285 return oneLine();
286 }
287
288 return next();
289 };
290
291 return oneLine();
292 });
293 };
294
295 // Is this a glob?
296 if (!/\*/.exec(pattern)) {
297 // No, not a glob
298 return eachLineOfOneFile(arguments[0], function(err) {
299 return finalCallback(err);
300 });
301 }
302
303 /* otherwise */
304 options.filenameFilter = options.filenameFilter || function(){return true;};
305
306 return glob(pattern, options, function(err, files_) {
307 if (err) { return finalCallback(err); }
308
309 files = files_;
310 return sg.__each(files, function(filename_, next) {
311 var filename = path.join(options.cwd || '', filename_);
312
313 return fs.stat(filename, function(err, stats) {
314 if (err) { return next(); }
315 if (!stats.isFile()) { return next(); }
316
317 if (!options.filenameFilter(filename)) { return next(); }
318
319 return eachLineOfOneFile(filename, next);
320 });
321
322 }, function() {
323 return finalCallback();
324 });
325 });
326 };
327
328 sg.spawnEz = function(command, args, options) {
329 var stderr = sg.extract(options, 'stderr');
330 var stdout = sg.extract(options, 'stdout');
331 var close = sg.extract(options, 'close');
332 var newline = sg.extract(options, 'newline');
333
334 var proc, errRemainder = '', outRemainder = '', streamOptions = {};
335
336 //verbose(0, "---------------------------------------- spawnEzing ----------------------------------", command, args, options);
337 if (_.keys(options).length > 0) {
338 proc = spawn(command, args, options);
339 } else {
340 proc = spawn(command, args);
341 }
342
343 if (newline) {
344 streamOptions.newline = newline;
345 }
346
347 if (stderr) {
348 proc.stderr.on('data', function(chunk) {
349 errRemainder = sg.str2lines(errRemainder, chunk, streamOptions, stderr);
350 });
351 }
352
353 if (stdout) {
354 proc.stdout.on('data', function(chunk) {
355 outRemainder = sg.str2lines(outRemainder, chunk, streamOptions, stdout);
356 });
357 }
358
359 if (close) {
360 proc.on('close', close);
361 }
362 };
363
364 /**
365 * Invoke an external command.
366 *
367 * A lot like exec (just have one callback), but with all the goodness
368 * of spawn. The callback also returns all the info that spawn can.
369 *
370 * @alias module:sgsg.exec
371 *
372 * @param {string} cmd The external command to run.
373 * @param {array} args Arguments to the command
374 * @param {object} [options] options that will get passed to spawn.
375 * @param {function} callback The callback will be called when the command has completed.
376 *
377 * callback(error, exitCode, stdoutChunks, stderrChunks, signal)
378 */
379 sg.exec = function(cmd, args /*, options, callback*/) {
380 var args_ = _.rest(arguments, 2);
381 var callback = args_.pop() || lib.noop;
382 var options = args_.pop() || null;
383
384 var encoding = 'utf8';
385 if (options && 'encoding' in options) {
386 encoding = sg.extract(options, 'encoding');
387 }
388
389 var proc, error, exitCode, stdoutChunks = [], stderrChunks = [], signal;
390
391 if (options !== null) {
392 proc = spawn(cmd, args, options);
393 } else {
394 proc = spawn(cmd, args);
395 }
396
397 proc.stdout.setEncoding(encoding);
398 proc.stdout.on('data', function(chunk) {
399 stdoutChunks.push(chunk);
400 });
401
402 proc.stderr.setEncoding('utf8');
403 proc.stderr.on('data', function(chunk) {
404 stderrChunks.push(chunk);
405 });
406
407 // We must guard against calling the callback multiple times
408 var finalFunctionHasBeenCalled = false, closeTimer = null;
409 var finalFunction = function(which) {
410 if (finalFunctionHasBeenCalled) { return; }
411
412 if (which === 'close') {
413 finalFunctionHasBeenCalled = true;
414 if (closeTimer) {
415 clearTimeout(closeTimer);
416 }
417 return callback(error, exitCode, stdoutChunks, stderrChunks, signal);
418 }
419
420 // A close may arrive... give it a few moments
421 return closeTimer = setTimeout(function() {
422 if (finalFunctionHasBeenCalled) { return; }
423 finalFunctionHasBeenCalled = true;
424 return callback(error, exitCode, stdoutChunks, stderrChunks, signal);
425 }, 1000);
426 };
427
428 proc.on('close', function(exitCode_, signal_) {
429 exitCode = exitCode_;
430 signal = signal_;
431 return finalFunction('close');
432 });
433
434 proc.on('error', function(err) {
435 error = err;
436 return finalFunction('error');
437 });
438
439 proc.on('exit', function(exitCode_, signal_) {
440 exitCode = exitCode_;
441 signal = signal_;
442 return finalFunction('exit');
443 });
444
445 };
446
447 /**
448 * A wrapper for sg.exec() with a simple callback signature:
449 *
450 * * callback(err, stdoutLines)
451 *
452 * * If the spawned process had an error of any kind, err will be info
453 * on the error.
454 * * stdoutLines will always be the stdout, split into lines.
455 */
456 sg.execEz = function(cmd, args /*, options, callback*/) {
457 var args_ = _.rest(arguments, 2);
458 var callback = args_.pop() || lib.noop;
459 var options = args_.pop() || null;
460 var ezOptions = options || {};
461
462 return sg.exec(cmd, args, options, function(error, exitCode, stdoutChunks, stderrChunks, signal) {
463 var stdout = (stdoutChunks || []).join('').split('\n');
464 var stderr = (stderrChunks || []).join('');
465 var err = {};
466
467 if (error) { err.error = error; }
468 if (signal) { err.signal = signal; }
469 if (exitCode !== 0) { err.exitCode = exitCode; }
470 if (stderr.length > 0) { err.stderr = stderr; }
471
472 if (sg.numKeys(err) !== 0) {
473 if (!ezOptions.quiet) {
474 sg.reportOutput(cmd+args.join(" "), error, exitCode, stdoutChunks, stderrChunks, signal);
475 }
476 } else {
477 err = null;
478 }
479
480 return callback(err, stdout);
481 });
482 };
483
484 /**
485 * Makes a nice, usually compact, reporting of what sg.exec() outputs.
486 *
487 * This function is intended to be used when you call sg.exec() on a utility / program
488 * that produces a small amount of output. It works great for short bash scripts that
489 * you write and invoke from your Node.js code -- when they are written to output a
490 * short and sweet success message, or to send error info on stderr.
491 *
492 * * Gives detailed info on any error information.
493 * * Tries to condense stdout (if it is only one line, combines it.)
494 *
495 * Returns a 2-tuple: [err, stdout-as-one-big-string]
496 *
497 */
498 sg.reportOutput = function(msg_, error, exitCode, stdoutChunks, stderrChunks, signal) {
499
500 const stdoutLines = _.compact(stdoutChunks.join('').split('\n'));
501
502 if (msg_ !== null) {
503 const msg = sg.lpad(msg_+':', 50);
504 const stderrLines = _.compact(stderrChunks.join('').split('\n'));
505
506 // Write the final status (exitCode, signal) first.
507 if (stdoutLines.length === 1) {
508 console.log(`${msg} exit: ${exitCode}, SIGNAL: ${signal}: ${stdoutLines[0]}`);
509 } else {
510 console.log(`${msg} exit: ${exitCode}, SIGNAL: ${signal}`);
511 }
512
513 // Then write any error objects
514 if (error) {
515 console.error(error);
516 }
517
518 // Then, write full stdout
519 if (stdoutLines.length === 1) {
520 } else {
521 _.each(stdoutLines, line => {
522 console.log(`${msg} ${line}`);
523 });
524 }
525
526 // Then write full stderr
527 const stderr = stderrChunks.join('');
528 if (stderr.length > 0) {
529 _.each(stderrLines, line => {
530 console.error(`${msg} ${line}`);
531 });
532 }
533 }
534
535 var err = (exitCode !== 0 ? `NONZEROEXIT:${exitCode}` : (signal ? `SIG${signal}` : error));
536 return [err, stdoutLines];
537 };
538
539 /**
540 *
541 */
542 sg.getMacAddress = function(callback) {
543 if (sg.macAddress_) { return callback(null, sg.macAddress_); }
544
545 var currInterface, ifaceStats, currIfaceStats = {}, m, value;
546 return sg.execEz('ifconfig', [], {}, function(err, lines) {
547 _.each(lines, function(line, lineNum) {
548 if ((m = line.match(/^([a-z0-9]+):\s*(.*)$/i))) {
549 // Are we starting a new section for the interface?
550 if (currInterface) {
551 // We are on a line that starts an interface, but we are still remembering the previous interface
552 analyzeStats();
553 }
554
555 currInterface = m[1];
556 currIfaceStats = {ifaceName: currInterface};
557 line = m[2];
558 }
559
560 // Some options are [key: value]; some are [key value]; some are [key=value]
561 if ((m = line.match(/([a-z0-9_]+)(:|=)?\s*(.*)$/i))) {
562 value = m[3];
563 if (m[1] === 'ether') {
564 value = value.replace(/\s*/g, '');
565 }
566 currIfaceStats[m[1]] = value;
567 }
568 });
569
570 analyzeStats();
571 sg.macAddress_ = ifaceStats.ether || sg.macAddress_;
572
573 return callback(null, sg.macAddress_);
574 });
575
576 function analyzeStats() {
577 if (currIfaceStats.status !== 'active') { return; }
578 if (!currIfaceStats.inet) { return; }
579 if (!currIfaceStats.ether.match(/^([a-f0-9]{2}[:]){5}[a-f0-9]{2}$/i)) { return; }
580
581 // Is our new stats better than one we already have?
582 if (!ifaceStats) {
583 ifaceStats = sg.deepCopy(currIfaceStats);
584 } else if (currIfaceStats.ether > ifaceStats.ether) {
585 ifaceStats = sg.deepCopy(currIfaceStats);
586 }
587 }
588 };
589 sg.macAddress_ = null;
590
591 /**
592 *
593 */
594 sg.getHardwareId = function(callback) {
595 return sg.getMacAddress(callback);
596 };
597
598 /**
599 * Writes the array of chunks to a file.
600 *
601 * @alias module:sgsg.writeChunks
602 *
603 * @param {string} path The chunks will be written to the file named by path.
604 * @param {array} chunks The array of chunks to be written.
605 */
606 sg.writeChunks = function(path, chunks, callback) {
607
608 var stream = fs.createWriteStream(path);
609 var i = 0;
610
611 // This write function is taken from the Node.js web site.
612 write();
613 function write() {
614 var ok = true;
615 do {
616
617 if (i < chunks.length - 1) {
618 // see if we should continue, or wait
619 // don't pass the callback, because we're not done yet.
620 ok = stream.write(chunks[i]);
621 } else {
622 // last time!
623 stream.write(chunks[i], callback);
624 }
625
626 i += 1;
627 } while (i < chunks.length && ok);
628
629 if (i < chunks.length) {
630 // had to stop early!
631 // write some more once it drains
632 stream.once('drain', write);
633 }
634 }
635 };
636
637 var defAttr = function(obj, key, def_) {
638 var defValue = def_ || {};
639 obj[key] = obj[key] || defValue;
640 return obj[key];
641 };
642
643 var incAttr = function(obj, key) {
644 obj[key] = (obj[key] || 0) + 1;
645 return obj[key];
646 };
647
648 sg.DataHouse = function() {
649 var self = this;
650 self.data = {};
651
652 self.multiKeyCount = function(key, value, mfg) {
653 var args = _.toArray(arguments);
654 var last = args.pop();
655 var obj = self.data;
656
657 _.each(args, function(arg) {
658 obj = defAttr(obj, arg);
659 });
660
661 return incAttr(obj, last);
662 };
663 };
664
665
666
667 return sg;
668};