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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: William Grant
  • Date: 2009-06-29 03:26:06 UTC
  • mto: This revision was merged to the branch mainline in revision 1322.
  • Revision ID: grantw@unimelb.edu.au-20090629032606-s8wzl6gd9y6873we
Need rsync too.

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 md5
9
 
import os
10
 
import Queue
11
 
import signal
12
 
import socket
13
 
import sys
14
 
import traceback
15
 
from threading import Thread
16
 
from functools import partial
17
 
 
18
 
import common.chat
19
 
 
20
 
class Interrupt(Exception):
21
 
    def __init__(self):
22
 
        Exception.__init__(self, "Interrupted!")
23
 
 
24
 
class ExpiryTimer(object):
25
 
    def __init__(self, idle):
26
 
        self.idle = idle
27
 
        signal.signal(signal.SIGALRM, partial(self.timeout))
28
 
 
29
 
    def ping(self):
30
 
        signal.alarm(self.idle)
31
 
 
32
 
    def start(self, time):
33
 
        signal.alarm(time)
34
 
 
35
 
    def stop(self):
36
 
        self.ping()
37
 
 
38
 
    def timeout(self, signum, frame):
39
 
        sys.exit(1)
40
 
        
41
 
class StdinFromWeb(object):
42
 
    def __init__(self, cmdQ, lineQ):
43
 
        self.cmdQ = cmdQ
44
 
        self.lineQ = lineQ
45
 
 
46
 
    def readline(self):
47
 
        self.cmdQ.put({"input":None})
48
 
        expiry.ping()
49
 
        ln = self.lineQ.get()
50
 
        if 'chat' in ln:
51
 
            return ln['chat']
52
 
        if 'interrupt' in ln:
53
 
            raise Interrupt()
54
 
        if 'terminate' in ln:
55
 
            sys.exit(0)
56
 
            
57
 
 
58
 
class StdoutToWeb(object):
59
 
    def __init__(self, cmdQ, lineQ):
60
 
        self.cmdQ = cmdQ
61
 
        self.lineQ = lineQ
62
 
        self.remainder = ''
63
 
 
64
 
    def _trim_incomplete_final(self, stuff):
65
 
        '''Trim an incomplete UTF-8 character from the end of a string.
66
 
           Returns (trimmed_string, count_of_trimmed_bytes).
67
 
        '''
68
 
        tokill = incomplete_utf8_sequence(stuff)
69
 
        if tokill == 0:
70
 
            return (stuff, tokill)
71
 
        else:
72
 
            return (stuff[:-tokill], tokill)
73
 
 
74
 
    def write(self, stuff):
75
 
        # print will only give a non-file a unicode or str. There's no way
76
 
        # to convince it to encode unicodes, so we have to do it ourselves.
77
 
        # Yay for file special-cases (fileobject.c, PyFile_WriteObject).
78
 
        # If somebody wants to write some other object to here, they do it
79
 
        # at their own peril.
80
 
        if isinstance(stuff, unicode):
81
 
            stuff = stuff.encode('utf-8')
82
 
        self.remainder = self.remainder + stuff
83
 
 
84
 
        # if there's less than 128 bytes, buffer
85
 
        if len(self.remainder) < 128:
86
 
            return
87
 
 
88
 
        # if there's lots, then send it in 1/2K blocks
89
 
        while len(self.remainder) > 512:
90
 
            # We send things as Unicode inside JSON, so we must only send
91
 
            # complete UTF-8 characters.
92
 
            (blk, count) = self._trim_incomplete_final(self.remainder[:512])
93
 
            self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
94
 
            expiry.ping()
95
 
            ln = self.lineQ.get()
96
 
            self.remainder = self.remainder[512 - count:]
97
 
 
98
 
        # Finally, split the remainder up into lines, and ship all the
99
 
        # completed lines off to the server.
100
 
        lines = self.remainder.split("\n")
101
 
        self.remainder = lines[-1]
102
 
        del lines[-1]
103
 
 
104
 
        if len(lines) > 0:
105
 
            lines.append('')
106
 
            text = "\n".join(lines)
107
 
            self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
108
 
            expiry.ping()
109
 
            ln = self.lineQ.get()
110
 
            if 'interrupt' in ln:
111
 
                raise Interrupt()
112
 
 
113
 
    def flush(self):
114
 
        if len(self.remainder) > 0:
115
 
            (out, count) = self._trim_incomplete_final(self.remainder)
116
 
            self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
117
 
            expiry.ping()
118
 
            ln = self.lineQ.get()
119
 
            # Leave incomplete characters in the buffer.
120
 
            # Yes, this does mean that an incomplete character will be left
121
 
            # off the end, but we discussed this and it was deemed best.
122
 
            self.remainder = self.remainder[len(self.remainder)-count:]
123
 
            if 'interrupt' in ln:
124
 
                raise Interrupt()
125
 
 
126
 
class WebIO(object):
127
 
    """Provides a file like interface to the Web front end of the console.
128
 
    You may print text to the console using write(), flush any buffered output 
129
 
    using flush(), or request text from the console using readline()"""
130
 
    
131
 
    def __init__(self, cmdQ, lineQ):
132
 
        self.cmdQ = cmdQ
133
 
        self.lineQ = lineQ
134
 
        self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
135
 
        self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
136
 
 
137
 
    def write(self, stuff):
138
 
        self.stdout.write(stuff)
139
 
 
140
 
    def flush(self):
141
 
        self.stdout.flush()
142
 
 
143
 
    def readline(self):
144
 
        self.stdout.flush()
145
 
        return self.stdin.readline()
146
 
 
147
 
class PythonRunner(Thread):
148
 
    def __init__(self, cmdQ, lineQ):
149
 
        self.cmdQ = cmdQ
150
 
        self.lineQ = lineQ
151
 
        self.webio = WebIO(self.cmdQ, self.lineQ)
152
 
        Thread.__init__(self)
153
 
 
154
 
    def execCmd(self, cmd):
155
 
        try:
156
 
            sys.stdin = self.webio
157
 
            sys.stdout = self.webio
158
 
            sys.stderr = self.webio
159
 
            # We don't expect a return value - 'single' symbol prints it.
160
 
            eval(cmd, self.globs)
161
 
            self.webio.flush()
162
 
            self.cmdQ.put({"okay": None})
163
 
            self.curr_cmd = ''
164
 
        except:
165
 
            tb = format_exc_start(start=1)
166
 
            self.webio.flush()
167
 
            self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
168
 
            self.curr_cmd = ''
169
 
 
170
 
    def run(self):
171
 
        self.globs = {}
172
 
        self.globs['__builtins__'] = globals()['__builtins__']
173
 
        self.curr_cmd = ''
174
 
 
175
 
        while True:
176
 
            ln = self.lineQ.get()
177
 
            if 'chat' in ln:
178
 
                if self.curr_cmd == '':
179
 
                    self.curr_cmd = ln['chat']
180
 
                else:
181
 
                    self.curr_cmd = self.curr_cmd + '\n' + ln['chat']
182
 
                try:
183
 
                    cmd = codeop.compile_command(self.curr_cmd, '<web session>')
184
 
                    if cmd is None:
185
 
                        # The command was incomplete,
186
 
                        # so send back a None, so the
187
 
                        # client can print a '...'
188
 
                        self.cmdQ.put({"more":None})
189
 
                    else:
190
 
                        self.execCmd(cmd)
191
 
                except:
192
 
                    tb = format_exc_start(start=3)
193
 
                    self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
194
 
                    self.webio.flush()
195
 
                    self.curr_cmd = ''
196
 
            if 'block' in ln:
197
 
                # throw away a partial command.
198
 
                try:
199
 
                    cmd = compile(ln['block'], "<web session>", 'exec');
200
 
                    self.execCmd(cmd)
201
 
                except:
202
 
                    tb = format_exc_start(start=1)
203
 
                    self.webio.flush()
204
 
                    self.cmdQ.put({"exc": ''.join(tb).decode('utf-8', 'replace')})
205
 
                    self.curr_cmd = ''
206
 
 
207
 
def daemonize():
208
 
    if os.fork():   # launch child and...
209
 
        os._exit(0) # kill off parent
210
 
    os.setsid()
211
 
    if os.fork():   # launch child and...
212
 
        os._exit(0) # kill off parent again.
213
 
    os.umask(077)
214
 
 
215
 
# The global 'magic' is the secret that the client and server share
216
 
# which is used to create and md5 digest to authenticate requests.
217
 
# It is assigned a real value at startup.
218
 
magic = ''
219
 
 
220
 
cmdQ = Queue.Queue()
221
 
lineQ = Queue.Queue()
222
 
interpThread = PythonRunner(cmdQ, lineQ)
223
 
 
224
 
# Default expiry time of 15 minutes
225
 
expiry = ExpiryTimer(15 * 60)
226
 
 
227
 
def initializer():
228
 
    interpThread.setDaemon(True)
229
 
    interpThread.start()
230
 
    expiry.ping()
231
 
 
232
 
def dispatch_msg(msg):
233
 
    expiry.ping()
234
 
    lineQ.put({msg['cmd']:msg['text']})
235
 
    return cmdQ.get()
236
 
 
237
 
def format_exc_start(start=0):
238
 
    etype, value, tb = sys.exc_info()
239
 
    tbbits = traceback.extract_tb(tb)[start:]
240
 
    list = ['Traceback (most recent call last):\n']
241
 
    list = list + traceback.format_list(tbbits)
242
 
    list = list + traceback.format_exception_only(etype, value)
243
 
    return ''.join(list)
244
 
 
245
 
def incomplete_utf8_sequence(byteseq):
246
 
    """
247
 
    str -> int
248
 
    Given a UTF-8-encoded byte sequence (str), returns the number of bytes at
249
 
    the end of the string which comprise an incomplete UTF-8 character
250
 
    sequence.
251
 
 
252
 
    If the string is empty or ends with a complete character OR INVALID
253
 
    sequence, returns 0.
254
 
    Otherwise, returns 1-3 indicating the number of bytes in the final
255
 
    incomplete (but valid) character sequence.
256
 
 
257
 
    Does not check any bytes before the final sequence for correctness.
258
 
 
259
 
    >>> incomplete_utf8_sequence("")
260
 
    0
261
 
    >>> incomplete_utf8_sequence("xy")
262
 
    0
263
 
    >>> incomplete_utf8_sequence("xy\xc3\xbc")
264
 
    0
265
 
    >>> incomplete_utf8_sequence("\xc3")
266
 
    1
267
 
    >>> incomplete_utf8_sequence("\xbc\xc3")
268
 
    1
269
 
    >>> incomplete_utf8_sequence("xy\xbc\xc3")
270
 
    1
271
 
    >>> incomplete_utf8_sequence("xy\xe0\xa0")
272
 
    2
273
 
    >>> incomplete_utf8_sequence("xy\xf4")
274
 
    1
275
 
    >>> incomplete_utf8_sequence("xy\xf4\x8f")
276
 
    2
277
 
    >>> incomplete_utf8_sequence("xy\xf4\x8f\xa0")
278
 
    3
279
 
    """
280
 
    count = 0
281
 
    expect = None
282
 
    for b in byteseq[::-1]:
283
 
        b = ord(b)
284
 
        count += 1
285
 
        if b & 0x80 == 0x0:
286
 
            # 0xxxxxxx (single-byte character)
287
 
            expect = 1
288
 
            break
289
 
        elif b & 0xc0 == 0x80:
290
 
            # 10xxxxxx (subsequent byte)
291
 
            pass
292
 
        elif b & 0xe0 == 0xc0:
293
 
            # 110xxxxx (start of 2-byte sequence)
294
 
            expect = 2
295
 
            break
296
 
        elif b & 0xf0 == 0xe0:
297
 
            # 1110xxxx (start of 3-byte sequence)
298
 
            expect = 3
299
 
            break
300
 
        elif b & 0xf8 == 0xf0:
301
 
            # 11110xxx (start of 4-byte sequence)
302
 
            expect = 4
303
 
            break
304
 
        else:
305
 
            # Invalid byte
306
 
            return 0
307
 
 
308
 
        if count >= 4:
309
 
            # Seen too many "subsequent bytes", invalid
310
 
            return 0
311
 
 
312
 
    if expect is None:
313
 
        # We never saw a "first byte", invalid
314
 
        return 0
315
 
 
316
 
    # We now know expect and count
317
 
    if count >= expect:
318
 
        # Complete, or we saw an invalid sequence
319
 
        return 0
320
 
    elif count < expect:
321
 
        # Incomplete
322
 
        return count
323
 
 
324
 
if __name__ == "__main__":
325
 
    port = int(sys.argv[1])
326
 
    magic = sys.argv[2]
327
 
    if len(sys.argv) >= 4:
328
 
        # working_dir
329
 
        os.chdir(sys.argv[3])
330
 
        # Make python's search path follow the cwd
331
 
        sys.path[0] = ''
332
 
        os.environ['HOME'] = sys.argv[3]
333
 
 
334
 
    common.chat.start_server(port, magic, True, dispatch_msg, initializer)