1 | var net = require('net');
|
2 | var spawn = require('child_process').spawn;
|
3 |
|
4 | var through2 = require('through2');
|
5 | var Promise = require('bluebird');
|
6 | var es = require('event-stream');
|
7 | var shellEscape = require('./shellEscape');
|
8 |
|
9 | var winston = require('winston');
|
10 |
|
11 | module.exports = function(logFile, options){
|
12 | return new Runner(logFile, options);
|
13 | };
|
14 |
|
15 | module.exports.remoteStream = function(){
|
16 | var input = es.stringify();
|
17 | var connection = net.connect.apply(net, arguments);
|
18 | var output = input.pipe(connection)
|
19 | .pipe(es.split())
|
20 | .pipe(es.parse());
|
21 |
|
22 | connection.on('error', output.emit.bind(output, 'error'));
|
23 |
|
24 | return es.duplex(input, output);
|
25 | };
|
26 |
|
27 | function Runner(logFile, options){
|
28 | this.logFile = logFile;
|
29 | this.options = options || {};
|
30 |
|
31 | this.logger = new (winston.Logger)({
|
32 | transports: [
|
33 | new (winston.transports.Console)(),
|
34 | new (winston.transports.File)({ filename: logFile })
|
35 | ]
|
36 | });
|
37 |
|
38 | this.clients = [];
|
39 | return net.createServer(this.onConnection.bind(this));
|
40 | }
|
41 |
|
42 | Runner.prototype.onConnection = function(socket){
|
43 | this.clients.push(socket);
|
44 |
|
45 | var self = this;
|
46 |
|
47 | var logger = this.logger;
|
48 |
|
49 | socket.on('end', function(){
|
50 | self.clients.splice(self.clients.indexOf(socket), 1);
|
51 | });
|
52 |
|
53 | var bash = spawn('bash', [], this.options);
|
54 |
|
55 | var inputLogs = [];
|
56 | var bashLogs = [];
|
57 | bash.stdout.on('data', function(data){
|
58 | bashLogs.push(data.toString());
|
59 | logger.info('stdout: '+data.toString());
|
60 | });
|
61 | bash.stderr.on('data', function(data){
|
62 | bashLogs.push(data.toString());
|
63 | logger.error(data.toString());
|
64 | });
|
65 |
|
66 | var validator = this.validate();
|
67 | var responder = es.stringify();
|
68 |
|
69 | socket
|
70 | .pipe(es.split())
|
71 | .pipe(es.parse())
|
72 | .pipe(through2.obj(function(chunk, enc, cb){
|
73 | if (typeof chunk === 'object' && chunk.end === true) {
|
74 | logger.info('ending bash stdin');
|
75 | bash.stdin.end();
|
76 | } else {
|
77 | this.push(chunk);
|
78 | }
|
79 | cb();
|
80 | }))
|
81 | .pipe(validator)
|
82 | .pipe(this.stringify())
|
83 | .pipe(through2.obj(function(line, enc, cb){
|
84 | inputLogs.push(line);
|
85 | logger.info('executing: '+line);
|
86 | cb(null, line);
|
87 | }))
|
88 | .pipe(bash.stdin);
|
89 |
|
90 | responder
|
91 | .pipe(through2.obj(function(chunk, enc, cb){
|
92 | logger.info('responding', chunk);
|
93 | cb(null, chunk);
|
94 | }))
|
95 | .pipe(socket);
|
96 |
|
97 | validator.on('error', function(err){
|
98 | responder.write({
|
99 | status: 'error',
|
100 | error: err.stack || err
|
101 | });
|
102 | });
|
103 |
|
104 | bash.on('exit', function(code, signal){
|
105 | var message = 'bash closed with code '+code+', signal '+signal;
|
106 | if (code === 0) {
|
107 | logger.info(message);
|
108 | responder.write({
|
109 | status: 'success',
|
110 | logs: bashLogs.join('\n'),
|
111 | input: inputLogs.join('')
|
112 | });
|
113 | } else {
|
114 | logger.error(message);
|
115 | responder.write({
|
116 | status: 'error',
|
117 | code: code,
|
118 | signal: signal,
|
119 | logs: bashLogs.join('\n'),
|
120 | input: inputLogs.join('')
|
121 | });
|
122 | }
|
123 | logger.close();
|
124 | responder.end();
|
125 | });
|
126 | };
|
127 |
|
128 | Runner.prototype.stringify = function(){
|
129 | return through2.obj(function(chunk, enc, callback){
|
130 | var out = shellEscape(chunk)+';\n';
|
131 | callback(null, out);
|
132 | });
|
133 | };
|
134 | Runner.prototype.validate = function(){
|
135 | return through2.obj(function(chunk, enc, callback){
|
136 | if (Array.isArray(chunk)) {
|
137 | callback(null, chunk);
|
138 | } else {
|
139 | callback('invalid: not an array');
|
140 | }
|
141 | });
|
142 | };
|