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

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: Matt Giuca
  • Date: 2009-12-15 04:44:25 UTC
  • Revision ID: matt.giuca@gmail.com-20091215044425-uxzmdrd89s2d5ax6
doc: Added dev/faq, with some helpful tips on creating certain objects.

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