4
# python-console <port> <magic> [<working-dir>]
17
from threading import Thread
22
# This version must be supported by both the local and remote code
25
class Interrupt(Exception):
27
Exception.__init__(self, "Interrupted!")
29
class ExpiryTimer(object):
30
def __init__(self, idle):
32
signal.signal(signal.SIGALRM, self.timeout)
35
signal.alarm(self.idle)
37
def start(self, time):
43
def timeout(self, signum, frame):
46
class StdinFromWeb(object):
47
def __init__(self, cmdQ, lineQ):
52
self.cmdQ.put({"input":None})
54
action, params = self.lineQ.get()
57
elif action == 'interrupt':
60
class StdoutToWeb(object):
61
def __init__(self, cmdQ, lineQ):
66
def _trim_incomplete_final(self, stuff):
67
'''Trim an incomplete UTF-8 character from the end of a string.
68
Returns (trimmed_string, count_of_trimmed_bytes).
70
tokill = ivle.util.incomplete_utf8_sequence(stuff)
72
return (stuff, tokill)
74
return (stuff[:-tokill], tokill)
76
def write(self, stuff):
77
# print will only give a non-file a unicode or str. There's no way
78
# to convince it to encode unicodes, so we have to do it ourselves.
79
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
80
# If somebody wants to write some other object to here, they do it
82
if isinstance(stuff, unicode):
83
stuff = stuff.encode('utf-8')
84
self.remainder = self.remainder + stuff
86
# if there's less than 128 bytes, buffer
87
if len(self.remainder) < 128:
90
# if there's lots, then send it in 1/2K blocks
91
while len(self.remainder) > 512:
92
# We send things as Unicode inside JSON, so we must only send
93
# complete UTF-8 characters.
94
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
95
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
97
action, params = self.lineQ.get()
98
self.remainder = self.remainder[512 - count:]
100
# Finally, split the remainder up into lines, and ship all the
101
# completed lines off to the server.
102
lines = self.remainder.split("\n")
103
self.remainder = lines[-1]
108
text = "\n".join(lines)
109
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
111
action, params = self.lineQ.get()
112
if action == 'interrupt':
116
if len(self.remainder) > 0:
117
(out, count) = self._trim_incomplete_final(self.remainder)
118
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
120
action, params = self.lineQ.get()
121
# Leave incomplete characters in the buffer.
122
# Yes, this does mean that an incomplete character will be left
123
# off the end, but we discussed this and it was deemed best.
124
self.remainder = self.remainder[len(self.remainder)-count:]
125
if action == 'interrupt':
129
"""Provides a file like interface to the Web front end of the console.
130
You may print text to the console using write(), flush any buffered output
131
using flush(), or request text from the console using readline()"""
132
# FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to
133
# be able to deal with the streams individually.
135
def __init__(self, cmdQ, lineQ):
138
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
139
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
141
def write(self, stuff):
142
self.stdout.write(stuff)
149
return self.stdin.readline()
151
class PythonRunner(Thread):
152
def __init__(self, cmdQ, lineQ):
155
self.webio = WebIO(self.cmdQ, self.lineQ)
156
self.cc = codeop.CommandCompiler()
157
Thread.__init__(self)
159
def execCmd(self, cmd):
161
# We don't expect a return value - 'single' symbol prints it.
165
return({"okay": None})
169
tb = format_exc_start(start=2)
170
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
173
# Set up global space and partial command buffer
177
# Set up I/O to use web interface
178
sys.stdin = self.webio
179
sys.stdout = self.webio
180
sys.stderr = self.webio
182
# Handlers for each action
184
'splash': self.handle_splash,
185
'chat': self.handle_chat,
186
'block': self.handle_block,
187
'globals': self.handle_globals,
188
'call': self.handle_call,
189
'execute': self.handle_execute,
190
'setvars': self.handle_setvars,
193
# Run the processing loop
195
action, params = self.lineQ.get()
197
response = actions[action](params)
199
response = {'error': repr(e)}
201
self.cmdQ.put(response)
203
def handle_splash(self, params):
204
# Initial console splash screen
205
python_version = '.'.join(str(v) for v in sys.version_info[:3])
206
splash_text = ("""IVLE %s Python Console (Python %s)
207
Type "help", "copyright", "credits" or "license" for more information.
208
""" % (ivle.__version__, python_version))
209
return {'output': splash_text}
211
def handle_chat(self, params):
212
# Set up the partial cmd buffer
213
if self.curr_cmd == '':
214
self.curr_cmd = params
216
self.curr_cmd = self.curr_cmd + '\n' + params
218
# Try to execute the buffer
220
# A single trailing newline simply indicates that the line is
221
# finished. Two trailing newlines indicate the end of a block.
222
# Unfortunately, codeop.CommandCompiler causes even one to
224
# Thus we need to remove a trailing newline from the command,
225
# unless there are *two* trailing newlines, or multi-line indented
226
# blocks are impossible. See Google Code issue 105.
227
cmd_text = self.curr_cmd
228
if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
229
cmd_text = cmd_text[:-1]
230
cmd = self.cc(cmd_text, '<web session>')
232
# The command was incomplete, so send back a None, so the
233
# client can print a '...'
234
return({"more":None})
236
return(self.execCmd(cmd))
238
# Clear any partial command
240
# Flush the output buffers
243
# Return the exception
244
tb = format_exc_start(start=3)
245
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
247
def handle_block(self, params):
248
# throw away any partial command.
251
# Try to execute a complete block of code
253
cmd = compile(params, "<web session>", 'exec');
254
return(self.execCmd(cmd))
256
# Flush the output buffers
259
# Return the exception
260
tb = format_exc_start(start=1)
261
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
263
def handle_globals(self, params):
264
# Unpickle the new space (if provided)
265
if isinstance(params, dict):
269
self.globs[g] = cPickle.loads(params[g])
273
# Return the current globals
274
return({'globals': flatten(self.globs)})
276
def handle_call(self, params):
279
# throw away any partial command.
282
if isinstance(params, dict):
285
if isinstance(params['args'], list):
286
args = map(self.eval, params['args'])
289
if isinstance(params['kwargs'], dict):
291
for kwarg in params['kwargs']:
292
kwargs[kwarg] = self.eval(
293
params['kwargs'][kwarg])
298
function = self.eval(params['function'])
300
call['result'] = function(*args, **kwargs)
303
tb = format_exc_start(start=1)
304
exception['traceback'] = \
305
''.join(tb).decode('utf-8', 'replace')
306
exception['except'] = cPickle.dumps(e,
308
call['exception'] = exception
310
tb = format_exc_start(start=1)
311
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
313
# Flush the output buffers
317
# Write out the inspection object
320
return({'response': 'failure'})
322
def handle_execute(self, params):
323
# throw away any partial command.
326
# Like block but return a serialization of the state
327
# throw away partial command
328
response = {'okay': None}
330
cmd = compile(params, "<web session>", 'exec');
331
# We don't expect a return value - 'single' symbol prints it.
334
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
340
# Return the inspection object
343
def handle_setvars(self, params):
344
# Adds some variables to the global dictionary
345
for var in params['set_vars']:
347
self.globs[var] = self.eval(params['set_vars'][var])
349
tb = format_exc_start(start=1)
350
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
352
return({'okay': None})
354
def eval(self, source):
355
""" Evaluates a string in the private global space """
356
return eval(source, self.globs)
358
# The global 'magic' is the secret that the client and server share
359
# which is used to create and md5 digest to authenticate requests.
360
# It is assigned a real value at startup.
364
lineQ = Queue.Queue()
365
interpThread = PythonRunner(cmdQ, lineQ)
368
# Default expiry time of 15 minutes
369
expiry = ExpiryTimer(15 * 60)
372
interpThread.setDaemon(True)
374
signal.signal(signal.SIGXCPU, sig_handler)
377
def sig_handler(signum, frame):
378
"""Handles response from signals"""
380
if signum == signal.SIGXCPU:
381
terminate = "CPU time limit exceeded"
383
def dispatch_msg(msg):
385
if msg['cmd'] == 'terminate':
386
terminate = "User requested restart"
388
raise ivle.chat.Terminate({"terminate":terminate})
390
lineQ.put((msg['cmd'],msg['text']))
391
response = cmdQ.get()
393
raise ivle.chat.Terminate({"terminate":terminate})
396
def format_exc_start(start=0):
397
etype, value, tb = sys.exc_info()
398
tbbits = traceback.extract_tb(tb)[start:]
399
list = ['Traceback (most recent call last):\n']
400
list = list + traceback.format_list(tbbits)
401
list = list + traceback.format_exception_only(etype, value)
405
# Takes an object and returns a flattened version suitable for JSON
410
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
411
except (TypeError, cPickle.PicklingError):
413
o_type = type(object[o]).__name__
414
o_name = object[o].__name__
415
fake_o = ivle.util.FakeObject(o_type, o_name)
416
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
417
except AttributeError:
421
if __name__ == "__main__":
422
port = int(sys.argv[1])
425
# Sanitise the Enviroment
427
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
429
if len(sys.argv) >= 4:
431
os.chdir(sys.argv[3])
432
os.environ['HOME'] = sys.argv[3]
434
# Make python's search path follow the cwd
437
ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)