-
Notifications
You must be signed in to change notification settings - Fork 1
/
execcode.py
487 lines (442 loc) · 18.8 KB
/
execcode.py
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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# vim: et:ts=4:textwidth=80
# Automark
#
# David Llewellyn-Jones
# Liverpool John Moores University
# 18/12/2014
# Released under the GPL v.3. See the LICENSE file for more details.
"""
Execute code with given inputs and return the outputs.
Provide a local alternative to the ideone Sphere Engine code execution
engine for Java programs. A single Java source file is provided to
create a submission. This is executed with the given intputs, after
which the outputs are returned for testing.
"""
import os
import sys
from collections import namedtuple
from Queue import Queue, Empty
from time import strftime, time, sleep
from shutil import copytree
from subprocess import PIPE, Popen, STDOUT
Status = namedtuple('Status', ['key', 'value'])
ON_POSIX = 'posix' in sys.builtin_module_names
__all__ = ('ExecCode')
class ExecCode(object):
"""
Compile and execute a piece of code so it can be evaluted
"""
# user and password are ignored in this implementation
# createSubmission(sourceCode, input)
# return error
# getSubmissionStatus()
# return error, status, result
# getSubmissionDetails(withSource, withOutput, withStderr, withCmpinfo)
# return time, date, status, result, memory, signal, source, input,
# output, stderr, cmpinfo
def __init__(self, tempfolder, classname):
"""
Create a new instane of ExecCode(tempfolder).
"""
# Establish the temp folder
self._tempfolder = tempfolder
self._classname = classname
self._tempsourceleaf = classname + '.java'
# This doesn't confirm to PEP 8, but has been left to match the ideone API
def createSubmission(
self, user, password, sourceCode, language, input, run, private):
"""
Create a new piece of code to be compiled and executed.
"""
# Store the persistent info
self._sourceCode = sourceCode
self._input = input
self._status = 0
self._date = strftime('%Y-%m-%d %H-%M-%S')
# Create the temp folder if it doesn't already exist
if not os.path.exists(self._tempfolder):
os.makedirs(self._tempfolder)
if not os.path.exists(self._tempfolder + '/uk'):
copytree('java/uk', self._tempfolder + '/uk')
# Create the temporary source file to build based on the source code
# provided
self._tempsource = os.path.join(self._tempfolder, self._tempsourceleaf)
ExecCode._tidy_up(self._tempfolder)
with open(self._tempsource, 'w') as file:
file.write(sourceCode)
# Set up details of the submission in the sub-thread
self._submission = self.Submission(
self._tempfolder, self._tempsource, self._classname, input)
self._status = 1
# Spawn the sub-thread to perform compilation and execution of the
# submission
self._submission.start()
status = {'item': [Status('error', 'OK'), Status('link', 0)]}
return status
# This doesn't confirm to PEP 8, but has been left to match the ideone API
def getSubmissionStatus(self, user, password, link):
"""
Get the status of the code submission.
"""
if self._status == 0:
# The compilation/execution sub-thread hasn't been spawned yet,
# so we construct the response ourselves
status = {'item': [Status('error', 'OK'), Status('status', -1),
Status('result', 0)]}
else:
# Get the response from the compilation/executionsub-thread
status = self._submission._get_submission_status()
return status
# This doesn't confirm to PEP 8, but has been left to match the ideone API
def getSubmissionDetails(
self, user, password, link, withSource, withInput, withOutput,
withStderr, withCmpinfo):
"""
Get detailed information about a submission compilation and execution.
"""
if self._status == 0:
# The compilation/execution sub-thread hasn't been spawned yet,
# so we construct the response ourselves
details = {'item': []}
details['item'].append(Status('error', 'OK'))
details['item'].append(Status('time', 0))
details['item'].append(Status('status', -1))
details['item'].append(Status('result', 0))
details['item'].append(Status('memory', 0))
details['item'].append(Status('signal', 0))
details['item'].append(Status('public', False))
if withInput:
details['item'].append(Status('input', self._input))
if withOutput:
details['item'].append(Status('output', ''))
if withStderr:
details['item'].append(Status('stderr', ''))
if withCmpinfo:
details['item'].append(Status('cmpinfo', ''))
else:
# Get the response from the compilation/executionsub-thread
details = self._submission.get_submission_details(
withSource, withInput, withOutput, withStderr, withCmpinfo)
details['item'].append(Status('date', self._date))
if withSource:
details['item'].append(Status('source', self._sourceCode))
return details
@staticmethod
def get_value(response, key):
"""
Extract a value from the return data for the given key.
This is a helper function that makes it easier to deal with the
value returned by the methods.
"""
value = ''
# Find the item with the appropriate key
for item in response['item']:
if item.key == key:
# Return the value for this item
value = item.value
return value
@staticmethod
def check_submissions_status(status):
description = 'Unknown'
"""
Print out a status string based on the status number.
"""
if status < 0:
description = 'Waiting for compilation'
elif status == 1:
description = 'Compiling'
elif status == 3:
description = 'Running'
# From http://stackoverflow.com/questions/377017/test-if-executable-
# exists-in-python
@staticmethod
def _which(program):
"""
Check whether a given executable exists.
"""
def is_exe(fpath):
# Establish whether the file is executablee
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
# The file exists and is executable
return program
else:
# Check the PATH environment variable
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
# the path is executable and has the correct name
return exe_file
# Couldn't find the executable either directly or in the path
return None
@staticmethod
def response_check_compiled(response):
"""
Check whether the code compiled correctly.
Convert the response value from the process into a boolean
representing whether or not the code successfully compiled without
error.
Return True if the code compiled without error.
"""
compiled = False
if response >= 12:
compiled = True
return compiled
@staticmethod
def _tidy_up(tempfolder):
"""
Tidy the temporary folder containing build files.
Removes various temporary files generated by the checking process.
"""
# Remove any files with .class or .java extensions
for file in os.listdir(tempfolder):
extension = os.path.splitext(file)[1]
if (extension == '.class') or (extension == '.java'):
path = os.path.join(tempfolder, file)
os.remove(path)
import threading
class Submission(threading.Thread):
"""
Threading class to support parallel compilation and execution.
"""
def __init__(self, tempfolder, tempsource, classname, input):
"""
Initialise the thread.
"""
# Set up the initial variable values that the thread needss
self._error = 'OK'
self._status = -1
self._result = 0
self._update_status = ExecCode.threading.Lock()
self._tempfolder = tempfolder
self._tempsource = tempsource
self._classname = classname
self._input = input
self._cmpinfo = ''
self._time_start = 0
self._time_end = 0
self._date = ''
self._compile_result = [0, '']
self._exec_result = [0, '', '']
self._maxexectime = 3.0
ExecCode.threading.Thread.__init__(self)
def run(self):
"""
The thread entry point.
"""
# Compile the source file
self._compile_result = self._compile_source(
self._tempfolder, self._tempsource)
if self._compile_result[0] != 0:
# The compilation failed, so just return the output from the
# compilation
self._cmpinfo = self._compile_result[1]
self._set_submission_status('OK', 0, 11)
else:
# The compilation was successful
self._cmpinfo = ''
self._set_submission_status('OK', 3, 0)
# Execute the resulting Java class file
self._exec_result = self._execute(
self._tempfolder, self._classname, self._input)
# Capture the returned output from the execution
self._output = self._exec_result[1]
self._stderr = self._exec_result[2]
if self._exec_result[0] != 0:
# The execuation failed, so note the details
self._set_submission_status('OK', 0, 12)
else:
if self._timelimitexceeded:
# The execution took too long
self._set_submission_status('OK', 0, 13)
else:
# The execuation was successful
self._set_submission_status('OK', 0, 15)
def _set_submission_status(self, error, status, result):
"""
For internal use, set the status info for a given submssion.
"""
# Ensure only one thread can read/write the details simultaneously
# by acquring a lock
self._update_status.acquire()
# Set the details
self._error = error
self._status = status
self._result = result
# Release the lock
self._update_status.release()
# Status
# < 0 - waiting for compilation - the submission awaits execution
# in the queue
# 0 - done - the program has finished
# 1 - compilation - the program is being compiled
# 3 - running - the program is being executed
# Result
# 0 - not running - the submission has been created with run
# parameter set to false
# 11 - compilation error - the program could not be executed due to
# compilation error
# 12 - runtime error - the program finished because of the runtime
# error, for example: division by zero, array index out of
# bounds, uncaught exception
# 13 - time limit exceeded - the program didn't stop before the time
# limit
# 15 - success - everything went ok
# 17 - memory limit exceeded - the program tried to use more memory
# than it is allowed to
# 19 - illegal system call - the program tried to call illegal
# system function
# 20 - internal error - some problem occurred; try to submit the
# program again
def _get_submission_status(self):
"""
For internal use, gets status info about a given submssion.
"""
# Ensure only one thread can read/write the details simultaneously
# by acquring a lock
self._update_status.acquire()
# Structure the data appropriately
status = {'item': [Status('error', self._error), Status(
'status', self._status), Status('result', self._result)]}
# Release the lock
self._update_status.release()
return status
def get_submission_details(
self, withSource, withInput, withOutput, withStderr,
withCmpinfo):
"""
For internal use, gets details of a completed submssion.
"""
details = {'item': []}
details['item'].append(Status('error', self._error))
details['item'].append(Status('time', (
self._time_end - self._time_start)))
details['item'].append(Status('status', self._status))
details['item'].append(Status('result', self._result))
details['item'].append(Status('memory', 0))
details['item'].append(Status('signal', self._exec_result[0]))
details['item'].append(Status('public', False))
# Some of the return key-value pairs are optional
if withInput:
details['item'].append(Status('input', self._input))
if withOutput:
details['item'].append(Status('output', self._exec_result[1]))
if withStderr:
details['item'].append(Status('stderr', self._exec_result[2]))
if withCmpinfo:
details['item'].append(Status('cmpinfo', self._cmpinfo))
return details
def _compile_source(self, tempfolder, tempsource):
"""
For internal use, compiles the java source code.
"""
result = False
output = ''
if ExecCode._which('javac') == None:
# The Java compiler couldn't be found
output = 'Java compiler javac could not be found'
self._set_submission_status('OK', 0, 20)
else:
# The Java compiler is present
self._set_submission_status('OK', 1, 0)
# Execuate the compilation as a subprocess
program = Popen(
['javac', '-classpath', tempfolder, '-sourcepath',
tempfolder, '-d', tempfolder, tempsource],
shell=False, cwd='.',
stderr=STDOUT, stdin=PIPE, stdout=PIPE)
# Collect any outputs from the compilation process
output = program.stdout.read()
program.communicate()
result = program.returncode
if result == 0:
# Compilation error
self._set_submission_status('OK', 0, 11)
else:
# Compilation success
self._set_submission_status('OK', 3, 0)
return [result, output]
def _execute(self, tempfolder, classname, input):
"""
For internal use, executes the java source code.
"""
output = ''
if ExecCode._which('java') == None:
# The Java VM could not be found
print 'Java VM could not be found'
self._set_submission_status('OK', 1, 0)
else:
# The Java VM is present
self._time_start = time()
# Execute the compiled code as a subprocess
program = Popen(
['java', classname], shell=False, cwd=tempfolder,
bufsize=1,
stderr=PIPE, stdin=PIPE, stdout=PIPE, close_fds=ON_POSIX)
output_queue = Queue()
output_collector = ExecCode.threading.Thread(
target=ExecCode.Submission._enqueue_output,
args=(program.stdout, output_queue))
output_collector.daemon = True
output_collector.start()
# Pass the input to the running code
program.stdin.write(input)
# Collect any output form the the running code
output = ''
exectime = 0.0
result = None
self._timelimitexceeded = False
while (result == None) and not self._timelimitexceeded:
program.poll()
try:
line = output_queue.get_nowait()
except Empty:
# No output, wait a bit and check again
sleep(0.1)
result = program.returncode
else:
# We caught some output, so record it
output += line
# Check how long the program's been running for
exectime = time() - self._time_start
if exectime >= self._maxexectime:
# Too long!
self._timelimitexceeded = True
if self._timelimitexceeded:
program.kill()
error = program.stderr.read()
result = program.returncode
if result == None:
result = 0
self._time_end = time()
return [result, output.decode("utf-8"), error.decode("utf-8")]
# From http://stackoverflow.com/questions/375427/non-blocking-
# read-on-a-subprocess-pipe-in-python
@staticmethod
def _enqueue_output(out, queue):
"""
For internal use, stores output generated by execution.
"""
for line in iter(out.readline, b''):
queue.put(line)
out.close()
#program = ExecCode('build', 'CourseworkTask1')
#sourcecode = ''
#with open('/home/flypig/Documents/LJMU/Projects/AutoMarking'
# '/automark/DLJ/cmpgyate/temp.java') as file:
# sourcecode = file.read()
#program.createSubmission('', '', sourcecode, 11, '10\n11\n12\n', True, True)
#status = -1;
#wait_time = 0
#while status != 0:
# sleep(wait_time)
# wait_time = 0.1
# response = program.getSubmissionStatus('', '', '')
# status = ExecCode.get_value(response, 'status')
# print ExecCode.check_submissions_status (status)
#details = program.getSubmissionDetails('', '', '', True, True, True,
# True, True)
#print ExecCode.get_value(details, 'output')