4
# python-console <port> <magic> [<working-dir>]
17
from threading import Thread
22
# This version must be supported by both the local and remote code
25
class Interrupt(Exception):
27
Exception.__init__(self, "Interrupted!")
29
class ExpiryTimer(object):
30
def __init__(self, idle):
32
signal.signal(signal.SIGALRM, self.timeout)
35
signal.alarm(self.idle)
37
def start(self, time):
43
def timeout(self, signum, frame):
46
class StdinFromWeb(object):
47
def __init__(self, cmdQ, lineQ):
52
self.cmdQ.put({"input":None})
60
class StdoutToWeb(object):
61
def __init__(self, cmdQ, lineQ):
66
def _trim_incomplete_final(self, stuff):
67
'''Trim an incomplete UTF-8 character from the end of a string.
68
Returns (trimmed_string, count_of_trimmed_bytes).
70
tokill = common.util.incomplete_utf8_sequence(stuff)
72
return (stuff, tokill)
74
return (stuff[:-tokill], tokill)
76
def write(self, stuff):
77
# print will only give a non-file a unicode or str. There's no way
78
# to convince it to encode unicodes, so we have to do it ourselves.
79
# Yay for file special-cases (fileobject.c, PyFile_WriteObject).
80
# If somebody wants to write some other object to here, they do it
82
if isinstance(stuff, unicode):
83
stuff = stuff.encode('utf-8')
84
self.remainder = self.remainder + stuff
86
# if there's less than 128 bytes, buffer
87
if len(self.remainder) < 128:
90
# if there's lots, then send it in 1/2K blocks
91
while len(self.remainder) > 512:
92
# We send things as Unicode inside JSON, so we must only send
93
# complete UTF-8 characters.
94
(blk, count) = self._trim_incomplete_final(self.remainder[:512])
95
self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
98
self.remainder = self.remainder[512 - count:]
100
# Finally, split the remainder up into lines, and ship all the
101
# completed lines off to the server.
102
lines = self.remainder.split("\n")
103
self.remainder = lines[-1]
108
text = "\n".join(lines)
109
self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
111
ln = self.lineQ.get()
112
if 'interrupt' in ln:
116
if len(self.remainder) > 0:
117
(out, count) = self._trim_incomplete_final(self.remainder)
118
self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
120
ln = self.lineQ.get()
121
# Leave incomplete characters in the buffer.
122
# Yes, this does mean that an incomplete character will be left
123
# off the end, but we discussed this and it was deemed best.
124
self.remainder = self.remainder[len(self.remainder)-count:]
125
if 'interrupt' in ln:
129
"""Provides a file like interface to the Web front end of the console.
130
You may print text to the console using write(), flush any buffered output
131
using flush(), or request text from the console using readline()"""
133
def __init__(self, cmdQ, lineQ):
136
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
137
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
139
def write(self, stuff):
140
self.stdout.write(stuff)
147
return self.stdin.readline()
149
class PythonRunner(Thread):
150
def __init__(self, cmdQ, lineQ):
153
self.webio = WebIO(self.cmdQ, self.lineQ)
154
Thread.__init__(self)
156
def execCmd(self, cmd):
158
sys.stdin = self.webio
159
sys.stdout = self.webio
160
sys.stderr = self.webio
161
# We don't expect a return value - 'single' symbol prints it.
164
self.cmdQ.put({"okay": None})
167
tb = format_exc_start(start=1)
169
self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
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')})
207
elif 'globals' in ln:
208
# Unpickle the new space (if provided)
209
if isinstance(ln['globals'],dict):
211
for g in ln['globals']:
213
self.globs[g] = cPickle.loads(ln['globals'][g])
217
# Return the current globals
218
self.cmdQ.put({'globals': flatten(self.globs)})
221
sys.stdin = self.webio
222
sys.stdout = self.webio
223
sys.stderr = self.webio
225
if isinstance(ln['call'], dict):
229
if isinstance(params['args'], list):
230
args = map(self.eval, params['args'])
233
if isinstance(params['kwargs'], dict):
235
for kwarg in params['kwargs']:
236
kwargs[kwarg] = self.eval(
237
params['kwargs'][kwarg])
242
function = self.eval(params['function'])
244
call['result'] = function(*args, **kwargs)
247
tb = format_exc_start(start=1)
248
exception['traceback'] = \
249
''.join(tb).decode('utf-8', 'replace')
250
exception['except'] = cPickle.dumps(e,
252
call['exception'] = exception
254
tb = format_exc_start(start=1)
256
{"exc": ''.join(tb).decode('utf-8', 'replace')})
258
# Write out the inspection object
261
self.cmdQ.put({'response': 'failure'})
263
elif 'execute' in ln:
264
# Like block but return a serialization of the state
265
# throw away partial command
266
response = {'okay': None}
267
sys.stdin = self.webio
268
sys.stdout = self.webio
269
sys.stderr = self.webio
271
cmd = compile(ln['execute'], "<web session>", 'exec');
272
# We don't expect a return value - 'single' symbol prints
276
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
282
# Return the inspection object
283
self.cmdQ.put(response)
285
# Clear any previous command
287
elif 'set_vars' in ln:
288
# Adds some variables to the global dictionary
289
for var in ln['set_vars']:
291
self.globs[var] = self.eval(ln['set_vars'][var])
293
tb = format_exc_start(start=1)
295
{"exc": ''.join(tb).decode('utf-8', 'replace')})
297
self.cmdQ.put({'okay': None})
299
raise Exception, "Invalid Command"
301
def eval(self, source):
302
""" Evaluates a string in the private global space """
303
return eval(source, self.globs)
306
if os.fork(): # launch child and...
307
os._exit(0) # kill off parent
309
if os.fork(): # launch child and...
310
os._exit(0) # kill off parent again.
313
# The global 'magic' is the secret that the client and server share
314
# which is used to create and md5 digest to authenticate requests.
315
# It is assigned a real value at startup.
319
lineQ = Queue.Queue()
320
interpThread = PythonRunner(cmdQ, lineQ)
323
# Default expiry time of 15 minutes
324
expiry = ExpiryTimer(15 * 60)
327
interpThread.setDaemon(True)
329
signal.signal(signal.SIGXCPU, sig_handler)
332
def sig_handler(signum, frame):
333
"""Handles response from signals"""
335
if signum == signal.SIGXCPU:
336
terminate = "CPU Time Limit Exceeded"
338
def dispatch_msg(msg):
340
if msg['cmd'] == 'terminate':
341
terminate = "User requested console be terminated"
343
raise common.chat.Terminate({"terminate":terminate})
345
lineQ.put({msg['cmd']:msg['text']})
347
raise common.chat.Terminate({"terminate":terminate})
350
def format_exc_start(start=0):
351
etype, value, tb = sys.exc_info()
352
tbbits = traceback.extract_tb(tb)[start:]
353
list = ['Traceback (most recent call last):\n']
354
list = list + traceback.format_list(tbbits)
355
list = list + traceback.format_exception_only(etype, value)
359
# Takes an object and returns a flattened version suitable for JSON
364
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
367
o_type = type(object[o]).__name__
368
o_name = object[o].__name__
369
fake_o = common.util.FakeObject(o_type, o_name)
370
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
371
except AttributeError:
375
if __name__ == "__main__":
376
port = int(sys.argv[1])
379
# Sanitise the Enviroment
381
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
383
if len(sys.argv) >= 4:
385
os.chdir(sys.argv[3])
386
os.environ['HOME'] = sys.argv[3]
388
# Make python's search path follow the cwd
391
common.chat.start_server(port, magic, True, dispatch_msg, initializer)