4
# python-console <port> <magic> [<working-dir>]
15
from threading import Thread
20
# This version must be supported by both the local and remote code
23
class Interrupt(Exception):
25
Exception.__init__(self, "Interrupted!")
27
class ExpiryTimer(object):
28
def __init__(self, idle):
30
signal.signal(signal.SIGALRM, self.timeout)
33
signal.alarm(self.idle)
35
def start(self, time):
41
def timeout(self, signum, frame):
44
class StdinFromWeb(object):
45
def __init__(self, cmdQ, lineQ):
50
self.cmdQ.put({"input":None})
52
action, params = self.lineQ.get()
55
elif action == 'interrupt':
58
class StdoutToWeb(object):
59
def __init__(self, cmdQ, lineQ):
64
def _trim_incomplete_final(self, stuff):
65
'''Trim an incomplete UTF-8 character from the end of a string.
66
Returns (trimmed_string, count_of_trimmed_bytes).
68
tokill = ivle.util.incomplete_utf8_sequence(stuff)
70
return (stuff, tokill)
72
return (stuff[:-tokill], tokill)
74
def write(self, stuff):
75
# print will only give a non-file a unicode or str. There's no way
76
# to convince it to encode unicodes, so we have to do it ourselves.
77
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
78
# If somebody wants to write some other object to here, they do it
80
if isinstance(stuff, unicode):
81
stuff = stuff.encode('utf-8')
82
self.remainder = self.remainder + stuff
84
# if there's less than 128 bytes, buffer
85
if len(self.remainder) < 128:
88
# if there's lots, then send it in 1/2K blocks
89
while len(self.remainder) > 512:
90
# We send things as Unicode inside JSON, so we must only send
91
# complete UTF-8 characters.
92
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
93
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
95
action, params = self.lineQ.get()
96
self.remainder = self.remainder[512 - count:]
98
# Finally, split the remainder up into lines, and ship all the
99
# completed lines off to the server.
100
lines = self.remainder.split("\n")
101
self.remainder = lines[-1]
106
text = "\n".join(lines)
107
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
109
action, params = self.lineQ.get()
110
if action == 'interrupt':
114
if len(self.remainder) > 0:
115
(out, count) = self._trim_incomplete_final(self.remainder)
116
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
118
action, params = self.lineQ.get()
119
# Leave incomplete characters in the buffer.
120
# Yes, this does mean that an incomplete character will be left
121
# off the end, but we discussed this and it was deemed best.
122
self.remainder = self.remainder[len(self.remainder)-count:]
123
if action == 'interrupt':
127
"""Provides a file like interface to the Web front end of the console.
128
You may print text to the console using write(), flush any buffered output
129
using flush(), or request text from the console using readline()"""
130
# FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to
131
# be able to deal with the streams individually.
133
def __init__(self, cmdQ, lineQ):
136
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
137
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
139
def write(self, stuff):
140
self.stdout.write(stuff)
147
return self.stdin.readline()
149
class PythonRunner(Thread):
150
def __init__(self, cmdQ, lineQ):
153
self.webio = WebIO(self.cmdQ, self.lineQ)
154
self.cc = codeop.CommandCompiler()
155
Thread.__init__(self)
157
def execCmd(self, cmd):
159
# We don't expect a return value - 'single' symbol prints it.
163
return({"okay": None})
167
tb = format_exc_start(start=2)
168
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
171
# Set up global space and partial command buffer
172
self.globs = {'__name__': '__main__'}
175
# Set up I/O to use web interface
176
sys.stdin = self.webio
177
sys.stdout = self.webio
178
sys.stderr = self.webio
180
# Handlers for each action
182
'splash': self.handle_splash,
183
'chat': self.handle_chat,
184
'block': self.handle_block,
185
'globals': self.handle_globals,
186
'call': self.handle_call,
187
'execute': self.handle_execute,
188
'setvars': self.handle_setvars,
191
# Run the processing loop
193
action, params = self.lineQ.get()
195
response = actions[action](params)
197
response = {'error': repr(e)}
199
self.cmdQ.put(response)
201
def handle_splash(self, params):
202
# Initial console splash screen
203
python_version = '.'.join(str(v) for v in sys.version_info[:3])
204
splash_text = ("""IVLE %s Python Console (Python %s)
205
Type "help", "copyright", "credits" or "license" for more information.
206
""" % (ivle.__version__, python_version))
207
return {'output': splash_text}
209
def handle_chat(self, params):
210
# Set up the partial cmd buffer
211
if self.curr_cmd == '':
212
self.curr_cmd = params
214
self.curr_cmd = self.curr_cmd + '\n' + params
216
# Try to execute the buffer
218
# A single trailing newline simply indicates that the line is
219
# finished. Two trailing newlines indicate the end of a block.
220
# Unfortunately, codeop.CommandCompiler causes even one to
222
# Thus we need to remove a trailing newline from the command,
223
# unless there are *two* trailing newlines, or multi-line indented
224
# blocks are impossible. See Google Code issue 105.
225
cmd_text = self.curr_cmd
226
if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
227
cmd_text = cmd_text[:-1]
228
cmd = self.cc(cmd_text, '<web session>')
230
# The command was incomplete, so send back a None, so the
231
# client can print a '...'
232
return({"more":None})
234
return(self.execCmd(cmd))
236
# Clear any partial command
238
# Flush the output buffers
241
# Return the exception
242
tb = format_exc_start(start=3)
243
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
245
def handle_block(self, params):
246
# throw away any partial command.
249
# Try to execute a complete block of code
251
cmd = compile(params, "<web session>", 'exec');
252
return(self.execCmd(cmd))
254
# Flush the output buffers
257
# Return the exception
258
tb = format_exc_start(start=1)
259
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
261
def handle_globals(self, params):
262
# Unpickle the new space (if provided)
263
if isinstance(params, dict):
264
self.globs = {'__name__': '__main__'}
267
self.globs[g] = cPickle.loads(str(params[g]))
268
except cPickle.UnpicklingError:
271
# Return the current globals
272
return({'globals': flatten(self.globs)})
274
def handle_call(self, params):
277
# throw away any partial command.
280
if isinstance(params, dict):
283
if isinstance(params['args'], list):
284
args = map(self.eval, params['args'])
287
if isinstance(params['kwargs'], dict):
289
for kwarg in params['kwargs']:
290
kwargs[kwarg] = self.eval(
291
params['kwargs'][kwarg])
296
function = self.eval(params['function'])
298
call['result'] = function(*args, **kwargs)
301
tb = format_exc_start(start=1)
302
exception['traceback'] = \
303
''.join(tb).decode('utf-8', 'replace')
304
exception['except'] = cPickle.dumps(e,
306
call['exception'] = exception
308
tb = format_exc_start(start=1)
309
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
311
# Flush the output buffers
315
# Write out the inspection object
318
return({'response': 'failure'})
320
def handle_execute(self, params):
321
# throw away any partial command.
324
# Like block but return a serialization of the state
325
# throw away partial command
326
response = {'okay': None}
328
cmd = compile(params, "<web session>", 'exec');
329
# We don't expect a return value - 'single' symbol prints it.
332
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
338
# Return the inspection object
341
def handle_setvars(self, params):
342
# Adds some variables to the global dictionary
343
for var in params['set_vars']:
345
self.globs[var] = self.eval(params['set_vars'][var])
347
tb = format_exc_start(start=1)
348
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
350
return({'okay': None})
352
def eval(self, source):
353
""" Evaluates a string in the private global space """
354
return eval(source, self.globs)
356
# The global 'magic' is the secret that the client and server share
357
# which is used to create and md5 digest to authenticate requests.
358
# It is assigned a real value at startup.
362
lineQ = Queue.Queue()
363
interpThread = PythonRunner(cmdQ, lineQ)
366
# Default expiry time of 15 minutes
367
expiry = ExpiryTimer(15 * 60)
370
interpThread.setDaemon(True)
372
signal.signal(signal.SIGXCPU, sig_handler)
375
def sig_handler(signum, frame):
376
"""Handles response from signals"""
378
if signum == signal.SIGXCPU:
379
terminate = "CPU time limit exceeded"
381
def dispatch_msg(msg):
383
if msg['cmd'] == 'terminate':
384
terminate = "User requested restart"
386
raise ivle.chat.Terminate({"terminate":terminate})
388
lineQ.put((msg['cmd'],msg['text']))
389
response = cmdQ.get()
391
raise ivle.chat.Terminate({"terminate":terminate})
394
def format_exc_start(start=0):
395
etype, value, tb = sys.exc_info()
396
tbbits = traceback.extract_tb(tb)[start:]
397
list = ['Traceback (most recent call last):\n']
398
list = list + traceback.format_list(tbbits)
399
list = list + traceback.format_exception_only(etype, value)
403
# Takes an object and returns a flattened version suitable for JSON
408
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
409
except (TypeError, cPickle.PicklingError):
411
o_type = type(object[o]).__name__
412
o_name = object[o].__name__
413
fake_o = ivle.util.FakeObject(o_type, o_name)
414
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
415
except AttributeError:
419
if __name__ == "__main__":
420
port = int(sys.argv[1])
423
# Make python's search path follow the cwd
426
ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)