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
'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_chat(self, params):
203
# Set up the partial cmd buffer
204
if self.curr_cmd == '':
205
self.curr_cmd = params
207
self.curr_cmd = self.curr_cmd + '\n' + params
209
# Try to execute the buffer
211
# A single trailing newline simply indicates that the line is
212
# finished. Two trailing newlines indicate the end of a block.
213
# Unfortunately, codeop.CommandCompiler causes even one to
215
# Thus we need to remove a trailing newline from the command,
216
# unless there are *two* trailing newlines, or multi-line indented
217
# blocks are impossible. See Google Code issue 105.
218
cmd_text = self.curr_cmd
219
if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
220
cmd_text = cmd_text[:-1]
221
cmd = self.cc(cmd_text, '<web session>')
223
# The command was incomplete, so send back a None, so the
224
# client can print a '...'
225
return({"more":None})
227
return(self.execCmd(cmd))
229
# Clear any partial command
231
# Flush the output buffers
234
# Return the exception
235
tb = format_exc_start(start=3)
236
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
238
def handle_block(self, params):
239
# throw away any partial command.
242
# Try to execute a complete block of code
244
cmd = compile(params, "<web session>", 'exec');
245
return(self.execCmd(cmd))
247
# Flush the output buffers
250
# Return the exception
251
tb = format_exc_start(start=1)
252
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
254
def handle_globals(self, params):
255
# Unpickle the new space (if provided)
256
if isinstance(params, dict):
260
self.globs[g] = cPickle.loads(params[g])
264
# Return the current globals
265
return({'globals': flatten(self.globs)})
267
def handle_call(self, params):
270
# throw away any partial command.
273
if isinstance(params, dict):
276
if isinstance(params['args'], list):
277
args = map(self.eval, params['args'])
280
if isinstance(params['kwargs'], dict):
282
for kwarg in params['kwargs']:
283
kwargs[kwarg] = self.eval(
284
params['kwargs'][kwarg])
289
function = self.eval(params['function'])
291
call['result'] = function(*args, **kwargs)
294
tb = format_exc_start(start=1)
295
exception['traceback'] = \
296
''.join(tb).decode('utf-8', 'replace')
297
exception['except'] = cPickle.dumps(e,
299
call['exception'] = exception
301
tb = format_exc_start(start=1)
302
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
304
# Flush the output buffers
308
# Write out the inspection object
311
return({'response': 'failure'})
313
def handle_execute(self, params):
314
# throw away any partial command.
317
# Like block but return a serialization of the state
318
# throw away partial command
319
response = {'okay': None}
321
cmd = compile(params, "<web session>", 'exec');
322
# We don't expect a return value - 'single' symbol prints it.
325
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
331
# Return the inspection object
334
def handle_setvars(self, params):
335
# Adds some variables to the global dictionary
336
for var in params['set_vars']:
338
self.globs[var] = self.eval(params['set_vars'][var])
340
tb = format_exc_start(start=1)
341
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
343
return({'okay': None})
345
def eval(self, source):
346
""" Evaluates a string in the private global space """
347
return eval(source, self.globs)
349
# The global 'magic' is the secret that the client and server share
350
# which is used to create and md5 digest to authenticate requests.
351
# It is assigned a real value at startup.
355
lineQ = Queue.Queue()
356
interpThread = PythonRunner(cmdQ, lineQ)
359
# Default expiry time of 15 minutes
360
expiry = ExpiryTimer(15 * 60)
363
interpThread.setDaemon(True)
365
signal.signal(signal.SIGXCPU, sig_handler)
368
def sig_handler(signum, frame):
369
"""Handles response from signals"""
371
if signum == signal.SIGXCPU:
372
terminate = "CPU Time Limit Exceeded"
374
def dispatch_msg(msg):
376
if msg['cmd'] == 'terminate':
377
terminate = "User requested console be terminated"
379
raise ivle.chat.Terminate({"terminate":terminate})
381
lineQ.put((msg['cmd'],msg['text']))
382
response = cmdQ.get()
384
raise ivle.chat.Terminate({"terminate":terminate})
387
def format_exc_start(start=0):
388
etype, value, tb = sys.exc_info()
389
tbbits = traceback.extract_tb(tb)[start:]
390
list = ['Traceback (most recent call last):\n']
391
list = list + traceback.format_list(tbbits)
392
list = list + traceback.format_exception_only(etype, value)
396
# Takes an object and returns a flattened version suitable for JSON
401
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
402
except (TypeError, cPickle.PicklingError):
404
o_type = type(object[o]).__name__
405
o_name = object[o].__name__
406
fake_o = ivle.util.FakeObject(o_type, o_name)
407
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
408
except AttributeError:
412
if __name__ == "__main__":
413
port = int(sys.argv[1])
416
# Sanitise the Enviroment
418
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
420
if len(sys.argv) >= 4:
422
os.chdir(sys.argv[3])
423
os.environ['HOME'] = sys.argv[3]
425
# Make python's search path follow the cwd
428
ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)