4
# python-console <port> <magic> [<working-dir>]
15
from threading import Thread
16
from functools import partial
20
class Interrupt(Exception):
22
Exception.__init__(self, "Interrupted!")
24
class ExpiryTimer(object):
25
def __init__(self, idle):
27
signal.signal(signal.SIGALRM, partial(self.timeout))
30
signal.alarm(self.idle)
32
def start(self, time):
38
def timeout(self, signum, frame):
41
class StdinFromWeb(object):
42
def __init__(self, cmdQ, lineQ):
47
self.cmdQ.put({"input":None})
58
class StdoutToWeb(object):
59
def __init__(self, cmdQ, lineQ):
64
def _trim_incomplete_final(self, stuff):
65
'''Trim an incomplete UTF-8 character from the end of a string.
66
Returns (trimmed_string, count_of_trimmed_bytes).
68
tokill = incomplete_utf8_sequence(stuff)
70
return (stuff, tokill)
72
return (stuff[:-tokill], tokill)
74
def write(self, stuff):
75
# print will only give a non-file a unicode or str. There's no way
76
# to convince it to encode unicodes, so we have to do it ourselves.
77
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
78
# If somebody wants to write some other object to here, they do it
80
if isinstance(stuff, unicode):
81
stuff = stuff.encode('utf-8')
82
self.remainder = self.remainder + stuff
84
# if there's less than 128 bytes, buffer
85
if len(self.remainder) < 128:
88
# if there's lots, then send it in 1/2K blocks
89
while len(self.remainder) > 512:
90
# We send things as Unicode inside JSON, so we must only send
91
# complete UTF-8 characters.
92
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
93
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
96
self.remainder = self.remainder[512 - count:]
98
# Finally, split the remainder up into lines, and ship all the
99
# completed lines off to the server.
100
lines = self.remainder.split("\n")
101
self.remainder = lines[-1]
106
text = "\n".join(lines)
107
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
109
ln = self.lineQ.get()
110
if 'interrupt' in ln:
114
if len(self.remainder) > 0:
115
(out, count) = self._trim_incomplete_final(self.remainder)
116
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
118
ln = self.lineQ.get()
119
# Leave incomplete characters in the buffer.
120
# Yes, this does mean that an incomplete character will be left
121
# off the end, but we discussed this and it was deemed best.
122
self.remainder = self.remainder[len(self.remainder)-count:]
123
if 'interrupt' in ln:
127
"""Provides a file like interface to the Web front end of the console.
128
You may print text to the console using write(), flush any buffered output
129
using flush(), or request text from the console using readline()"""
131
def __init__(self, cmdQ, lineQ):
134
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
135
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
137
def write(self, stuff):
138
self.stdout.write(stuff)
145
return self.stdin.readline()
147
class PythonRunner(Thread):
148
def __init__(self, cmdQ, lineQ):
151
self.webio = WebIO(self.cmdQ, self.lineQ)
152
Thread.__init__(self)
154
def execCmd(self, cmd):
156
sys.stdin = self.webio
157
sys.stdout = self.webio
158
sys.stderr = self.webio
159
# We don't expect a return value - 'single' symbol prints it.
160
eval(cmd, self.globs)
162
self.cmdQ.put({"okay": None})
165
tb = format_exc_start(start=1)
167
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
172
self.globs['__builtins__'] = globals()['__builtins__']
176
ln = self.lineQ.get()
178
if self.curr_cmd == '':
179
self.curr_cmd = ln['chat']
181
self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
183
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
185
# The command was incomplete,
186
# so send back a None, so the
187
# client can print a '...'
188
self.cmdQ.put({"more":None})
192
tb = format_exc_start(start=3)
193
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
197
# throw away a partial command.
199
cmd = compile(ln['block'], "<web session>", 'exec');
202
tb = format_exc_start(start=1)
204
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
208
if os.fork(): # launch child and...
209
os._exit(0) # kill off parent
211
if os.fork(): # launch child and...
212
os._exit(0) # kill off parent again.
215
# The global 'magic' is the secret that the client and server share
216
# which is used to create and md5 digest to authenticate requests.
217
# It is assigned a real value at startup.
221
lineQ = Queue.Queue()
222
interpThread = PythonRunner(cmdQ, lineQ)
224
# Default expiry time of 15 minutes
225
expiry = ExpiryTimer(15 * 60)
228
interpThread.setDaemon(True)
232
def dispatch_msg(msg):
234
lineQ.put({msg['cmd']:msg['text']})
237
def format_exc_start(start=0):
238
etype, value, tb = sys.exc_info()
239
tbbits = traceback.extract_tb(tb)[start:]
240
list = ['Traceback (most recent call last):\n']
241
list = list + traceback.format_list(tbbits)
242
list = list + traceback.format_exception_only(etype, value)
245
def incomplete_utf8_sequence(byteseq):
248
Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
249
the end of the string which comprise an incomplete UTF-8 character
252
If the string is empty or ends with a complete character OR INVALID
254
Otherwise, returns 1-3 indicating the number of bytes in the final
255
incomplete (but valid) character sequence.
257
Does not check any bytes before the final sequence for correctness.
259
>>> incomplete_utf8_sequence("")
261
>>> incomplete_utf8_sequence("xy")
263
>>> incomplete_utf8_sequence("xy\xc3\xbc")
265
>>> incomplete_utf8_sequence("\xc3")
267
>>> incomplete_utf8_sequence("\xbc\xc3")
269
>>> incomplete_utf8_sequence("xy\xbc\xc3")
271
>>> incomplete_utf8_sequence("xy\xe0\xa0")
273
>>> incomplete_utf8_sequence("xy\xf4")
275
>>> incomplete_utf8_sequence("xy\xf4\x8f")
277
>>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
282
for b in byteseq[::-1]:
286
# 0xxxxxxx (single-byte character)
289
elif b & 0xc0 == 0x80:
290
# 10xxxxxx (subsequent byte)
292
elif b & 0xe0 == 0xc0:
293
# 110xxxxx (start of 2-byte sequence)
296
elif b & 0xf0 == 0xe0:
297
# 1110xxxx (start of 3-byte sequence)
300
elif b & 0xf8 == 0xf0:
301
# 11110xxx (start of 4-byte sequence)
309
# Seen too many "subsequent bytes", invalid
313
# We never saw a "first byte", invalid
316
# We now know expect and count
318
# Complete, or we saw an invalid sequence
324
if __name__ == "__main__":
325
port = int(sys.argv[1])
327
if len(sys.argv) >= 4:
329
os.chdir(sys.argv[3])
330
# Make python's search path follow the cwd
332
os.environ['HOME'] = sys.argv[3]
334
common.chat.start_server(port, magic, True, dispatch_msg, initializer)