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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: dcoles
  • Date: 2008-08-18 04:34:11 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1021
Database: Adds active column to problem_attempt table.
This allows a group of problem attempts (ie. from previous semesters) to be
marked as 'inactive' and thus counting toward a problem being assessment for
the current semester.

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)