~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: Matt Giuca
  • Date: 2010-07-20 05:45:41 UTC
  • Revision ID: matt.giuca@gmail.com-20100720054541-953ltb8rlo2vxhvq
ivle.interpret: Added 'jail_call' function, which allows regular IVLE web app code to call jail CGI functions and get their contents.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
# usage:
 
4
#   python-console <port> <magic> [<working-dir>]
 
5
 
 
6
import cjson
 
7
import codeop
 
8
import cPickle
 
9
import cStringIO
 
10
import md5
 
11
import Queue
 
12
import signal
 
13
import socket
 
14
import sys
 
15
import traceback
 
16
from threading import Thread
 
17
 
 
18
import ivle.chat
 
19
import ivle.util
 
20
 
 
21
# This version must be supported by both the local and remote code
 
22
PICKLEVERSION = 0
 
23
 
 
24
class Interrupt(Exception):
 
25
    def __init__(self):
 
26
        Exception.__init__(self, "Interrupted!")
 
27
 
 
28
class ExpiryTimer(object):
 
29
    def __init__(self, idle):
 
30
        self.idle = idle
 
31
        signal.signal(signal.SIGALRM, self.timeout)
 
32
 
 
33
    def ping(self):
 
34
        signal.alarm(self.idle)
 
35
 
 
36
    def start(self, time):
 
37
        signal.alarm(time)
 
38
 
 
39
    def stop(self):
 
40
        self.ping()
 
41
 
 
42
    def timeout(self, signum, frame):
 
43
        sys.exit(1)
 
44
 
 
45
class StdinFromWeb(object):
 
46
    def __init__(self, cmdQ, lineQ):
 
47
        self.cmdQ = cmdQ
 
48
        self.lineQ = lineQ
 
49
 
 
50
    def readline(self):
 
51
        self.cmdQ.put({"input":None})
 
52
        expiry.ping()
 
53
        action, params = self.lineQ.get()
 
54
        if action == 'chat':
 
55
            return params
 
56
        elif action == 'interrupt':
 
57
            raise Interrupt()
 
58
 
 
59
class StdoutToWeb(object):
 
60
    def __init__(self, cmdQ, lineQ):
 
61
        self.cmdQ = cmdQ
 
62
        self.lineQ = lineQ
 
63
        self.remainder = ''
 
64
 
 
65
    def _trim_incomplete_final(self, stuff):
 
66
        '''Trim an incomplete UTF-8 character from the end of a string.
 
67
           Returns (trimmed_string, count_of_trimmed_bytes).
 
68
        '''
 
69
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
 
70
        if tokill == 0:
 
71
            return (stuff, tokill)
 
72
        else:
 
73
            return (stuff[:-tokill], tokill)
 
74
 
 
75
    def write(self, stuff):
 
76
        # print will only give a non-file a unicode or str. There's no way
 
77
        # to convince it to encode unicodes, so we have to do it ourselves.
 
78
        # Yay for file special-cases (fileobject.c, PyFile_WriteObject).
 
79
        # If somebody wants to write some other object to here, they do it
 
80
        # at their own peril.
 
81
        if isinstance(stuff, unicode):
 
82
            stuff = stuff.encode('utf-8')
 
83
        self.remainder = self.remainder + stuff
 
84
 
 
85
        # if there's less than 128 bytes, buffer
 
86
        if len(self.remainder) < 128:
 
87
            return
 
88
 
 
89
        # if there's lots, then send it in 1/2K blocks
 
90
        while len(self.remainder) > 512:
 
91
            # We send things as Unicode inside JSON, so we must only send
 
92
            # complete UTF-8 characters.
 
93
            (blk, count) = self._trim_incomplete_final(self.remainder[:512])
 
94
            self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
 
95
            expiry.ping()
 
96
            action, params = self.lineQ.get()
 
97
            self.remainder = self.remainder[512 - count:]
 
98
 
 
99
        # Finally, split the remainder up into lines, and ship all the
 
100
        # completed lines off to the server.
 
101
        lines = self.remainder.split("\n")
 
102
        self.remainder = lines[-1]
 
103
        del lines[-1]
 
104
 
 
105
        if len(lines) > 0:
 
106
            lines.append('')
 
107
            text = "\n".join(lines)
 
108
            self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
 
109
            expiry.ping()
 
110
            action, params = self.lineQ.get()
 
111
            if action == 'interrupt':
 
112
                raise Interrupt()
 
113
 
 
114
    def flush(self):
 
115
        if len(self.remainder) > 0:
 
116
            (out, count) = self._trim_incomplete_final(self.remainder)
 
117
            self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
 
118
            expiry.ping()
 
119
            action, params = self.lineQ.get()
 
120
            # Leave incomplete characters in the buffer.
 
121
            # Yes, this does mean that an incomplete character will be left
 
122
            # off the end, but we discussed this and it was deemed best.
 
123
            self.remainder = self.remainder[len(self.remainder)-count:]
 
124
            if action == 'interrupt':
 
125
                raise Interrupt()
 
126
 
 
127
class WebIO(object):
 
128
    """Provides a file like interface to the Web front end of the console.
 
129
    You may print text to the console using write(), flush any buffered output 
 
130
    using flush(), or request text from the console using readline()"""
 
131
    # FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to 
 
132
    # be able to deal with the streams individually.
 
133
    
 
134
    def __init__(self, cmdQ, lineQ):
 
135
        self.cmdQ = cmdQ
 
136
        self.lineQ = lineQ
 
137
        self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
 
138
        self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
 
139
 
 
140
    def write(self, stuff):
 
141
        self.stdout.write(stuff)
 
142
 
 
143
    def flush(self):
 
144
        self.stdout.flush()
 
145
 
 
146
    def readline(self):
 
147
        self.stdout.flush()
 
148
        return self.stdin.readline()
 
149
 
 
150
class PythonRunner(Thread):
 
151
    def __init__(self, cmdQ, lineQ):
 
152
        self.cmdQ = cmdQ
 
153
        self.lineQ = lineQ
 
154
        self.webio = WebIO(self.cmdQ, self.lineQ)
 
155
        self.cc = codeop.CommandCompiler()
 
156
        Thread.__init__(self)
 
157
 
 
158
    def execCmd(self, cmd):
 
159
        try:
 
160
            # We don't expect a return value - 'single' symbol prints it.
 
161
            self.eval(cmd)
 
162
            self.curr_cmd = ''
 
163
            self.webio.flush()
 
164
            return({"okay": None})
 
165
        except:
 
166
            self.curr_cmd = ''
 
167
            self.webio.flush()
 
168
            tb = format_exc_start(start=2)
 
169
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
170
 
 
171
    def run(self):
 
172
        # Set up global space and partial command buffer
 
173
        self.globs = {'__name__': '__main__'}
 
174
        self.curr_cmd = ''
 
175
 
 
176
        # Set up I/O to use web interface
 
177
        sys.stdin = self.webio
 
178
        sys.stdout = self.webio
 
179
        sys.stderr = self.webio
 
180
 
 
181
        # Handlers for each action
 
182
        actions = {
 
183
            'splash': self.handle_splash,
 
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,
 
190
            }
 
191
 
 
192
        # Run the processing loop
 
193
        while True:
 
194
            action, params = self.lineQ.get()
 
195
            try:
 
196
                response = actions[action](params)
 
197
            except Exception, e:
 
198
                response = {'error': repr(e)}
 
199
            finally:
 
200
                self.cmdQ.put(response)
 
201
 
 
202
    def handle_splash(self, params):
 
203
        # Initial console splash screen
 
204
        python_version = '.'.join(str(v) for v in sys.version_info[:3])
 
205
        splash_text = ("""IVLE %s Python Console (Python %s)
 
206
Type "help", "copyright", "credits" or "license" for more information.
 
207
""" % (ivle.__version__, python_version))
 
208
        return {'output': splash_text}
 
209
 
 
210
    def handle_chat(self, params):
 
211
        # Set up the partial cmd buffer
 
212
        if self.curr_cmd == '':
 
213
            self.curr_cmd = params
 
214
        else:
 
215
            self.curr_cmd = self.curr_cmd + '\n' + params
 
216
 
 
217
        # Try to execute the buffer
 
218
        try:
 
219
            # A single trailing newline simply indicates that the line is
 
220
            # finished. Two trailing newlines indicate the end of a block.
 
221
            # Unfortunately, codeop.CommandCompiler causes even one to
 
222
            # terminate a block.
 
223
            # Thus we need to remove a trailing newline from the command,
 
224
            # unless there are *two* trailing newlines, or multi-line indented
 
225
            # blocks are impossible. See Google Code issue 105.
 
226
            cmd_text = self.curr_cmd
 
227
            if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
 
228
                cmd_text = cmd_text[:-1]
 
229
            cmd = self.cc(cmd_text, '<web session>')
 
230
            if cmd is None:
 
231
                # The command was incomplete, so send back a None, so the              
 
232
                # client can print a '...'
 
233
                return({"more":None})
 
234
            else:
 
235
                return(self.execCmd(cmd))
 
236
        except:
 
237
            # Clear any partial command
 
238
            self.curr_cmd = ''
 
239
            # Flush the output buffers
 
240
            sys.stderr.flush()
 
241
            sys.stdout.flush()
 
242
            # Return the exception
 
243
            tb = format_exc_start(start=3)
 
244
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
245
 
 
246
    def handle_block(self, params):
 
247
        # throw away any partial command.
 
248
        self.curr_cmd = ''
 
249
 
 
250
        # Try to execute a complete block of code
 
251
        try:
 
252
            cmd = compile(params, "<web session>", 'exec');
 
253
            return(self.execCmd(cmd))
 
254
        except:
 
255
            # Flush the output buffers
 
256
            sys.stderr.flush()
 
257
            sys.stdout.flush()
 
258
            # Return the exception
 
259
            tb = format_exc_start(start=1)
 
260
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
261
 
 
262
    def handle_globals(self, params):
 
263
        # Unpickle the new space (if provided)
 
264
        if isinstance(params, dict):
 
265
            self.globs = {'__name__': '__main__'}
 
266
            for g in params:
 
267
                try:
 
268
                    self.globs[g] = cPickle.loads(params[g])
 
269
                except:
 
270
                    pass
 
271
 
 
272
        # Return the current globals
 
273
        return({'globals': flatten(self.globs)})
 
274
 
 
275
    def handle_call(self, params):
 
276
        call = {}
 
277
        
 
278
        # throw away any partial command.
 
279
        self.curr_cmd = ''
 
280
 
 
281
        if isinstance(params, dict):
 
282
            try:
 
283
                # Expand parameters
 
284
                if isinstance(params['args'], list):
 
285
                    args = map(self.eval, params['args'])
 
286
                else:
 
287
                    args = []
 
288
                if isinstance(params['kwargs'], dict):
 
289
                    kwargs = {}
 
290
                    for kwarg in params['kwargs']:
 
291
                        kwargs[kwarg] = self.eval(
 
292
                            params['kwargs'][kwarg])
 
293
                else:
 
294
                    kwargs = {}
 
295
 
 
296
                # Run the fuction
 
297
                function = self.eval(params['function'])
 
298
                try:
 
299
                    call['result'] = function(*args, **kwargs)
 
300
                except Exception, e:
 
301
                    exception = {}
 
302
                    tb = format_exc_start(start=1)
 
303
                    exception['traceback'] = \
 
304
                        ''.join(tb).decode('utf-8', 'replace')
 
305
                    exception['except'] = cPickle.dumps(e,
 
306
                        PICKLEVERSION)
 
307
                    call['exception'] = exception
 
308
            except Exception, e:
 
309
                tb = format_exc_start(start=1)
 
310
                call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
 
311
            
 
312
            # Flush the output buffers
 
313
            sys.stderr.flush()
 
314
            sys.stdout.flush()
 
315
 
 
316
            # Write out the inspection object
 
317
            return(call)
 
318
        else:
 
319
            return({'response': 'failure'})
 
320
 
 
321
    def handle_execute(self, params):
 
322
        # throw away any partial command.
 
323
        self.curr_cmd = ''
 
324
        
 
325
        # Like block but return a serialization of the state
 
326
        # throw away partial command
 
327
        response = {'okay': None}
 
328
        try:
 
329
            cmd = compile(params, "<web session>", 'exec');
 
330
            # We don't expect a return value - 'single' symbol prints it.
 
331
            self.eval(cmd)
 
332
        except Exception, e:
 
333
            response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
 
334
           
 
335
        # Flush the output
 
336
        sys.stderr.flush()
 
337
        sys.stdout.flush()
 
338
               
 
339
        # Return the inspection object
 
340
        return(response)
 
341
 
 
342
    def handle_setvars(self, params):
 
343
        # Adds some variables to the global dictionary
 
344
        for var in params['set_vars']:
 
345
            try:
 
346
                self.globs[var] = self.eval(params['set_vars'][var])
 
347
            except Exception, e:
 
348
                tb = format_exc_start(start=1)
 
349
                return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
350
 
 
351
        return({'okay': None})
 
352
 
 
353
    def eval(self, source):
 
354
        """ Evaluates a string in the private global space """
 
355
        return eval(source, self.globs)
 
356
 
 
357
# The global 'magic' is the secret that the client and server share
 
358
# which is used to create and md5 digest to authenticate requests.
 
359
# It is assigned a real value at startup.
 
360
magic = ''
 
361
 
 
362
cmdQ = Queue.Queue()
 
363
lineQ = Queue.Queue()
 
364
interpThread = PythonRunner(cmdQ, lineQ)
 
365
terminate = None
 
366
 
 
367
# Default expiry time of 15 minutes
 
368
expiry = ExpiryTimer(15 * 60)
 
369
 
 
370
def initializer():
 
371
    interpThread.setDaemon(True)
 
372
    interpThread.start()
 
373
    signal.signal(signal.SIGXCPU, sig_handler)
 
374
    expiry.ping()
 
375
 
 
376
def sig_handler(signum, frame):
 
377
    """Handles response from signals"""
 
378
    global terminate
 
379
    if signum == signal.SIGXCPU:
 
380
        terminate = "CPU time limit exceeded"
 
381
 
 
382
def dispatch_msg(msg):
 
383
    global terminate
 
384
    if msg['cmd'] == 'terminate':
 
385
        terminate = "User requested restart"
 
386
    if terminate:
 
387
        raise ivle.chat.Terminate({"terminate":terminate})
 
388
    expiry.ping()
 
389
    lineQ.put((msg['cmd'],msg['text']))
 
390
    response = cmdQ.get()
 
391
    if terminate:
 
392
        raise ivle.chat.Terminate({"terminate":terminate})
 
393
    return response
 
394
 
 
395
def format_exc_start(start=0):
 
396
    etype, value, tb = sys.exc_info()
 
397
    tbbits = traceback.extract_tb(tb)[start:]
 
398
    list = ['Traceback (most recent call last):\n']
 
399
    list = list + traceback.format_list(tbbits)
 
400
    list = list + traceback.format_exception_only(etype, value)
 
401
    return ''.join(list)
 
402
 
 
403
 
 
404
# Takes an object and returns a flattened version suitable for JSON
 
405
def flatten(object):
 
406
    flat = {}
 
407
    for o in object:
 
408
        try:
 
409
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
 
410
        except (TypeError, cPickle.PicklingError):
 
411
            try:
 
412
                o_type = type(object[o]).__name__
 
413
                o_name = object[o].__name__
 
414
                fake_o = ivle.util.FakeObject(o_type, o_name)
 
415
                flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
 
416
            except AttributeError:
 
417
                pass
 
418
    return flat
 
419
 
 
420
if __name__ == "__main__":
 
421
    port = int(sys.argv[1])
 
422
    magic = sys.argv[2]
 
423
 
 
424
    # Make python's search path follow the cwd
 
425
    sys.path[0] = ''
 
426
 
 
427
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)