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})
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 = 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')})
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
ln = self.lineQ.get()
112
if 'interrupt' in ln:
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
ln = 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 'interrupt' in ln:
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()"""
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
Thread.__init__(self)
156
def execCmd(self, cmd):
158
sys.stdin = self.webio
159
sys.stdout = self.webio
160
sys.stderr = self.webio
161
# We don't expect a return value - 'single' symbol prints it.
164
self.cmdQ.put({"okay": None})
167
tb = format_exc_start(start=1)
169
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
174
self.globs['__builtins__'] = globals()['__builtins__']
178
ln = self.lineQ.get()
180
if self.curr_cmd == '':
181
self.curr_cmd = ln['chat']
183
self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
185
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
187
# The command was incomplete,
188
# so send back a None, so the
189
# client can print a '...'
190
self.cmdQ.put({"more":None})
194
tb = format_exc_start(start=3)
195
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
199
# throw away a partial command.
201
cmd = compile(ln['block'], "<web session>", 'exec');
204
tb = format_exc_start(start=1)
206
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
211
self.globs['__builtins__'] = globals()['__builtins__']
212
self.cmdQ.put({'response': 'okay'})
213
# Unpickle the new space (if provided)
214
if isinstance(ln['flush'],dict):
215
for g in ln['flush']:
217
self.globs[g] = cPickle.loads(ln['flush'][g])
222
stdout = cStringIO.StringIO()
223
stderr = cStringIO.StringIO()
227
if isinstance(ln['call'], dict):
231
if isinstance(params['args'], list):
232
args = map(self.eval, params['args'])
235
if isinstance(params['kwargs'], dict):
237
for kwarg in params['kwargs']:
238
kwargs[kwarg] = self.eval(
239
params['kwargs'][kwarg])
244
function = self.eval(params['function'])
246
call['result'] = function(*args, **kwargs)
249
tb = format_exc_start(start=1)
250
exception['traceback'] = \
251
''.join(tb).decode('utf-8', 'replace')
252
exception['except'] = cPickle.dumps(e,
254
call['exception'] = exception
256
tb = format_exc_start(start=1)
258
{"exc": ''.join(tb).decode('utf-8', 'replace')})
260
# Write out the inspection object
261
call['stdout'] = stdout.getvalue()
262
call['stderr'] = stderr.getvalue()
265
self.cmdQ.put({'response': 'failure'})
269
elif 'inspect' in ln:
270
# Like block but return a serialization of the state
271
# throw away partial command
273
stdout = cStringIO.StringIO()
274
stderr = cStringIO.StringIO()
276
cmd = compile(ln['inspect'], "<web session>", 'exec');
280
# We don't expect a return value - 'single' symbol prints
285
tb = format_exc_start(start=1)
286
exception['traceback'] = \
287
''.join(tb).decode('utf-8', 'replace')
288
exception['except'] = cPickle.dumps(e, PICKLEVERSION)
289
inspection['exception'] = exception
291
# Write out the inspection object
292
inspection['stdout'] = stdout.getvalue()
293
inspection['stderr'] = stderr.getvalue()
294
inspection['globals'] = flatten(self.globs)
295
self.cmdQ.put(inspection)
299
elif 'set_vars' in ln:
300
# Adds some variables to the global dictionary
301
for var in ln['set_vars']:
303
self.globs[var] = self.eval(ln['set_vars'][var])
305
tb = format_exc_start(start=1)
307
{"exc": ''.join(tb).decode('utf-8', 'replace')})
309
self.cmdQ.put({'response': 'okay'})
311
raise Exception, "Invalid Command"
313
def eval(self, source):
314
""" Evaluates a string in the private global space """
315
return eval(source, self.globs)
318
if os.fork(): # launch child and...
319
os._exit(0) # kill off parent
321
if os.fork(): # launch child and...
322
os._exit(0) # kill off parent again.
325
# The global 'magic' is the secret that the client and server share
326
# which is used to create and md5 digest to authenticate requests.
327
# It is assigned a real value at startup.
331
lineQ = Queue.Queue()
332
interpThread = PythonRunner(cmdQ, lineQ)
335
# Default expiry time of 15 minutes
336
expiry = ExpiryTimer(15 * 60)
339
interpThread.setDaemon(True)
341
signal.signal(signal.SIGXCPU, sig_handler)
344
def sig_handler(signum, frame):
345
"""Handles response from signals"""
347
if signum == signal.SIGXCPU:
348
terminate = "CPU Time Limit Exceeded"
350
def dispatch_msg(msg):
352
if msg['cmd'] == 'restart':
353
terminate = "User requested console be reset"
355
raise common.chat.Terminate({"restart":terminate})
357
lineQ.put({msg['cmd']:msg['text']})
359
raise common.chat.Terminate({"restart":terminate})
362
def format_exc_start(start=0):
363
etype, value, tb = sys.exc_info()
364
tbbits = traceback.extract_tb(tb)[start:]
365
list = ['Traceback (most recent call last):\n']
366
list = list + traceback.format_list(tbbits)
367
list = list + traceback.format_exception_only(etype, value)
370
def incomplete_utf8_sequence(byteseq):
373
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
374
the end of the string which comprise an incomplete UTF-8 character
377
If the string is empty or ends with a complete character OR INVALID
379
Otherwise, returns 1-3 indicating the number of bytes in the final
380
incomplete (but valid) character sequence.
382
Does not check any bytes before the final sequence for correctness.
384
>>> incomplete_utf8_sequence("")
386
>>> incomplete_utf8_sequence("xy")
388
>>> incomplete_utf8_sequence("xy\xc3\xbc")
390
>>> incomplete_utf8_sequence("\xc3")
392
>>> incomplete_utf8_sequence("\xbc\xc3")
394
>>> incomplete_utf8_sequence("xy\xbc\xc3")
396
>>> incomplete_utf8_sequence("xy\xe0\xa0")
398
>>> incomplete_utf8_sequence("xy\xf4")
400
>>> incomplete_utf8_sequence("xy\xf4\x8f")
402
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
407
for b in byteseq[::-1]:
411
# 0xxxxxxx (single-byte character)
414
elif b & 0xc0 == 0x80:
415
# 10xxxxxx (subsequent byte)
417
elif b & 0xe0 == 0xc0:
418
# 110xxxxx (start of 2-byte sequence)
421
elif b & 0xf0 == 0xe0:
422
# 1110xxxx (start of 3-byte sequence)
425
elif b & 0xf8 == 0xf0:
426
# 11110xxx (start of 4-byte sequence)
434
# Seen too many "subsequent bytes", invalid
438
# We never saw a "first byte", invalid
441
# We now know expect and count
443
# Complete, or we saw an invalid sequence
449
# Takes an object and returns a flattened version suitable for JSON
454
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
457
o_type = type(object[o]).__name__
458
o_name = object[o].__name__
459
fake_o = common.util.FakeObject(o_type, o_name)
460
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
461
except AttributeError:
465
if __name__ == "__main__":
466
port = int(sys.argv[1])
469
# Sanitise the Enviroment
471
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
473
if len(sys.argv) >= 4:
475
os.chdir(sys.argv[3])
476
os.environ['HOME'] = sys.argv[3]
478
# Make python's search path follow the cwd
481
common.chat.start_server(port, magic, True, dispatch_msg, initializer)