4
# python-console <port> <magic> [<working-dir>]
17
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})
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 = 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')})
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
ln = self.lineQ.get()
111
if 'interrupt' in ln:
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
ln = 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 'interrupt' in ln:
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()"""
132
def __init__(self, cmdQ, lineQ):
135
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
136
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
138
def write(self, stuff):
139
self.stdout.write(stuff)
146
return self.stdin.readline()
148
class PythonRunner(Thread):
149
def __init__(self, cmdQ, lineQ):
152
self.webio = WebIO(self.cmdQ, self.lineQ)
153
Thread.__init__(self)
155
def execCmd(self, cmd):
157
sys.stdin = self.webio
158
sys.stdout = self.webio
159
sys.stderr = self.webio
160
# We don't expect a return value - 'single' symbol prints it.
163
self.cmdQ.put({"okay": None})
166
tb = format_exc_start(start=1)
168
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
173
self.globs['__builtins__'] = globals()['__builtins__']
177
ln = self.lineQ.get()
179
if self.curr_cmd == '':
180
self.curr_cmd = ln['chat']
182
self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
184
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
186
# The command was incomplete,
187
# so send back a None, so the
188
# client can print a '...'
189
self.cmdQ.put({"more":None})
193
tb = format_exc_start(start=3)
194
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
198
# throw away a partial command.
200
cmd = compile(ln['block'], "<web session>", 'exec');
203
tb = format_exc_start(start=1)
205
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
210
self.globs['__builtins__'] = globals()['__builtins__']
211
self.cmdQ.put({'response': 'okay'})
212
# Unpickle the new space (if provided)
213
if isinstance(ln['flush'],dict):
214
for g in ln['flush']:
216
self.globs[g] = cPickle.loads(ln['flush'][g])
220
if isinstance(ln['call'], dict):
224
if isinstance(params['args'], list):
225
args = map(self.eval, params['args'])
228
if isinstance(params['kwargs'], dict):
230
for kwarg in params['kwargs']:
231
kwargs[kwarg] = self.eval(
232
params['kwargs'][kwarg])
237
function = self.eval(params['function'])
238
result = function(*args, **kwargs)
239
self.cmdQ.put({'output': result})
241
tb = format_exc_start(start=1)
243
{"exc": ''.join(tb).decode('utf-8', 'replace')})
245
self.cmdQ.put({'response': 'failure'})
246
elif 'inspect' in ln:
247
# Like block but return a serialization of the state
248
# throw away partial command
250
stdout = cStringIO.StringIO()
251
stderr = cStringIO.StringIO()
253
cmd = compile(ln['inspect'], "<web session>", 'exec');
257
# We don't expect a return value - 'single' symbol prints
262
tb = format_exc_start(start=1)
263
exception['traceback'] = \
264
''.join(tb).decode('utf-8', 'replace')
265
exception['except'] = cPickle.dumps(e, PICKLEVERSION)
266
inspection['exception'] = exception
268
# Write out the inspection object
269
inspection['stdout'] = stdout.getvalue()
270
inspection['stderr'] = stderr.getvalue()
271
inspection['globals'] = flatten(self.globs)
272
self.cmdQ.put(inspection)
277
raise Exception, "Invalid Command"
279
def eval(self, source):
280
""" Evaluates a string in the private global space """
281
return eval(source, self.globs)
284
if os.fork(): # launch child and...
285
os._exit(0) # kill off parent
287
if os.fork(): # launch child and...
288
os._exit(0) # kill off parent again.
291
# The global 'magic' is the secret that the client and server share
292
# which is used to create and md5 digest to authenticate requests.
293
# It is assigned a real value at startup.
297
lineQ = Queue.Queue()
298
interpThread = PythonRunner(cmdQ, lineQ)
301
# Default expiry time of 15 minutes
302
expiry = ExpiryTimer(15 * 60)
305
interpThread.setDaemon(True)
307
signal.signal(signal.SIGXCPU, sig_handler)
310
def sig_handler(signum, frame):
311
"""Handles response from signals"""
313
if signum == signal.SIGXCPU:
314
terminate = "CPU Time Limit Exceeded"
316
def dispatch_msg(msg):
318
if msg['cmd'] == 'restart':
319
terminate = "User requested console be reset"
321
raise common.chat.Terminate({"restart":terminate})
323
lineQ.put({msg['cmd']:msg['text']})
325
raise common.chat.Terminate({"restart":terminate})
328
def format_exc_start(start=0):
329
etype, value, tb = sys.exc_info()
330
tbbits = traceback.extract_tb(tb)[start:]
331
list = ['Traceback (most recent call last):\n']
332
list = list + traceback.format_list(tbbits)
333
list = list + traceback.format_exception_only(etype, value)
336
def incomplete_utf8_sequence(byteseq):
339
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
340
the end of the string which comprise an incomplete UTF-8 character
343
If the string is empty or ends with a complete character OR INVALID
345
Otherwise, returns 1-3 indicating the number of bytes in the final
346
incomplete (but valid) character sequence.
348
Does not check any bytes before the final sequence for correctness.
350
>>> incomplete_utf8_sequence("")
352
>>> incomplete_utf8_sequence("xy")
354
>>> incomplete_utf8_sequence("xy\xc3\xbc")
356
>>> incomplete_utf8_sequence("\xc3")
358
>>> incomplete_utf8_sequence("\xbc\xc3")
360
>>> incomplete_utf8_sequence("xy\xbc\xc3")
362
>>> incomplete_utf8_sequence("xy\xe0\xa0")
364
>>> incomplete_utf8_sequence("xy\xf4")
366
>>> incomplete_utf8_sequence("xy\xf4\x8f")
368
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
373
for b in byteseq[::-1]:
377
# 0xxxxxxx (single-byte character)
380
elif b & 0xc0 == 0x80:
381
# 10xxxxxx (subsequent byte)
383
elif b & 0xe0 == 0xc0:
384
# 110xxxxx (start of 2-byte sequence)
387
elif b & 0xf0 == 0xe0:
388
# 1110xxxx (start of 3-byte sequence)
391
elif b & 0xf8 == 0xf0:
392
# 11110xxx (start of 4-byte sequence)
400
# Seen too many "subsequent bytes", invalid
404
# We never saw a "first byte", invalid
407
# We now know expect and count
409
# Complete, or we saw an invalid sequence
415
# Takes an object and returns a flattened version suitable for JSON
420
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
425
if __name__ == "__main__":
426
port = int(sys.argv[1])
429
# Sanitise the Enviroment
431
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
433
if len(sys.argv) >= 4:
435
os.chdir(sys.argv[3])
436
os.environ['HOME'] = sys.argv[3]
438
# Make python's search path follow the cwd
441
common.chat.start_server(port, magic, True, dispatch_msg, initializer)