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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: dcoles
  • Date: 2008-08-13 07:26:44 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1017
Console: Refactored a lot of the console stuff into a class to make calls to 
the console from Python a lot nicer. Eventually it should even be able to be 
used by consoleservice but needs a little more work (at the moment it's only 
used for starting the console).
Also added in a few Tutorial specific functions to console such as 'inspect' 
which gives a summary of the the evaluated code block and flush to clear and 
optionally set globals.

Listmake will need to be rerun after this revision due to the new 
lib/common/console.py

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
            eval(cmd, self.globs)
 
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
                    if 'args' in params:
 
223
                        args = params['args']
 
224
                    else:
 
225
                        args = []
 
226
                    if 'kwargs' in params:
 
227
                        kwargs = params['kwargs']
 
228
                    else:
 
229
                        kwargs = {}
 
230
 
 
231
                    try:
 
232
                        function = cPickle.loads(params['function'])
 
233
                        result = function(*args, **kwargs)
 
234
                        self.cmdQ.put({'output': result})
 
235
                    except Exception,e:
 
236
                        self.cmdQ.put({'response': 'failure: %s'%repr(e)})
 
237
                else:
 
238
                    self.cmdQ.put({'response': 'failure'})
 
239
            elif 'inspect' in ln:
 
240
                # Like block but return a serialization of the state
 
241
                # throw away partial command
 
242
                inspection = {}
 
243
                stdout = cStringIO.StringIO()
 
244
                stderr = cStringIO.StringIO()
 
245
                try:
 
246
                    cmd = compile(ln['inspect'], "<web session>", 'exec');
 
247
                    sys.stdin = None
 
248
                    sys.stdout = stdout
 
249
                    sys.stderr = stderr
 
250
                    # We don't expect a return value - 'single' symbol prints 
 
251
                    # it.
 
252
                    eval(cmd, self.globs)
 
253
                except Exception, e:
 
254
                    exception = {}
 
255
                    tb = format_exc_start(start=1)
 
256
                    exception['traceback'] = \
 
257
                        ''.join(tb).decode('utf-8', 'replace')
 
258
                    exception['except'] = cPickle.dumps(e, PICKLEVERSION)
 
259
                    inspection['exception'] = exception                
 
260
                
 
261
                # Write out the inspection object
 
262
                inspection['stdout'] = stdout.getvalue()
 
263
                inspection['stderr'] = stderr.getvalue()
 
264
                inspection['globals'] = flatten(self.globs)
 
265
                self.cmdQ.put(inspection)
 
266
                stdout.close()
 
267
                stderr.close()
 
268
                self.curr_cmd = ''
 
269
            else:
 
270
                raise Exception, "Invalid Command"
 
271
 
 
272
def daemonize():
 
273
    if os.fork():   # launch child and...
 
274
        os._exit(0) # kill off parent
 
275
    os.setsid()
 
276
    if os.fork():   # launch child and...
 
277
        os._exit(0) # kill off parent again.
 
278
    os.umask(077)
 
279
 
 
280
# The global 'magic' is the secret that the client and server share
 
281
# which is used to create and md5 digest to authenticate requests.
 
282
# It is assigned a real value at startup.
 
283
magic = ''
 
284
 
 
285
cmdQ = Queue.Queue()
 
286
lineQ = Queue.Queue()
 
287
interpThread = PythonRunner(cmdQ, lineQ)
 
288
terminate = None
 
289
 
 
290
# Default expiry time of 15 minutes
 
291
expiry = ExpiryTimer(15 * 60)
 
292
 
 
293
def initializer():
 
294
    interpThread.setDaemon(True)
 
295
    interpThread.start()
 
296
    signal.signal(signal.SIGXCPU, sig_handler)
 
297
    expiry.ping()
 
298
 
 
299
def sig_handler(signum, frame):
 
300
    """Handles response from signals"""
 
301
    global terminate
 
302
    if signum == signal.SIGXCPU:
 
303
        terminate = "CPU Time Limit Exceeded"
 
304
 
 
305
def dispatch_msg(msg):
 
306
    global terminate
 
307
    if msg['cmd'] == 'restart':
 
308
        terminate = "User requested console be reset"
 
309
    if terminate:
 
310
        raise common.chat.Terminate({"restart":terminate})
 
311
    expiry.ping()
 
312
    lineQ.put({msg['cmd']:msg['text']})
 
313
    if terminate:
 
314
        raise common.chat.Terminate({"restart":terminate})
 
315
    return cmdQ.get()
 
316
 
 
317
def format_exc_start(start=0):
 
318
    etype, value, tb = sys.exc_info()
 
319
    tbbits = traceback.extract_tb(tb)[start:]
 
320
    list = ['Traceback (most recent call last):\n']
 
321
    list = list + traceback.format_list(tbbits)
 
322
    list = list + traceback.format_exception_only(etype, value)
 
323
    return ''.join(list)
 
324
 
 
325
def incomplete_utf8_sequence(byteseq):
 
326
    """
 
327
    str -> int
 
328
    Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
 
329
    the end of the string which comprise an incomplete UTF-8 character
 
330
    sequence.
 
331
 
 
332
    If the string is empty or ends with a complete character OR INVALID
 
333
    sequence, returns 0.
 
334
    Otherwise, returns 1-3 indicating the number of bytes in the final
 
335
    incomplete (but valid) character sequence.
 
336
 
 
337
    Does not check any bytes before the final sequence for correctness.
 
338
 
 
339
    >>> incomplete_utf8_sequence("")
 
340
    0
 
341
    >>> incomplete_utf8_sequence("xy")
 
342
    0
 
343
    >>> incomplete_utf8_sequence("xy\xc3\xbc")
 
344
    0
 
345
    >>> incomplete_utf8_sequence("\xc3")
 
346
    1
 
347
    >>> incomplete_utf8_sequence("\xbc\xc3")
 
348
    1
 
349
    >>> incomplete_utf8_sequence("xy\xbc\xc3")
 
350
    1
 
351
    >>> incomplete_utf8_sequence("xy\xe0\xa0")
 
352
    2
 
353
    >>> incomplete_utf8_sequence("xy\xf4")
 
354
    1
 
355
    >>> incomplete_utf8_sequence("xy\xf4\x8f")
 
356
    2
 
357
    >>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
 
358
    3
 
359
    """
 
360
    count = 0
 
361
    expect = None
 
362
    for b in byteseq[::-1]:
 
363
        b = ord(b)
 
364
        count += 1
 
365
        if b & 0x80 == 0x0:
 
366
            # 0xxxxxxx (single-byte character)
 
367
            expect = 1
 
368
            break
 
369
        elif b & 0xc0 == 0x80:
 
370
            # 10xxxxxx (subsequent byte)
 
371
            pass
 
372
        elif b & 0xe0 == 0xc0:
 
373
            # 110xxxxx (start of 2-byte sequence)
 
374
            expect = 2
 
375
            break
 
376
        elif b & 0xf0 == 0xe0:
 
377
            # 1110xxxx (start of 3-byte sequence)
 
378
            expect = 3
 
379
            break
 
380
        elif b & 0xf8 == 0xf0:
 
381
            # 11110xxx (start of 4-byte sequence)
 
382
            expect = 4
 
383
            break
 
384
        else:
 
385
            # Invalid byte
 
386
            return 0
 
387
 
 
388
        if count >= 4:
 
389
            # Seen too many "subsequent bytes", invalid
 
390
            return 0
 
391
 
 
392
    if expect is None:
 
393
        # We never saw a "first byte", invalid
 
394
        return 0
 
395
 
 
396
    # We now know expect and count
 
397
    if count >= expect:
 
398
        # Complete, or we saw an invalid sequence
 
399
        return 0
 
400
    elif count < expect:
 
401
        # Incomplete
 
402
        return count
 
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:
 
411
            pass
 
412
    return flat
 
413
 
 
414
if __name__ == "__main__":
 
415
    port = int(sys.argv[1])
 
416
    magic = sys.argv[2]
 
417
    
 
418
    # Sanitise the Enviroment
 
419
    os.environ = {}
 
420
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
 
421
 
 
422
    if len(sys.argv) >= 4:
 
423
        # working_dir
 
424
        os.chdir(sys.argv[3])
 
425
        os.environ['HOME'] = sys.argv[3]
 
426
 
 
427
    # Make python's search path follow the cwd
 
428
    sys.path[0] = ''
 
429
 
 
430
    common.chat.start_server(port, magic, True, dispatch_msg, initializer)