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
self.cc = codeop.CommandCompiler()
157
Thread.__init__(self)
159
def execCmd(self, cmd):
161
# We don't expect a return value - 'single' symbol prints it.
165
return({"okay": None})
169
tb = format_exc_start(start=2)
170
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
173
# Set up global space and partial command buffer
177
# Set up I/O to use web interface
178
sys.stdin = self.webio
179
sys.stdout = self.webio
180
sys.stderr = self.webio
182
# Handlers for each action
184
'chat': self.handle_chat,
185
'block': self.handle_block,
186
'globals': self.handle_globals,
187
'call': self.handle_call,
188
'execute': self.handle_execute,
189
'setvars': self.handle_setvars,
192
# Run the processing loop
194
action, params = self.lineQ.get()
196
response = actions[action](params)
198
response = {'error': repr(e)}
200
self.cmdQ.put(response)
202
def handle_chat(self, params):
203
# Set up the partial cmd buffer
204
if self.curr_cmd == '':
205
self.curr_cmd = params
207
self.curr_cmd = self.curr_cmd + '\n' + params
209
# Try to execute the buffer
211
cmd = self.cc(self.curr_cmd, '<web session>')
213
# The command was incomplete, so send back a None, so the
214
# client can print a '...'
215
return({"more":None})
217
return(self.execCmd(cmd))
219
# Clear any partial command
221
# Flush the output buffers
224
# Return the exception
225
tb = format_exc_start(start=3)
226
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
228
def handle_block(self, params):
229
# throw away any partial command.
232
# Try to execute a complete block of code
234
cmd = compile(params, "<web session>", 'exec');
235
return(self.execCmd(cmd))
237
# Flush the output buffers
240
# Return the exception
241
tb = format_exc_start(start=1)
242
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
244
def handle_globals(self, params):
245
# Unpickle the new space (if provided)
246
if isinstance(params, dict):
250
self.globs[g] = cPickle.loads(params[g])
254
# Return the current globals
255
return({'globals': flatten(self.globs)})
257
def handle_call(self, params):
260
# throw away any partial command.
263
if isinstance(params, dict):
266
if isinstance(params['args'], list):
267
args = map(self.eval, params['args'])
270
if isinstance(params['kwargs'], dict):
272
for kwarg in params['kwargs']:
273
kwargs[kwarg] = self.eval(
274
params['kwargs'][kwarg])
279
function = self.eval(params['function'])
281
call['result'] = function(*args, **kwargs)
284
tb = format_exc_start(start=1)
285
exception['traceback'] = \
286
''.join(tb).decode('utf-8', 'replace')
287
exception['except'] = cPickle.dumps(e,
289
call['exception'] = exception
291
tb = format_exc_start(start=1)
292
call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
294
# Flush the output buffers
298
# Write out the inspection object
301
return({'response': 'failure'})
303
def handle_execute(self, params):
304
# throw away any partial command.
307
# Like block but return a serialization of the state
308
# throw away partial command
309
response = {'okay': None}
311
cmd = compile(params, "<web session>", 'exec');
312
# We don't expect a return value - 'single' symbol prints it.
315
response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
321
# Return the inspection object
324
def handle_setvars(self, params):
325
# Adds some variables to the global dictionary
326
for var in params['set_vars']:
328
self.globs[var] = self.eval(params['set_vars'][var])
330
tb = format_exc_start(start=1)
331
return({"exc": ''.join(tb).decode('utf-8', 'replace')})
333
return({'okay': None})
335
def eval(self, source):
336
""" Evaluates a string in the private global space """
337
return eval(source, self.globs)
339
# The global 'magic' is the secret that the client and server share
340
# which is used to create and md5 digest to authenticate requests.
341
# It is assigned a real value at startup.
345
lineQ = Queue.Queue()
346
interpThread = PythonRunner(cmdQ, lineQ)
349
# Default expiry time of 15 minutes
350
expiry = ExpiryTimer(15 * 60)
353
interpThread.setDaemon(True)
355
signal.signal(signal.SIGXCPU, sig_handler)
358
def sig_handler(signum, frame):
359
"""Handles response from signals"""
361
if signum == signal.SIGXCPU:
362
terminate = "CPU Time Limit Exceeded"
364
def dispatch_msg(msg):
366
if msg['cmd'] == 'terminate':
367
terminate = "User requested console be terminated"
369
raise common.chat.Terminate({"terminate":terminate})
371
lineQ.put((msg['cmd'],msg['text']))
372
response = cmdQ.get()
374
raise common.chat.Terminate({"terminate":terminate})
377
def format_exc_start(start=0):
378
etype, value, tb = sys.exc_info()
379
tbbits = traceback.extract_tb(tb)[start:]
380
list = ['Traceback (most recent call last):\n']
381
list = list + traceback.format_list(tbbits)
382
list = list + traceback.format_exception_only(etype, value)
386
# Takes an object and returns a flattened version suitable for JSON
391
flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
394
o_type = type(object[o]).__name__
395
o_name = object[o].__name__
396
fake_o = common.util.FakeObject(o_type, o_name)
397
flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
398
except AttributeError:
402
if __name__ == "__main__":
403
port = int(sys.argv[1])
406
# Sanitise the Enviroment
408
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
410
if len(sys.argv) >= 4:
412
os.chdir(sys.argv[3])
413
os.environ['HOME'] = sys.argv[3]
415
# Make python's search path follow the cwd
418
common.chat.start_server(port, magic, True, dispatch_msg, initializer)