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})
56
class StdoutToWeb(object):
57
def __init__(self, cmdQ, lineQ):
62
def _trim_incomplete_final(self, stuff):
63
'''Trim an incomplete UTF-8 character from the end of a string.
64
Returns (trimmed_string, count_of_trimmed_bytes).
66
tokill = incomplete_utf8_sequence(stuff)
68
return (stuff, tokill)
70
return (stuff[:-tokill], tokill)
72
def write(self, stuff):
73
# print will only give a non-file a unicode or str. There's no way
74
# to convince it to encode unicodes, so we have to do it ourselves.
75
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
76
# If somebody wants to write some other object to here, they do it
78
if isinstance(stuff, unicode):
79
stuff = stuff.encode('utf-8')
80
self.remainder = self.remainder + stuff
82
# if there's less than 128 bytes, buffer
83
if len(self.remainder) < 128:
86
# if there's lots, then send it in 1/2K blocks
87
while len(self.remainder) > 512:
88
# We send things as Unicode inside JSON, so we must only send
89
# complete UTF-8 characters.
90
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
91
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
94
self.remainder = self.remainder[512 - count:]
96
# Finally, split the remainder up into lines, and ship all the
97
# completed lines off to the server.
98
lines = self.remainder.split("\n")
99
self.remainder = lines[-1]
104
text = "\n".join(lines)
105
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
107
ln = self.lineQ.get()
108
if 'interrupt' in ln:
112
if len(self.remainder) > 0:
113
(out, count) = self._trim_incomplete_final(self.remainder)
114
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
116
ln = self.lineQ.get()
117
# Leave incomplete characters in the buffer.
118
# Yes, this does mean that an incomplete character will be left
119
# off the end, but we discussed this and it was deemed best.
120
self.remainder = self.remainder[len(self.remainder)-count:]
121
if 'interrupt' in ln:
125
"""Provides a file like interface to the Web front end of the console.
126
You may print text to the console using write(), flush any buffered output
127
using flush(), or request text from the console using readline()"""
129
def __init__(self, cmdQ, lineQ):
132
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
133
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
135
def write(self, stuff):
136
self.stdout.write(stuff)
143
return self.stdin.readline()
145
class PythonRunner(Thread):
146
def __init__(self, cmdQ, lineQ):
149
self.webio = WebIO(self.cmdQ, self.lineQ)
150
Thread.__init__(self)
152
def execCmd(self, cmd):
154
sys.stdin = self.webio
155
sys.stdout = self.webio
156
sys.stderr = self.webio
157
# We don't expect a return value - 'single' symbol prints it.
158
eval(cmd, self.globs)
160
self.cmdQ.put({"okay": None})
163
tb = format_exc_start(start=1)
165
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
170
self.globs['__builtins__'] = globals()['__builtins__']
174
ln = self.lineQ.get()
176
if self.curr_cmd == '':
177
self.curr_cmd = ln['chat']
179
self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
181
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
183
# The command was incomplete,
184
# so send back a None, so the
185
# client can print a '...'
186
self.cmdQ.put({"more":None})
190
tb = format_exc_start(start=3)
191
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
195
# throw away a partial command.
197
cmd = compile(ln['block'], "<web session>", 'exec');
200
tb = format_exc_start(start=1)
202
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
206
if os.fork(): # launch child and...
207
os._exit(0) # kill off parent
209
if os.fork(): # launch child and...
210
os._exit(0) # kill off parent again.
213
# The global 'magic' is the secret that the client and server share
214
# which is used to create and md5 digest to authenticate requests.
215
# It is assigned a real value at startup.
219
lineQ = Queue.Queue()
220
interpThread = PythonRunner(cmdQ, lineQ)
223
# Default expiry time of 15 minutes
224
expiry = ExpiryTimer(15 * 60)
227
interpThread.setDaemon(True)
229
signal.signal(signal.SIGXCPU, sig_handler)
232
def sig_handler(signum, frame):
233
"""Handles response from signals"""
235
if signum == signal.SIGXCPU:
236
terminate = "CPU Time Limit Exceeded"
238
def dispatch_msg(msg):
240
lineQ.put({msg['cmd']:msg['text']})
242
raise common.chat.Terminate({"restart":terminate})
245
def format_exc_start(start=0):
246
etype, value, tb = sys.exc_info()
247
tbbits = traceback.extract_tb(tb)[start:]
248
list = ['Traceback (most recent call last):\n']
249
list = list + traceback.format_list(tbbits)
250
list = list + traceback.format_exception_only(etype, value)
253
def incomplete_utf8_sequence(byteseq):
256
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
257
the end of the string which comprise an incomplete UTF-8 character
260
If the string is empty or ends with a complete character OR INVALID
262
Otherwise, returns 1-3 indicating the number of bytes in the final
263
incomplete (but valid) character sequence.
265
Does not check any bytes before the final sequence for correctness.
267
>>> incomplete_utf8_sequence("")
269
>>> incomplete_utf8_sequence("xy")
271
>>> incomplete_utf8_sequence("xy\xc3\xbc")
273
>>> incomplete_utf8_sequence("\xc3")
275
>>> incomplete_utf8_sequence("\xbc\xc3")
277
>>> incomplete_utf8_sequence("xy\xbc\xc3")
279
>>> incomplete_utf8_sequence("xy\xe0\xa0")
281
>>> incomplete_utf8_sequence("xy\xf4")
283
>>> incomplete_utf8_sequence("xy\xf4\x8f")
285
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
290
for b in byteseq[::-1]:
294
# 0xxxxxxx (single-byte character)
297
elif b & 0xc0 == 0x80:
298
# 10xxxxxx (subsequent byte)
300
elif b & 0xe0 == 0xc0:
301
# 110xxxxx (start of 2-byte sequence)
304
elif b & 0xf0 == 0xe0:
305
# 1110xxxx (start of 3-byte sequence)
308
elif b & 0xf8 == 0xf0:
309
# 11110xxx (start of 4-byte sequence)
317
# Seen too many "subsequent bytes", invalid
321
# We never saw a "first byte", invalid
324
# We now know expect and count
326
# Complete, or we saw an invalid sequence
332
if __name__ == "__main__":
333
port = int(sys.argv[1])
336
# Sanitise the Enviroment
338
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
340
if len(sys.argv) >= 4:
342
os.chdir(sys.argv[3])
343
os.environ['HOME'] = sys.argv[3]
345
# Make python's search path follow the cwd
348
common.chat.start_server(port, magic, True, dispatch_msg, initializer)