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})
54
action, params = self.lineQ.get()
57
elif action == 'interrupt':
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')})
97
action, params = self.lineQ.get()
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
action, params = self.lineQ.get()
112
if action == 'interrupt':
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
action, params = 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 action == 'interrupt':
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()"""
132
# FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to
133
# be able to deal with the streams individually.
135
def __init__(self, cmdQ, lineQ):
138
self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
139
self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
141
def write(self, stuff):
142
self.stdout.write(stuff)
149
return self.stdin.readline()
151
class PythonRunner(Thread):
152
def __init__(self, cmdQ, lineQ):
155
self.webio = WebIO(self.cmdQ, self.lineQ)
156
Thread.__init__(self)
158
def execCmd(self, cmd):
160
# We don't expect a return value - 'single' symbol prints it.
164
return({"okay": None})
168
tb = format_exc_start(start=2)
169
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
172
# Set up global space and partial command buffer
176
# Set up I/O to use web interface
177
sys.stdin = self.webio
178
sys.stdout = self.webio
179
sys.stderr = self.webio
181
# Handlers for each action
183
'chat': self.handle_chat,
184
'block': self.handle_block,
185
'globals': self.handle_globals,
186
'call': self.handle_call,
187
'execute': self.handle_execute,
188
'setvars': self.handle_setvars,
191
# Run the processing loop
193
action, params = self.lineQ.get()
195
response = actions[action](params)
197
response = {'error': repr(e)}
199
self.cmdQ.put(response)
201
def handle_chat(self, params):
202
# Set up the partial cmd buffer
203
if self.curr_cmd == '':
204
self.curr_cmd = params
206
self.curr_cmd = self.curr_cmd + '\n' + params
208
# Try to execute the buffer
210
cmd = codeop.compile_command(self.curr_cmd, '<web session>')
212
# The command was incomplete, so send back a None, so the
213
# client can print a '...'
214
return({"more":None})
216
return(self.execCmd(cmd))
218
# Clear any partial command
220
# Flush the output buffers
223
# Return the exception
224
tb = format_exc_start(start=3)
225
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
227
def handle_block(self, params):
228
# throw away any partial command.
231
# Try to execute a complete block of code
233
cmd = compile(params, "<web session>", 'exec');
234
return(self.execCmd(cmd))
236
# Flush the output buffers
239
# Return the exception
240
tb = format_exc_start(start=1)
241
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
243
def handle_globals(self, params):
244
# Unpickle the new space (if provided)
245
if isinstance(params, dict):
249
self.globs[g] = cPickle.loads(params[g])
253
# Return the current globals
254
return({'globals': flatten(self.globs)})
256
def handle_call(self, params):
259
# throw away any partial command.
262
if isinstance(params, dict):
265
if isinstance(params['args'], list):
266
args = map(self.eval, params['args'])
269
if isinstance(params['kwargs'], dict):
271
for kwarg in params['kwargs']:
272
kwargs[kwarg] = self.eval(
273
params['kwargs'][kwarg])
278
function = self.eval(params['function'])
280
call['result'] = function(*args, **kwargs)
283
tb = format_exc_start(start=1)
284
exception['traceback'] = \
285
''.join(tb).decode('utf-8', 'replace')
286
exception['except'] = cPickle.dumps(e,
288
call['exception'] = exception
290
tb = format_exc_start(start=1)
291
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
293
# Flush the output buffers
297
# Write out the inspection object
300
return({'response': 'failure'})
302
def handle_execute(self, params):
303
# throw away any partial command.
306
# Like block but return a serialization of the state
307
# throw away partial command
308
response = {'okay': None}
310
cmd = compile(params, "<web session>", 'exec');
311
# We don't expect a return value - 'single' symbol prints it.
314
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
320
# Return the inspection object
323
def handle_setvars(self, params):
324
# Adds some variables to the global dictionary
325
for var in params['set_vars']:
327
self.globs[var] = self.eval(params['set_vars'][var])
329
tb = format_exc_start(start=1)
330
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
332
return({'okay': None})
334
def eval(self, source):
335
""" Evaluates a string in the private global space """
336
return eval(source, self.globs)
338
# The global 'magic' is the secret that the client and server share
339
# which is used to create and md5 digest to authenticate requests.
340
# It is assigned a real value at startup.
344
lineQ = Queue.Queue()
345
interpThread = PythonRunner(cmdQ, lineQ)
348
# Default expiry time of 15 minutes
349
expiry = ExpiryTimer(15 * 60)
352
interpThread.setDaemon(True)
354
signal.signal(signal.SIGXCPU, sig_handler)
357
def sig_handler(signum, frame):
358
"""Handles response from signals"""
360
if signum == signal.SIGXCPU:
361
terminate = "CPU Time Limit Exceeded"
363
def dispatch_msg(msg):
365
if msg['cmd'] == 'terminate':
366
terminate = "User requested console be terminated"
368
raise common.chat.Terminate({"terminate":terminate})
370
lineQ.put((msg['cmd'],msg['text']))
371
response = cmdQ.get()
373
raise common.chat.Terminate({"terminate":terminate})
376
def format_exc_start(start=0):
377
etype, value, tb = sys.exc_info()
378
tbbits = traceback.extract_tb(tb)[start:]
379
list = ['Traceback (most recent call last):\n']
380
list = list + traceback.format_list(tbbits)
381
list = list + traceback.format_exception_only(etype, value)
385
# Takes an object and returns a flattened version suitable for JSON
390
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
393
o_type = type(object[o]).__name__
394
o_name = object[o].__name__
395
fake_o = common.util.FakeObject(o_type, o_name)
396
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
397
except AttributeError:
401
if __name__ == "__main__":
402
port = int(sys.argv[1])
405
# Sanitise the Enviroment
407
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
409
if len(sys.argv) >= 4:
411
os.chdir(sys.argv[3])
412
os.environ['HOME'] = sys.argv[3]
414
# Make python's search path follow the cwd
417
common.chat.start_server(port, magic, True, dispatch_msg, initializer)