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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: William Grant
  • Date: 2010-02-11 05:27:24 UTC
  • Revision ID: grantw@unimelb.edu.au-20100211052724-171m0779yqzn1y1x
Add a setup.cfg with nose settings. You can now run the suite with 'IVLECONF=. nosetests'.

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 common.chat
20
 
import common.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 = common.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
 
        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 = {}
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
 
            '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,
189
 
            }
190
 
 
191
 
        # Run the processing loop
192
 
        while True:
193
 
            action, params = self.lineQ.get()
194
 
            try:
195
 
                response = actions[action](params)
196
 
            except Exception, e:
197
 
                response = {'error': repr(e)}
198
 
            finally:
199
 
                self.cmdQ.put(response)
200
 
                   
201
 
    def handle_chat(self, params):
202
 
        # Set up the partial cmd buffer
203
 
        if self.curr_cmd == '':
204
 
            self.curr_cmd = params
205
 
        else:
206
 
            self.curr_cmd = self.curr_cmd + '\n' + params
207
 
 
208
 
        # Try to execute the buffer
209
 
        try:
210
 
            cmd = codeop.compile_command(self.curr_cmd, '<web session>')
211
 
            if cmd is None:
212
 
                # The command was incomplete, so send back a None, so the              
213
 
                # client can print a '...'
214
 
                return({"more":None})
215
 
            else:
216
 
                return(self.execCmd(cmd))
217
 
        except:
218
 
            # Clear any partial command
219
 
            self.curr_cmd = ''
220
 
            # Flush the output buffers
221
 
            sys.stderr.flush()
222
 
            sys.stdout.flush()
223
 
            # Return the exception
224
 
            tb = format_exc_start(start=3)
225
 
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
226
 
 
227
 
    def handle_block(self, params):
228
 
        # throw away any partial command.
229
 
        self.curr_cmd = ''
230
 
 
231
 
        # Try to execute a complete block of code
232
 
        try:
233
 
            cmd = compile(params, "<web session>", 'exec');
234
 
            return(self.execCmd(cmd))
235
 
        except:
236
 
            # Flush the output buffers
237
 
            sys.stderr.flush()
238
 
            sys.stdout.flush()
239
 
            # Return the exception
240
 
            tb = format_exc_start(start=1)
241
 
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
242
 
 
243
 
    def handle_globals(self, params):
244
 
        # Unpickle the new space (if provided)
245
 
        if isinstance(params, dict):
246
 
            self.globs = {}
247
 
            for g in params:
248
 
                try:
249
 
                    self.globs[g] = cPickle.loads(params[g])
250
 
                except:
251
 
                    pass
252
 
 
253
 
        # Return the current globals
254
 
        return({'globals': flatten(self.globs)})
255
 
 
256
 
    def handle_call(self, params):
257
 
        call = {}
258
 
        
259
 
        # throw away any partial command.
260
 
        self.curr_cmd = ''
261
 
 
262
 
        if isinstance(params, dict):
263
 
            try:
264
 
                # Expand parameters
265
 
                if isinstance(params['args'], list):
266
 
                    args = map(self.eval, params['args'])
267
 
                else:
268
 
                    args = []
269
 
                if isinstance(params['kwargs'], dict):
270
 
                    kwargs = {}
271
 
                    for kwarg in params['kwargs']:
272
 
                        kwargs[kwarg] = self.eval(
273
 
                            params['kwargs'][kwarg])
274
 
                else:
275
 
                    kwargs = {}
276
 
 
277
 
                # Run the fuction
278
 
                function = self.eval(params['function'])
279
 
                try:
280
 
                    call['result'] = function(*args, **kwargs)
281
 
                except Exception, e:
282
 
                    exception = {}
283
 
                    tb = format_exc_start(start=1)
284
 
                    exception['traceback'] = \
285
 
                        ''.join(tb).decode('utf-8', 'replace')
286
 
                    exception['except'] = cPickle.dumps(e,
287
 
                        PICKLEVERSION)
288
 
                    call['exception'] = exception
289
 
            except Exception, e:
290
 
                tb = format_exc_start(start=1)
291
 
                call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
292
 
            
293
 
            # Flush the output buffers
294
 
            sys.stderr.flush()
295
 
            sys.stdout.flush()
296
 
 
297
 
            # Write out the inspection object
298
 
            return(call)
299
 
        else:
300
 
            return({'response': 'failure'})
301
 
 
302
 
    def handle_execute(self, params):
303
 
        # throw away any partial command.
304
 
        self.curr_cmd = ''
305
 
        
306
 
        # Like block but return a serialization of the state
307
 
        # throw away partial command
308
 
        response = {'okay': None}
309
 
        try:
310
 
            cmd = compile(params, "<web session>", 'exec');
311
 
            # We don't expect a return value - 'single' symbol prints it.
312
 
            self.eval(cmd)
313
 
        except Exception, e:
314
 
            response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
315
 
           
316
 
        # Flush the output
317
 
        sys.stderr.flush()
318
 
        sys.stdout.flush()
319
 
               
320
 
        # Return the inspection object
321
 
        return(response)
322
 
 
323
 
    def handle_setvars(self, params):
324
 
        # Adds some variables to the global dictionary
325
 
        for var in params['set_vars']:
326
 
            try:
327
 
                self.globs[var] = self.eval(params['set_vars'][var])
328
 
            except Exception, e:
329
 
                tb = format_exc_start(start=1)
330
 
                return({"exc": ''.join(tb).decode('utf-8', 'replace')})
331
 
 
332
 
        return({'okay': None})
333
 
 
334
 
    def eval(self, source):
335
 
        """ Evaluates a string in the private global space """
336
 
        return eval(source, self.globs)
337
 
 
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.
341
 
magic = ''
342
 
 
343
 
cmdQ = Queue.Queue()
344
 
lineQ = Queue.Queue()
345
 
interpThread = PythonRunner(cmdQ, lineQ)
346
 
terminate = None
347
 
 
348
 
# Default expiry time of 15 minutes
349
 
expiry = ExpiryTimer(15 * 60)
350
 
 
351
 
def initializer():
352
 
    interpThread.setDaemon(True)
353
 
    interpThread.start()
354
 
    signal.signal(signal.SIGXCPU, sig_handler)
355
 
    expiry.ping()
356
 
 
357
 
def sig_handler(signum, frame):
358
 
    """Handles response from signals"""
359
 
    global terminate
360
 
    if signum == signal.SIGXCPU:
361
 
        terminate = "CPU Time Limit Exceeded"
362
 
 
363
 
def dispatch_msg(msg):
364
 
    global terminate
365
 
    if msg['cmd'] == 'terminate':
366
 
        terminate = "User requested console be terminated"
367
 
    if terminate:
368
 
        raise common.chat.Terminate({"terminate":terminate})
369
 
    expiry.ping()
370
 
    lineQ.put((msg['cmd'],msg['text']))
371
 
    response = cmdQ.get()
372
 
    if terminate:
373
 
        raise common.chat.Terminate({"terminate":terminate})
374
 
    return response
375
 
 
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)
382
 
    return ''.join(list)
383
 
 
384
 
 
385
 
# Takes an object and returns a flattened version suitable for JSON
386
 
def flatten(object):
387
 
    flat = {}
388
 
    for o in object:
389
 
        try:
390
 
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
391
 
        except TypeError:
392
 
            try:
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:
398
 
                pass
399
 
    return flat
400
 
 
401
 
if __name__ == "__main__":
402
 
    port = int(sys.argv[1])
403
 
    magic = sys.argv[2]
404
 
    
405
 
    # Sanitise the Enviroment
406
 
    os.environ = {}
407
 
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
408
 
 
409
 
    if len(sys.argv) >= 4:
410
 
        # working_dir
411
 
        os.chdir(sys.argv[3])
412
 
        os.environ['HOME'] = sys.argv[3]
413
 
 
414
 
    # Make python's search path follow the cwd
415
 
    sys.path[0] = ''
416
 
 
417
 
    common.chat.start_server(port, magic, True, dispatch_msg, initializer)