-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjavaBox.js
356 lines (274 loc) · 11.5 KB
/
javaBox.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
const EventEmitter = require('events');
const Docker = require('dockerode');
const concat = require('concat-stream');
/**
* Docker object, connected on /var/run/docker.socket or default localhost docker port.
* @type {Docker}
*/
const docker = new Docker();
/**
* Time to wait between checking that the command has been executed (milliseconds).
* @type {number}
*/
const EXEC_WAIT_TIME_MS = 250;
/**
* JavaBox eventEmitter.
* @type {EventEmitter}
*/
const javaBox = new EventEmitter();
/**
* Initialising javaBox.
*/
javaBox.on('runJava', runJava);
javaBox.on('runJunit', runJunit);
/**
* Creates a docker container with newly created execution
* to run the Main.java specified inside the tar.
*
* @param messageId {String}: id of the given message/request.
* @param main {String}: entry point class name.
* @param tarBuffer {String}: the buffer of the tar containing java files
* @param timeLimitCompileMs
* @param timeLimitExecutionMs
*/
function runJava(messageId, main, tarBuffer, timeLimitCompileMs, timeLimitExecutionMs) {
const className = main.split('.')[0];
const javacCmd = ['javac', '-cp', 'home', `home/${main}`];
const javaCmd = ['java', '-Djava.security.manager', '-cp', 'home', className];
const sourceLocation = tarBuffer;
const execution = dockerCommand(javacCmd, timeLimitCompileMs, dockerCommand(javaCmd, timeLimitExecutionMs));
createJContainer(messageId, sourceLocation, false, execution);
}
/**
* Creates a docker container with newly created execution
* to run the tests on files to test specified inside the tar.
*
* @param messageId {String}: id of the given message/request.
* @param junitFileNames [String]: array of different junit tests filename.
* @param tarBuffer {String}: the buffer of the tar containing java files (both test and testing).
* @param timeLimitCompileMs
* @param timeLimitExecutionMs
*/
function runJunit(messageId, junitFileNames, tarBuffer, timeLimitCompileMs, timeLimitExecutionMs) {
let junitFiles = [];
junitFileNames.forEach((f)=>{
junitFiles.push( f.name.split('.')[0]);
});
const className = 'TestRunner';
const javacCmd = ['javac', '-cp', 'home:libs/junit-4.12:libs/hamcrest-core-1.3:libs/json-simple-1.1.1'];
let javaCmd = ['java', '-cp', 'home:libs/junit-4.12:libs/hamcrest-core-1.3:libs/json-simple-1.1.1', className];
junitFiles.forEach((file)=>{
javacCmd.push('home/' + file + '.java');
javaCmd.push(file);
});
const sourceLocation = tarBuffer;
const execution = dockerCommand(javacCmd, timeLimitCompileMs, dockerCommand(javaCmd, timeLimitExecutionMs));
createJContainer(messageId, sourceLocation, true, execution);
}
/**
* Creates and starts a container with bash, JDK SE, maybe junit and executes the callback
* passing the container or error to it.
*
* @param messageId {String}: id of the given message/request.
* @param tarBuffer {String}: the buffer of the tar containing java files.
* @param isJunit {Boolean}: true if need to create container with junit support.
* @param callback {function(error, container)}: callback to operate on error and container or data in case of error,
* should accept two arguments.
*/
function createJContainer(messageId, tarBuffer, isJunit, callback) {
let copyToCall = 4;
const tryCallback = (container) => {
if (copyToCall === 0) {
callback(null, container);
}
};
const createOpts = {Image: 'openjdk:8u111-jdk', Tty: true, Cmd: ['/bin/bash']};
docker.createContainer(createOpts, (err, container) => {
if (err) {callback(err, container); return}
container['messageId'] = messageId;
const startOpts = {};
container.start(startOpts, (err, data) => {
if (err) {callback(err, data); return}
if (isJunit) {
const tarOptsRunner = {path: 'home'};
container.putArchive('./archives/TestRunner.class.tar', tarOptsRunner, (err, data) => {
copyToCall--;
if (err) callback(err, data);
else tryCallback(container);
});
const tarOptsSecureTest = {path: 'home'};
container.putArchive('./archives/SecureTest.class.tar', tarOptsSecureTest, (err, data) => {
copyToCall--;
if (err) callback(err, data);
else tryCallback(container);
});
const tarOptsLibs = {path: '/'};
container.putArchive('./archives/libs.tar', tarOptsLibs, (err, data) => {
copyToCall--;
if (err) callback(err, data);
else tryCallback(container);
});
} else {
copyToCall = 1;
}
const tarOptsSource = {path: 'home'};
container.putArchive(tarBuffer, tarOptsSource, (err, data) => {
copyToCall--;
if (err) callback(err, data);
else tryCallback(container);
});
});
});
}
/**
* Given a command, the timeout and the callback, returns a function that on some given container,
* executes the command, attaches the output listener and runs waitCmdExit.
*
* @param command {String}: command to be passed to container runtime environment.
* @param commandTimeLimitMs {Number}: execution timeout.
* @param callback {function(err, container)}: function to be executed after the command has finished.
*
* @return {function(err, container)}: function that executes the command on a container.
*/
function dockerCommand(command, commandTimeLimitMs, callback) {
const opts = {Cmd: command, AttachStdout: true, AttachStderr: true};
const execution = (err, container) => {
if (!err) container.exec(opts, (err, exec) => {
exec.start((err, stream) => {
let stdOut = '';
let stdErr = '';
const stdOutConcat = concat({}, (data) => {
try {
stdOut += data;
} catch (e) {
stdOut = 'Output larger than 268435440 bytes.';
}
}).on('error', (err) => {});
const stdErrConcat = concat({}, (data) => {
try {
stdErr += data;
} catch (e) {
stdOut = 'Output larger than 268435440 bytes.';
}
}).on('error', (err) => {});
container.modem.demuxStream(stream, stdOutConcat, stdErrConcat);
const streamInfo = {
getOut: () => {return stdOut},
getErr: () => {return stdErr},
endStream: () => {
stdOutConcat.end();
stdErrConcat.end();
}
};
waitCmdExit(container, exec, callback, streamInfo, commandTimeLimitMs);
});
});
};
return execution;
}
/**
* Given an execution of a command on a container, a stream handler and command timeout
* waits for the command to be finished or timeout to be expired and calls the callback
* if it isn't null, otherwise it calls the feedbackAndClose function.
*
* @param container {Container}: Active docker container.
* @param exec {Object}: Docker execution object.
* @param callback {function(err, container)}: function to be executed after the command has finished.
* @param streamInfo {Object}: returns stdOut, stdIn and closes concat stream
* @param commandTimeOutMs {Number}: execution timeout in ms.
* @param previousTimeMs {Number}: ms already spent on this execution, called when the function is called
* recursively, should not be passed otherwise.
*/
function waitCmdExit(container, exec, callback, streamInfo, commandTimeOutMs, previousTimeMs){
let timeSpentMs = previousTimeMs || 0;
const checkExit = (err, data) => {
if (data.Running) { // command is still running, check later or send time out
timeSpentMs += EXEC_WAIT_TIME_MS; // count time spent
if (timeSpentMs >= commandTimeOutMs){ // time period expired
feedbackAndClose(container, streamInfo, false, true);
} else {
waitCmdExit(container, exec, callback, streamInfo, commandTimeOutMs, timeSpentMs);
}
} else if ((data.ExitCode === 0) && (callback)) { // command successful, has next command
callback(null, container);
} else if (data.ExitCode === 0) { // command successful, it was the last command
feedbackAndClose(container, streamInfo, true, false)
} else { // command failed
feedbackAndClose(container, streamInfo, false, false);
}
};
setTimeout(() => exec.inspect(checkExit), EXEC_WAIT_TIME_MS);
}
/**
* Closes the stream, parses it assuming it could be a specific junit output,
* accordingly creates a feedback and emits it, closing and deleting docker container
* at the end.
*
* @param container {Container}: Active docker container.
* @param streamInfo Object, returns stdOut, stdIn and closes concat stream
* @param passed Boolean, true if no compile or runtime error during normal execution
* @param timeOut Boolean, true if timeout time elapsed
*
* @feedback
* if test files exist (junit output) and passed is true:
* {
* messageId,
* passed: Boolean (false if compile/runtime errors true otherwise),
* output: String,
* errorMessage: String (empty if `passed` is true),
* timeOut: Boolean,
* totalNumberOfTests: Integer,
* numberOfTestsPassed: Integer,
* testsOutput: String (output of all failed tests)
* }
* otherwise:
* {
* messageId,
* passed: Boolean (false if compile/runtime errors true otherwise),
* output: String,
* errorMessage: String (empty if `passed` is false),
* timeOut: Boolean
* }
*/
function feedbackAndClose(container, streamInfo, passed, timeOut) {
streamInfo.endStream();
const feedback = {
messageId: container.messageId,
passed: passed,
output: (!timeOut) ? streamInfo.getOut() : '',
errorMessage: (!timeOut) ? streamInfo.getErr() : "Reached maximum time limit",
timeOut: timeOut
};
const parsed = parseOutput(feedback.output);
feedback.output = parsed.normalOutput;
feedback.totalNumberOfTests = parsed.totalNumberOfTests;
feedback.numberOfTestsPassed = parsed.numberOfTestsPassed;
feedback.testsOutput = parsed.testsOutput;
javaBox.emit('result', feedback);
container.kill({}, () =>
container.remove({v: true}, () => {}));
}
/**
* Given a possibly junit output, parses it and if it's junit, adds
* information about passed tests to the return object.
*
* @param output {String}: the stdout of the execution.
*
* @return {Object}: Contains stdout and possibly some info about junit tests.
*/
function parseOutput(output){
const _INPUT_DELIMITER_ = '_!*^&_test-output';
const outputObject = {};
outputObject.normalOutput = output.split(_INPUT_DELIMITER_)[0];
let testOutput = null;
try {
testOutput = JSON.parse(output.split(_INPUT_DELIMITER_)[1]);
} catch(err) {}
if (testOutput){
outputObject.totalNumberOfTests = testOutput.totalNumberOfTests;
outputObject.numberOfTestsPassed = testOutput.numberOfTestsPassed;
outputObject.testsOutput = testOutput.testsOutput;
}
return outputObject;
}
module.exports = javaBox;