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.
161
eval(cmd, self.globs)
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):
223
args = params['args']
226
if 'kwargs' in params:
227
kwargs = params['kwargs']
232
function = cPickle.loads(params['function'])
233
result = function(*args, **kwargs)
234
self.cmdQ.put({'output': result})
236
self.cmdQ.put({'response': 'failure: %s'%repr(e)})
238
self.cmdQ.put({'response': 'failure'})
239
elif 'inspect' in ln:
240
# Like block but return a serialization of the state
241
# throw away partial command
243
stdout = cStringIO.StringIO()
244
stderr = cStringIO.StringIO()
246
cmd = compile(ln['inspect'], "<web session>", 'exec');
250
# We don't expect a return value - 'single' symbol prints
252
eval(cmd, self.globs)
255
tb = format_exc_start(start=1)
256
exception['traceback'] = \
257
''.join(tb).decode('utf-8', 'replace')
258
exception['except'] = cPickle.dumps(e, PICKLEVERSION)
259
inspection['exception'] = exception
261
# Write out the inspection object
262
inspection['stdout'] = stdout.getvalue()
263
inspection['stderr'] = stderr.getvalue()
264
inspection['globals'] = flatten(self.globs)
265
self.cmdQ.put(inspection)
270
raise Exception, "Invalid Command"
273
if os.fork(): # launch child and...
274
os._exit(0) # kill off parent
276
if os.fork(): # launch child and...
277
os._exit(0) # kill off parent again.
280
# The global 'magic' is the secret that the client and server share
281
# which is used to create and md5 digest to authenticate requests.
282
# It is assigned a real value at startup.
286
lineQ = Queue.Queue()
287
interpThread = PythonRunner(cmdQ, lineQ)
290
# Default expiry time of 15 minutes
291
expiry = ExpiryTimer(15 * 60)
294
interpThread.setDaemon(True)
296
signal.signal(signal.SIGXCPU, sig_handler)
299
def sig_handler(signum, frame):
300
"""Handles response from signals"""
302
if signum == signal.SIGXCPU:
303
terminate = "CPU Time Limit Exceeded"
305
def dispatch_msg(msg):
307
if msg['cmd'] == 'restart':
308
terminate = "User requested console be reset"
310
raise common.chat.Terminate({"restart":terminate})
312
lineQ.put({msg['cmd']:msg['text']})
314
raise common.chat.Terminate({"restart":terminate})
317
def format_exc_start(start=0):
318
etype, value, tb = sys.exc_info()
319
tbbits = traceback.extract_tb(tb)[start:]
320
list = ['Traceback (most recent call last):\n']
321
list = list + traceback.format_list(tbbits)
322
list = list + traceback.format_exception_only(etype, value)
325
def incomplete_utf8_sequence(byteseq):
328
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
329
the end of the string which comprise an incomplete UTF-8 character
332
If the string is empty or ends with a complete character OR INVALID
334
Otherwise, returns 1-3 indicating the number of bytes in the final
335
incomplete (but valid) character sequence.
337
Does not check any bytes before the final sequence for correctness.
339
>>> incomplete_utf8_sequence("")
341
>>> incomplete_utf8_sequence("xy")
343
>>> incomplete_utf8_sequence("xy\xc3\xbc")
345
>>> incomplete_utf8_sequence("\xc3")
347
>>> incomplete_utf8_sequence("\xbc\xc3")
349
>>> incomplete_utf8_sequence("xy\xbc\xc3")
351
>>> incomplete_utf8_sequence("xy\xe0\xa0")
353
>>> incomplete_utf8_sequence("xy\xf4")
355
>>> incomplete_utf8_sequence("xy\xf4\x8f")
357
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
362
for b in byteseq[::-1]:
366
# 0xxxxxxx (single-byte character)
369
elif b & 0xc0 == 0x80:
370
# 10xxxxxx (subsequent byte)
372
elif b & 0xe0 == 0xc0:
373
# 110xxxxx (start of 2-byte sequence)
376
elif b & 0xf0 == 0xe0:
377
# 1110xxxx (start of 3-byte sequence)
380
elif b & 0xf8 == 0xf0:
381
# 11110xxx (start of 4-byte sequence)
389
# Seen too many "subsequent bytes", invalid
393
# We never saw a "first byte", invalid
396
# We now know expect and count
398
# Complete, or we saw an invalid sequence
404
# Takes an object and returns a flattened version suitable for JSON
409
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
414
if __name__ == "__main__":
415
port = int(sys.argv[1])
418
# Sanitise the Enviroment
420
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
422
if len(sys.argv) >= 4:
424
os.chdir(sys.argv[3])
425
os.environ['HOME'] = sys.argv[3]
427
# Make python's search path follow the cwd
430
common.chat.start_server(port, magic, True, dispatch_msg, initializer)