4
# python-console <port> <magic> [<working-dir>]
16
from threading import Thread
21
# This version must be supported by both the local and remote code
24
class Interrupt(Exception):
26
Exception.__init__(self, "Interrupted!")
28
class ExpiryTimer(object):
29
def __init__(self, idle):
31
signal.signal(signal.SIGALRM, self.timeout)
34
signal.alarm(self.idle)
36
def start(self, time):
42
def timeout(self, signum, frame):
45
class StdinFromWeb(object):
46
def __init__(self, cmdQ, lineQ):
51
self.cmdQ.put({"input":None})
53
action, params = self.lineQ.get()
56
elif action == 'interrupt':
59
class StdoutToWeb(object):
60
def __init__(self, cmdQ, lineQ):
65
def _trim_incomplete_final(self, stuff):
66
'''Trim an incomplete UTF-8 character from the end of a string.
67
Returns (trimmed_string, count_of_trimmed_bytes).
69
tokill = ivle.util.incomplete_utf8_sequence(stuff)
71
return (stuff, tokill)
73
return (stuff[:-tokill], tokill)
75
def write(self, stuff):
76
# print will only give a non-file a unicode or str. There's no way
77
# to convince it to encode unicodes, so we have to do it ourselves.
78
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
79
# If somebody wants to write some other object to here, they do it
81
if isinstance(stuff, unicode):
82
stuff = stuff.encode('utf-8')
83
self.remainder = self.remainder + stuff
85
# if there's less than 128 bytes, buffer
86
if len(self.remainder) < 128:
89
# if there's lots, then send it in 1/2K blocks
90
while len(self.remainder) > 512:
91
# We send things as Unicode inside JSON, so we must only send
92
# complete UTF-8 characters.
93
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
94
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
96
action, params = self.lineQ.get()
97
self.remainder = self.remainder[512 - count:]
99
# Finally, split the remainder up into lines, and ship all the
100
# completed lines off to the server.
101
lines = self.remainder.split("\n")
102
self.remainder = lines[-1]
107
text = "\n".join(lines)
108
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
110
action, params = self.lineQ.get()
111
if action == 'interrupt':
115
if len(self.remainder) > 0:
116
(out, count) = self._trim_incomplete_final(self.remainder)
117
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
119
action, params = self.lineQ.get()
120
# Leave incomplete characters in the buffer.
121
# Yes, this does mean that an incomplete character will be left
122
# off the end, but we discussed this and it was deemed best.
123
self.remainder = self.remainder[len(self.remainder)-count:]
124
if action == 'interrupt':
128
"""Provides a file like interface to the Web front end of the console.
129
You may print text to the console using write(), flush any buffered output
130
using flush(), or request text from the console using readline()"""
131
# FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to
132
# be able to deal with the streams individually.
134
def __init__(self, cmdQ, lineQ):
137
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
138
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
140
def write(self, stuff):
141
self.stdout.write(stuff)
148
return self.stdin.readline()
150
class PythonRunner(Thread):
151
def __init__(self, cmdQ, lineQ):
154
self.webio = WebIO(self.cmdQ, self.lineQ)
155
self.cc = codeop.CommandCompiler()
156
Thread.__init__(self)
158
def execCmd(self, cmd):
160
# We don't expect a return value - 'single' symbol prints it.
164
return({"okay": None})
168
tb = format_exc_start(start=2)
169
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
172
# Set up global space and partial command buffer
173
self.globs = {'__name__': '__main__'}
176
# Set up I/O to use web interface
177
sys.stdin = self.webio
178
sys.stdout = self.webio
179
sys.stderr = self.webio
181
# Handlers for each action
183
'splash': self.handle_splash,
184
'chat': self.handle_chat,
185
'block': self.handle_block,
186
'globals': self.handle_globals,
187
'call': self.handle_call,
188
'execute': self.handle_execute,
189
'setvars': self.handle_setvars,
192
# Run the processing loop
194
action, params = self.lineQ.get()
196
response = actions[action](params)
198
response = {'error': repr(e)}
200
self.cmdQ.put(response)
202
def handle_splash(self, params):
203
# Initial console splash screen
204
python_version = '.'.join(str(v) for v in sys.version_info[:3])
205
splash_text = ("""IVLE %s Python Console (Python %s)
206
Type "help", "copyright", "credits" or "license" for more information.
207
""" % (ivle.__version__, python_version))
208
return {'output': splash_text}
210
def handle_chat(self, params):
211
# Set up the partial cmd buffer
212
if self.curr_cmd == '':
213
self.curr_cmd = params
215
self.curr_cmd = self.curr_cmd + '\n' + params
217
# Try to execute the buffer
219
# A single trailing newline simply indicates that the line is
220
# finished. Two trailing newlines indicate the end of a block.
221
# Unfortunately, codeop.CommandCompiler causes even one to
223
# Thus we need to remove a trailing newline from the command,
224
# unless there are *two* trailing newlines, or multi-line indented
225
# blocks are impossible. See Google Code issue 105.
226
cmd_text = self.curr_cmd
227
if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
228
cmd_text = cmd_text[:-1]
229
cmd = self.cc(cmd_text, '<web session>')
231
# The command was incomplete, so send back a None, so the
232
# client can print a '...'
233
return({"more":None})
235
return(self.execCmd(cmd))
237
# Clear any partial command
239
# Flush the output buffers
242
# Return the exception
243
tb = format_exc_start(start=3)
244
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
246
def handle_block(self, params):
247
# throw away any partial command.
250
# Try to execute a complete block of code
252
cmd = compile(params, "<web session>", 'exec');
253
return(self.execCmd(cmd))
255
# Flush the output buffers
258
# Return the exception
259
tb = format_exc_start(start=1)
260
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
262
def handle_globals(self, params):
263
# Unpickle the new space (if provided)
264
if isinstance(params, dict):
265
self.globs = {'__name__': '__main__'}
268
self.globs[g] = cPickle.loads(params[g])
272
# Return the current globals
273
return({'globals': flatten(self.globs)})
275
def handle_call(self, params):
278
# throw away any partial command.
281
if isinstance(params, dict):
284
if isinstance(params['args'], list):
285
args = map(self.eval, params['args'])
288
if isinstance(params['kwargs'], dict):
290
for kwarg in params['kwargs']:
291
kwargs[kwarg] = self.eval(
292
params['kwargs'][kwarg])
297
function = self.eval(params['function'])
299
call['result'] = function(*args, **kwargs)
302
tb = format_exc_start(start=1)
303
exception['traceback'] = \
304
''.join(tb).decode('utf-8', 'replace')
305
exception['except'] = cPickle.dumps(e,
307
call['exception'] = exception
309
tb = format_exc_start(start=1)
310
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
312
# Flush the output buffers
316
# Write out the inspection object
319
return({'response': 'failure'})
321
def handle_execute(self, params):
322
# throw away any partial command.
325
# Like block but return a serialization of the state
326
# throw away partial command
327
response = {'okay': None}
329
cmd = compile(params, "<web session>", 'exec');
330
# We don't expect a return value - 'single' symbol prints it.
333
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
339
# Return the inspection object
342
def handle_setvars(self, params):
343
# Adds some variables to the global dictionary
344
for var in params['set_vars']:
346
self.globs[var] = self.eval(params['set_vars'][var])
348
tb = format_exc_start(start=1)
349
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
351
return({'okay': None})
353
def eval(self, source):
354
""" Evaluates a string in the private global space """
355
return eval(source, self.globs)
357
# The global 'magic' is the secret that the client and server share
358
# which is used to create and md5 digest to authenticate requests.
359
# It is assigned a real value at startup.
363
lineQ = Queue.Queue()
364
interpThread = PythonRunner(cmdQ, lineQ)
367
# Default expiry time of 15 minutes
368
expiry = ExpiryTimer(15 * 60)
371
interpThread.setDaemon(True)
373
signal.signal(signal.SIGXCPU, sig_handler)
376
def sig_handler(signum, frame):
377
"""Handles response from signals"""
379
if signum == signal.SIGXCPU:
380
terminate = "CPU time limit exceeded"
382
def dispatch_msg(msg):
384
if msg['cmd'] == 'terminate':
385
terminate = "User requested restart"
387
raise ivle.chat.Terminate({"terminate":terminate})
389
lineQ.put((msg['cmd'],msg['text']))
390
response = cmdQ.get()
392
raise ivle.chat.Terminate({"terminate":terminate})
395
def format_exc_start(start=0):
396
etype, value, tb = sys.exc_info()
397
tbbits = traceback.extract_tb(tb)[start:]
398
list = ['Traceback (most recent call last):\n']
399
list = list + traceback.format_list(tbbits)
400
list = list + traceback.format_exception_only(etype, value)
404
# Takes an object and returns a flattened version suitable for JSON
409
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
410
except (TypeError, cPickle.PicklingError):
412
o_type = type(object[o]).__name__
413
o_name = object[o].__name__
414
fake_o = ivle.util.FakeObject(o_type, o_name)
415
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
416
except AttributeError:
420
if __name__ == "__main__":
421
port = int(sys.argv[1])
424
# Make python's search path follow the cwd
427
ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)