4
# python-console <port> <magic> [<working-dir>]
15
from threading import Thread
19
class Interrupt(Exception):
21
Exception.__init__(self, "Interrupted!")
23
class ExpiryTimer(object):
24
def __init__(self, idle):
26
signal.signal(signal.SIGALRM, self.timeout)
29
signal.alarm(self.idle)
31
def start(self, time):
37
def timeout(self, signum, frame):
40
class StdinFromWeb(object):
41
def __init__(self, cmdQ, lineQ):
46
self.cmdQ.put({"input":None})
54
class StdoutToWeb(object):
55
def __init__(self, cmdQ, lineQ):
60
def _trim_incomplete_final(self, stuff):
61
'''Trim an incomplete UTF-8 character from the end of a string.
62
Returns (trimmed_string, count_of_trimmed_bytes).
64
tokill = incomplete_utf8_sequence(stuff)
66
return (stuff, tokill)
68
return (stuff[:-tokill], tokill)
70
def write(self, stuff):
71
# print will only give a non-file a unicode or str. There's no way
72
# to convince it to encode unicodes, so we have to do it ourselves.
73
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
74
# If somebody wants to write some other object to here, they do it
76
if isinstance(stuff, unicode):
77
stuff = stuff.encode('utf-8')
78
self.remainder = self.remainder + stuff
80
# if there's less than 128 bytes, buffer
81
if len(self.remainder) < 128:
84
# if there's lots, then send it in 1/2K blocks
85
while len(self.remainder) > 512:
86
# We send things as Unicode inside JSON, so we must only send
87
# complete UTF-8 characters.
88
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
89
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
92
self.remainder = self.remainder[512 - count:]
94
# Finally, split the remainder up into lines, and ship all the
95
# completed lines off to the server.
96
lines = self.remainder.split("\n")
97
self.remainder = lines[-1]
102
text = "\n".join(lines)
103
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
105
ln = self.lineQ.get()
106
if 'interrupt' in ln:
110
if len(self.remainder) > 0:
111
(out, count) = self._trim_incomplete_final(self.remainder)
112
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
114
ln = self.lineQ.get()
115
# Leave incomplete characters in the buffer.
116
# Yes, this does mean that an incomplete character will be left
117
# off the end, but we discussed this and it was deemed best.
118
self.remainder = self.remainder[len(self.remainder)-count:]
119
if 'interrupt' in ln:
123
"""Provides a file like interface to the Web front end of the console.
124
You may print text to the console using write(), flush any buffered output
125
using flush(), or request text from the console using readline()"""
127
def __init__(self, cmdQ, lineQ):
130
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
131
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
133
def write(self, stuff):
134
self.stdout.write(stuff)
141
return self.stdin.readline()
143
class PythonRunner(Thread):
144
def __init__(self, cmdQ, lineQ):
147
self.webio = WebIO(self.cmdQ, self.lineQ)
148
Thread.__init__(self)
150
def execCmd(self, cmd):
152
sys.stdin = self.webio
153
sys.stdout = self.webio
154
sys.stderr = self.webio
155
# We don't expect a return value - 'single' symbol prints it.
156
eval(cmd, self.globs)
158
self.cmdQ.put({"okay": None})
161
tb = format_exc_start(start=1)
163
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
168
self.globs['__builtins__'] = globals()['__builtins__']
172
ln = self.lineQ.get()
174
if self.curr_cmd == '':
175
self.curr_cmd = ln['chat']
177
self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
179
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
181
# The command was incomplete,
182
# so send back a None, so the
183
# client can print a '...'
184
self.cmdQ.put({"more":None})
188
tb = format_exc_start(start=3)
189
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
193
# throw away a partial command.
195
cmd = compile(ln['block'], "<web session>", 'exec');
198
tb = format_exc_start(start=1)
200
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
204
if os.fork(): # launch child and...
205
os._exit(0) # kill off parent
207
if os.fork(): # launch child and...
208
os._exit(0) # kill off parent again.
211
# The global 'magic' is the secret that the client and server share
212
# which is used to create and md5 digest to authenticate requests.
213
# It is assigned a real value at startup.
217
lineQ = Queue.Queue()
218
interpThread = PythonRunner(cmdQ, lineQ)
221
# Default expiry time of 15 minutes
222
expiry = ExpiryTimer(15 * 60)
225
interpThread.setDaemon(True)
227
signal.signal(signal.SIGXCPU, sig_handler)
230
def sig_handler(signum, frame):
231
"""Handles response from signals"""
233
if signum == signal.SIGXCPU:
234
terminate = "CPU Time Limit Exceeded"
236
def dispatch_msg(msg):
238
if msg['cmd'] == 'restart':
239
terminate = "User requested console be reset"
241
raise common.chat.Terminate({"restart":terminate})
243
lineQ.put({msg['cmd']:msg['text']})
245
raise common.chat.Terminate({"restart":terminate})
248
def format_exc_start(start=0):
249
etype, value, tb = sys.exc_info()
250
tbbits = traceback.extract_tb(tb)[start:]
251
list = ['Traceback (most recent call last):\n']
252
list = list + traceback.format_list(tbbits)
253
list = list + traceback.format_exception_only(etype, value)
256
def incomplete_utf8_sequence(byteseq):
259
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
260
the end of the string which comprise an incomplete UTF-8 character
263
If the string is empty or ends with a complete character OR INVALID
265
Otherwise, returns 1-3 indicating the number of bytes in the final
266
incomplete (but valid) character sequence.
268
Does not check any bytes before the final sequence for correctness.
270
>>> incomplete_utf8_sequence("")
272
>>> incomplete_utf8_sequence("xy")
274
>>> incomplete_utf8_sequence("xy\xc3\xbc")
276
>>> incomplete_utf8_sequence("\xc3")
278
>>> incomplete_utf8_sequence("\xbc\xc3")
280
>>> incomplete_utf8_sequence("xy\xbc\xc3")
282
>>> incomplete_utf8_sequence("xy\xe0\xa0")
284
>>> incomplete_utf8_sequence("xy\xf4")
286
>>> incomplete_utf8_sequence("xy\xf4\x8f")
288
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
293
for b in byteseq[::-1]:
297
# 0xxxxxxx (single-byte character)
300
elif b & 0xc0 == 0x80:
301
# 10xxxxxx (subsequent byte)
303
elif b & 0xe0 == 0xc0:
304
# 110xxxxx (start of 2-byte sequence)
307
elif b & 0xf0 == 0xe0:
308
# 1110xxxx (start of 3-byte sequence)
311
elif b & 0xf8 == 0xf0:
312
# 11110xxx (start of 4-byte sequence)
320
# Seen too many "subsequent bytes", invalid
324
# We never saw a "first byte", invalid
327
# We now know expect and count
329
# Complete, or we saw an invalid sequence
335
if __name__ == "__main__":
336
port = int(sys.argv[1])
339
# Sanitise the Enviroment
341
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
343
if len(sys.argv) >= 4:
345
os.chdir(sys.argv[3])
346
os.environ['HOME'] = sys.argv[3]
348
# Make python's search path follow the cwd
351
common.chat.start_server(port, magic, True, dispatch_msg, initializer)