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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: William Grant
  • Date: 2010-07-20 04:52:31 UTC
  • mto: This revision was merged to the branch mainline in revision 1826.
  • Revision ID: grantw@unimelb.edu.au-20100720045231-8ia3uk8nao8zdq1i
Replace cjson with json, or simplejson if json is not available (Python <2.6)

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
 
 
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
 
        ln = self.lineQ.get()
54
 
        if 'chat' in ln:
55
 
            return ln['chat']
56
 
        if 'interrupt' in ln:
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 = 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
 
            ln = 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
 
            ln = self.lineQ.get()
111
 
            if 'interrupt' in ln:
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
 
            ln = 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 'interrupt' in ln:
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
 
    
132
 
    def __init__(self, cmdQ, lineQ):
133
 
        self.cmdQ = cmdQ
134
 
        self.lineQ = lineQ
135
 
        self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
136
 
        self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
137
 
 
138
 
    def write(self, stuff):
139
 
        self.stdout.write(stuff)
140
 
 
141
 
    def flush(self):
142
 
        self.stdout.flush()
143
 
 
144
 
    def readline(self):
145
 
        self.stdout.flush()
146
 
        return self.stdin.readline()
147
 
 
148
 
class PythonRunner(Thread):
149
 
    def __init__(self, cmdQ, lineQ):
150
 
        self.cmdQ = cmdQ
151
 
        self.lineQ = lineQ
152
 
        self.webio = WebIO(self.cmdQ, self.lineQ)
153
 
        Thread.__init__(self)
154
 
 
155
 
    def execCmd(self, cmd):
156
 
        try:
157
 
            sys.stdin = self.webio
158
 
            sys.stdout = self.webio
159
 
            sys.stderr = self.webio
160
 
            # We don't expect a return value - 'single' symbol prints it.
161
 
            self.eval(cmd)
162
 
            self.webio.flush()
163
 
            self.cmdQ.put({"okay": None})
164
 
            self.curr_cmd = ''
165
 
        except:
166
 
            tb = format_exc_start(start=1)
167
 
            self.webio.flush()
168
 
            self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
169
 
            self.curr_cmd = ''
170
 
 
171
 
    def run(self):
172
 
        self.globs = {}
173
 
        self.globs['__builtins__'] = globals()['__builtins__']
174
 
        self.curr_cmd = ''
175
 
 
176
 
        while True:
177
 
            ln = self.lineQ.get()
178
 
            if 'chat' in ln:
179
 
                if self.curr_cmd == '':
180
 
                    self.curr_cmd = ln['chat']
181
 
                else:
182
 
                    self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
183
 
                try:
184
 
                    cmd = codeop.compile_command(self.curr_cmd, '<web session>')
185
 
                    if cmd is None:
186
 
                        # The command was incomplete,
187
 
                        # so send back a None, so the
188
 
                        # client can print a '...'
189
 
                        self.cmdQ.put({"more":None})
190
 
                    else:
191
 
                        self.execCmd(cmd)
192
 
                except:
193
 
                    tb = format_exc_start(start=3)
194
 
                    self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
195
 
                    self.webio.flush()
196
 
                    self.curr_cmd = ''
197
 
            elif 'block' in ln:
198
 
                # throw away a partial command.
199
 
                try:
200
 
                    cmd = compile(ln['block'], "<web session>", 'exec');
201
 
                    self.execCmd(cmd)
202
 
                except:
203
 
                    tb = format_exc_start(start=1)
204
 
                    self.webio.flush()
205
 
                    self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
206
 
                    self.curr_cmd = ''
207
 
            elif 'flush' in ln:
208
 
                # reset the globals
209
 
                self.globs = {}
210
 
                self.globs['__builtins__'] = globals()['__builtins__']
211
 
                self.cmdQ.put({'response': 'okay'})
212
 
                # Unpickle the new space (if provided)
213
 
                if isinstance(ln['flush'],dict):
214
 
                    for g in ln['flush']:
215
 
                        try:
216
 
                            self.globs[g] = cPickle.loads(ln['flush'][g])
217
 
                        except:
218
 
                            pass
219
 
            elif 'call' in ln:
220
 
                if isinstance(ln['call'], dict):
221
 
                    params = ln['call']
222
 
                    try:
223
 
                        # Expand parameters
224
 
                        if isinstance(params['args'], list):
225
 
                            args = map(self.eval, params['args'])
226
 
                        else:
227
 
                            args = []
228
 
                        if isinstance(params['kwargs'], dict):
229
 
                            kwargs = {}
230
 
                            for kwarg in params['kwargs']:
231
 
                                kwargs[kwarg] = self.eval(
232
 
                                    params['kwargs'][kwarg])
233
 
                        else:
234
 
                            kwargs = {}
235
 
 
236
 
                        # Run the fuction
237
 
                        function = self.eval(params['function'])
238
 
                        result = function(*args, **kwargs)
239
 
                        self.cmdQ.put({'output': result})
240
 
                    except Exception, e:
241
 
                        tb = format_exc_start(start=1)
242
 
                        self.cmdQ.put(
243
 
                            {"exc": ''.join(tb).decode('utf-8', 'replace')})
244
 
                else:
245
 
                    self.cmdQ.put({'response': 'failure'})
246
 
            elif 'inspect' in ln:
247
 
                # Like block but return a serialization of the state
248
 
                # throw away partial command
249
 
                inspection = {}
250
 
                stdout = cStringIO.StringIO()
251
 
                stderr = cStringIO.StringIO()
252
 
                try:
253
 
                    cmd = compile(ln['inspect'], "<web session>", 'exec');
254
 
                    sys.stdin = None
255
 
                    sys.stdout = stdout
256
 
                    sys.stderr = stderr
257
 
                    # We don't expect a return value - 'single' symbol prints 
258
 
                    # it.
259
 
                    self.eval(cmd)
260
 
                except Exception, e:
261
 
                    exception = {}
262
 
                    tb = format_exc_start(start=1)
263
 
                    exception['traceback'] = \
264
 
                        ''.join(tb).decode('utf-8', 'replace')
265
 
                    exception['except'] = cPickle.dumps(e, PICKLEVERSION)
266
 
                    inspection['exception'] = exception                
267
 
                
268
 
                # Write out the inspection object
269
 
                inspection['stdout'] = stdout.getvalue()
270
 
                inspection['stderr'] = stderr.getvalue()
271
 
                inspection['globals'] = flatten(self.globs)
272
 
                self.cmdQ.put(inspection)
273
 
                stdout.close()
274
 
                stderr.close()
275
 
                self.curr_cmd = ''
276
 
            else:
277
 
                raise Exception, "Invalid Command"
278
 
 
279
 
    def eval(self, source):
280
 
        """ Evaluates a string in the private global space """
281
 
        return eval(source, self.globs)
282
 
 
283
 
def daemonize():
284
 
    if os.fork():   # launch child and...
285
 
        os._exit(0) # kill off parent
286
 
    os.setsid()
287
 
    if os.fork():   # launch child and...
288
 
        os._exit(0) # kill off parent again.
289
 
    os.umask(077)
290
 
 
291
 
# The global 'magic' is the secret that the client and server share
292
 
# which is used to create and md5 digest to authenticate requests.
293
 
# It is assigned a real value at startup.
294
 
magic = ''
295
 
 
296
 
cmdQ = Queue.Queue()
297
 
lineQ = Queue.Queue()
298
 
interpThread = PythonRunner(cmdQ, lineQ)
299
 
terminate = None
300
 
 
301
 
# Default expiry time of 15 minutes
302
 
expiry = ExpiryTimer(15 * 60)
303
 
 
304
 
def initializer():
305
 
    interpThread.setDaemon(True)
306
 
    interpThread.start()
307
 
    signal.signal(signal.SIGXCPU, sig_handler)
308
 
    expiry.ping()
309
 
 
310
 
def sig_handler(signum, frame):
311
 
    """Handles response from signals"""
312
 
    global terminate
313
 
    if signum == signal.SIGXCPU:
314
 
        terminate = "CPU Time Limit Exceeded"
315
 
 
316
 
def dispatch_msg(msg):
317
 
    global terminate
318
 
    if msg['cmd'] == 'restart':
319
 
        terminate = "User requested console be reset"
320
 
    if terminate:
321
 
        raise common.chat.Terminate({"restart":terminate})
322
 
    expiry.ping()
323
 
    lineQ.put({msg['cmd']:msg['text']})
324
 
    if terminate:
325
 
        raise common.chat.Terminate({"restart":terminate})
326
 
    return cmdQ.get()
327
 
 
328
 
def format_exc_start(start=0):
329
 
    etype, value, tb = sys.exc_info()
330
 
    tbbits = traceback.extract_tb(tb)[start:]
331
 
    list = ['Traceback (most recent call last):\n']
332
 
    list = list + traceback.format_list(tbbits)
333
 
    list = list + traceback.format_exception_only(etype, value)
334
 
    return ''.join(list)
335
 
 
336
 
def incomplete_utf8_sequence(byteseq):
337
 
    """
338
 
    str -> int
339
 
    Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
340
 
    the end of the string which comprise an incomplete UTF-8 character
341
 
    sequence.
342
 
 
343
 
    If the string is empty or ends with a complete character OR INVALID
344
 
    sequence, returns 0.
345
 
    Otherwise, returns 1-3 indicating the number of bytes in the final
346
 
    incomplete (but valid) character sequence.
347
 
 
348
 
    Does not check any bytes before the final sequence for correctness.
349
 
 
350
 
    >>> incomplete_utf8_sequence("")
351
 
    0
352
 
    >>> incomplete_utf8_sequence("xy")
353
 
    0
354
 
    >>> incomplete_utf8_sequence("xy\xc3\xbc")
355
 
    0
356
 
    >>> incomplete_utf8_sequence("\xc3")
357
 
    1
358
 
    >>> incomplete_utf8_sequence("\xbc\xc3")
359
 
    1
360
 
    >>> incomplete_utf8_sequence("xy\xbc\xc3")
361
 
    1
362
 
    >>> incomplete_utf8_sequence("xy\xe0\xa0")
363
 
    2
364
 
    >>> incomplete_utf8_sequence("xy\xf4")
365
 
    1
366
 
    >>> incomplete_utf8_sequence("xy\xf4\x8f")
367
 
    2
368
 
    >>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
369
 
    3
370
 
    """
371
 
    count = 0
372
 
    expect = None
373
 
    for b in byteseq[::-1]:
374
 
        b = ord(b)
375
 
        count += 1
376
 
        if b & 0x80 == 0x0:
377
 
            # 0xxxxxxx (single-byte character)
378
 
            expect = 1
379
 
            break
380
 
        elif b & 0xc0 == 0x80:
381
 
            # 10xxxxxx (subsequent byte)
382
 
            pass
383
 
        elif b & 0xe0 == 0xc0:
384
 
            # 110xxxxx (start of 2-byte sequence)
385
 
            expect = 2
386
 
            break
387
 
        elif b & 0xf0 == 0xe0:
388
 
            # 1110xxxx (start of 3-byte sequence)
389
 
            expect = 3
390
 
            break
391
 
        elif b & 0xf8 == 0xf0:
392
 
            # 11110xxx (start of 4-byte sequence)
393
 
            expect = 4
394
 
            break
395
 
        else:
396
 
            # Invalid byte
397
 
            return 0
398
 
 
399
 
        if count >= 4:
400
 
            # Seen too many "subsequent bytes", invalid
401
 
            return 0
402
 
 
403
 
    if expect is None:
404
 
        # We never saw a "first byte", invalid
405
 
        return 0
406
 
 
407
 
    # We now know expect and count
408
 
    if count >= expect:
409
 
        # Complete, or we saw an invalid sequence
410
 
        return 0
411
 
    elif count < expect:
412
 
        # Incomplete
413
 
        return count
414
 
 
415
 
# Takes an object and returns a flattened version suitable for JSON
416
 
def flatten(object):
417
 
    flat = {}
418
 
    for o in object:
419
 
        try:
420
 
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
421
 
        except:
422
 
            pass
423
 
    return flat
424
 
 
425
 
if __name__ == "__main__":
426
 
    port = int(sys.argv[1])
427
 
    magic = sys.argv[2]
428
 
    
429
 
    # Sanitise the Enviroment
430
 
    os.environ = {}
431
 
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
432
 
 
433
 
    if len(sys.argv) >= 4:
434
 
        # working_dir
435
 
        os.chdir(sys.argv[3])
436
 
        os.environ['HOME'] = sys.argv[3]
437
 
 
438
 
    # Make python's search path follow the cwd
439
 
    sys.path[0] = ''
440
 
 
441
 
    common.chat.start_server(port, magic, True, dispatch_msg, initializer)