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

« back to all changes in this revision

Viewing changes to scripts/python-console

Port the tos app to the new framework, and fix ivle.webapp.help's reference
to it.

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