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)
339
if os.fork(): # launch child and...
340
os._exit(0) # kill off parent
342
if os.fork(): # launch child and...
343
os._exit(0) # kill off parent again.
346
# The global 'magic' is the secret that the client and server share
347
# which is used to create and md5 digest to authenticate requests.
348
# It is assigned a real value at startup.
352
lineQ = Queue.Queue()
353
interpThread = PythonRunner(cmdQ, lineQ)
356
# Default expiry time of 15 minutes
357
expiry = ExpiryTimer(15 * 60)
360
interpThread.setDaemon(True)
362
signal.signal(signal.SIGXCPU, sig_handler)
365
def sig_handler(signum, frame):
366
"""Handles response from signals"""
368
if signum == signal.SIGXCPU:
369
terminate = "CPU Time Limit Exceeded"
371
def dispatch_msg(msg):
373
if msg['cmd'] == 'terminate':
374
terminate = "User requested console be terminated"
376
raise common.chat.Terminate({"terminate":terminate})
378
lineQ.put((msg['cmd'],msg['text']))
380
raise common.chat.Terminate({"terminate":terminate})
383
def format_exc_start(start=0):
384
etype, value, tb = sys.exc_info()
385
tbbits = traceback.extract_tb(tb)[start:]
386
list = ['Traceback (most recent call last):\n']
387
list = list + traceback.format_list(tbbits)
388
list = list + traceback.format_exception_only(etype, value)
392
# Takes an object and returns a flattened version suitable for JSON
397
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
400
o_type = type(object[o]).__name__
401
o_name = object[o].__name__
402
fake_o = common.util.FakeObject(o_type, o_name)
403
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
404
except AttributeError:
408
if __name__ == "__main__":
409
port = int(sys.argv[1])
412
# Sanitise the Enviroment
414
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
416
if len(sys.argv) >= 4:
418
os.chdir(sys.argv[3])
419
os.environ['HOME'] = sys.argv[3]
421
# Make python's search path follow the cwd
424
common.chat.start_server(port, magic, True, dispatch_msg, initializer)