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

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: William Grant
  • Date: 2010-02-26 01:09:49 UTC
  • Revision ID: grantw@unimelb.edu.au-20100226010949-xka8c9s6y7aq4id1
Scroll to the end of the console output after *every* output, not just some. This removes some artifiacts resulting from scrolling a fraction of a second too late.

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 ivle.chat
 
20
import ivle.util
 
21
 
 
22
# This version must be supported by both the local and remote code
 
23
PICKLEVERSION = 0
 
24
 
 
25
class Interrupt(Exception):
 
26
    def __init__(self):
 
27
        Exception.__init__(self, "Interrupted!")
 
28
 
 
29
class ExpiryTimer(object):
 
30
    def __init__(self, idle):
 
31
        self.idle = idle
 
32
        signal.signal(signal.SIGALRM, self.timeout)
 
33
 
 
34
    def ping(self):
 
35
        signal.alarm(self.idle)
 
36
 
 
37
    def start(self, time):
 
38
        signal.alarm(time)
 
39
 
 
40
    def stop(self):
 
41
        self.ping()
 
42
 
 
43
    def timeout(self, signum, frame):
 
44
        sys.exit(1)
 
45
 
 
46
class StdinFromWeb(object):
 
47
    def __init__(self, cmdQ, lineQ):
 
48
        self.cmdQ = cmdQ
 
49
        self.lineQ = lineQ
 
50
 
 
51
    def readline(self):
 
52
        self.cmdQ.put({"input":None})
 
53
        expiry.ping()
 
54
        action, params = self.lineQ.get()
 
55
        if action == 'chat':
 
56
            return params
 
57
        elif action == 'interrupt':
 
58
            raise Interrupt()
 
59
 
 
60
class StdoutToWeb(object):
 
61
    def __init__(self, cmdQ, lineQ):
 
62
        self.cmdQ = cmdQ
 
63
        self.lineQ = lineQ
 
64
        self.remainder = ''
 
65
 
 
66
    def _trim_incomplete_final(self, stuff):
 
67
        '''Trim an incomplete UTF-8 character from the end of a string.
 
68
           Returns (trimmed_string, count_of_trimmed_bytes).
 
69
        '''
 
70
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
 
71
        if tokill == 0:
 
72
            return (stuff, tokill)
 
73
        else:
 
74
            return (stuff[:-tokill], tokill)
 
75
 
 
76
    def write(self, stuff):
 
77
        # print will only give a non-file a unicode or str. There's no way
 
78
        # to convince it to encode unicodes, so we have to do it ourselves.
 
79
        # Yay for file special-cases (fileobject.c, PyFile_WriteObject).
 
80
        # If somebody wants to write some other object to here, they do it
 
81
        # at their own peril.
 
82
        if isinstance(stuff, unicode):
 
83
            stuff = stuff.encode('utf-8')
 
84
        self.remainder = self.remainder + stuff
 
85
 
 
86
        # if there's less than 128 bytes, buffer
 
87
        if len(self.remainder) < 128:
 
88
            return
 
89
 
 
90
        # if there's lots, then send it in 1/2K blocks
 
91
        while len(self.remainder) > 512:
 
92
            # We send things as Unicode inside JSON, so we must only send
 
93
            # complete UTF-8 characters.
 
94
            (blk, count) = self._trim_incomplete_final(self.remainder[:512])
 
95
            self.cmdQ.put({"output":blk.decode('utf-8', 'replace')})
 
96
            expiry.ping()
 
97
            action, params = self.lineQ.get()
 
98
            self.remainder = self.remainder[512 - count:]
 
99
 
 
100
        # Finally, split the remainder up into lines, and ship all the
 
101
        # completed lines off to the server.
 
102
        lines = self.remainder.split("\n")
 
103
        self.remainder = lines[-1]
 
104
        del lines[-1]
 
105
 
 
106
        if len(lines) > 0:
 
107
            lines.append('')
 
108
            text = "\n".join(lines)
 
109
            self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
 
110
            expiry.ping()
 
111
            action, params = self.lineQ.get()
 
112
            if action == 'interrupt':
 
113
                raise Interrupt()
 
114
 
 
115
    def flush(self):
 
116
        if len(self.remainder) > 0:
 
117
            (out, count) = self._trim_incomplete_final(self.remainder)
 
118
            self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
 
119
            expiry.ping()
 
120
            action, params = self.lineQ.get()
 
121
            # Leave incomplete characters in the buffer.
 
122
            # Yes, this does mean that an incomplete character will be left
 
123
            # off the end, but we discussed this and it was deemed best.
 
124
            self.remainder = self.remainder[len(self.remainder)-count:]
 
125
            if action == 'interrupt':
 
126
                raise Interrupt()
 
127
 
 
128
class WebIO(object):
 
129
    """Provides a file like interface to the Web front end of the console.
 
130
    You may print text to the console using write(), flush any buffered output 
 
131
    using flush(), or request text from the console using readline()"""
 
132
    # FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to 
 
133
    # be able to deal with the streams individually.
 
134
    
 
135
    def __init__(self, cmdQ, lineQ):
 
136
        self.cmdQ = cmdQ
 
137
        self.lineQ = lineQ
 
138
        self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
 
139
        self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
 
140
 
 
141
    def write(self, stuff):
 
142
        self.stdout.write(stuff)
 
143
 
 
144
    def flush(self):
 
145
        self.stdout.flush()
 
146
 
 
147
    def readline(self):
 
148
        self.stdout.flush()
 
149
        return self.stdin.readline()
 
150
 
 
151
class PythonRunner(Thread):
 
152
    def __init__(self, cmdQ, lineQ):
 
153
        self.cmdQ = cmdQ
 
154
        self.lineQ = lineQ
 
155
        self.webio = WebIO(self.cmdQ, self.lineQ)
 
156
        self.cc = codeop.CommandCompiler()
 
157
        Thread.__init__(self)
 
158
 
 
159
    def execCmd(self, cmd):
 
160
        try:
 
161
            # We don't expect a return value - 'single' symbol prints it.
 
162
            self.eval(cmd)
 
163
            self.curr_cmd = ''
 
164
            self.webio.flush()
 
165
            return({"okay": None})
 
166
        except:
 
167
            self.curr_cmd = ''
 
168
            self.webio.flush()
 
169
            tb = format_exc_start(start=2)
 
170
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
171
 
 
172
    def run(self):
 
173
        # Set up global space and partial command buffer
 
174
        self.globs = {}
 
175
        self.curr_cmd = ''
 
176
 
 
177
        # Set up I/O to use web interface
 
178
        sys.stdin = self.webio
 
179
        sys.stdout = self.webio
 
180
        sys.stderr = self.webio
 
181
 
 
182
        # Handlers for each action
 
183
        actions = {
 
184
            'splash': self.handle_splash,
 
185
            'chat': self.handle_chat,
 
186
            'block': self.handle_block,
 
187
            'globals': self.handle_globals,
 
188
            'call': self.handle_call,
 
189
            'execute': self.handle_execute,
 
190
            'setvars': self.handle_setvars,
 
191
            }
 
192
 
 
193
        # Run the processing loop
 
194
        while True:
 
195
            action, params = self.lineQ.get()
 
196
            try:
 
197
                response = actions[action](params)
 
198
            except Exception, e:
 
199
                response = {'error': repr(e)}
 
200
            finally:
 
201
                self.cmdQ.put(response)
 
202
 
 
203
    def handle_splash(self, params):
 
204
        # Initial console splash screen
 
205
        python_version = '.'.join(str(v) for v in sys.version_info[:3])
 
206
        splash_text = ("""IVLE %s Python Console (Python %s)
 
207
Type "help", "copyright", "credits" or "license" for more information.
 
208
""" % (ivle.__version__, python_version))
 
209
        return {'output': splash_text}
 
210
 
 
211
    def handle_chat(self, params):
 
212
        # Set up the partial cmd buffer
 
213
        if self.curr_cmd == '':
 
214
            self.curr_cmd = params
 
215
        else:
 
216
            self.curr_cmd = self.curr_cmd + '\n' + params
 
217
 
 
218
        # Try to execute the buffer
 
219
        try:
 
220
            # A single trailing newline simply indicates that the line is
 
221
            # finished. Two trailing newlines indicate the end of a block.
 
222
            # Unfortunately, codeop.CommandCompiler causes even one to
 
223
            # terminate a block.
 
224
            # Thus we need to remove a trailing newline from the command,
 
225
            # unless there are *two* trailing newlines, or multi-line indented
 
226
            # blocks are impossible. See Google Code issue 105.
 
227
            cmd_text = self.curr_cmd
 
228
            if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
 
229
                cmd_text = cmd_text[:-1]
 
230
            cmd = self.cc(cmd_text, '<web session>')
 
231
            if cmd is None:
 
232
                # The command was incomplete, so send back a None, so the              
 
233
                # client can print a '...'
 
234
                return({"more":None})
 
235
            else:
 
236
                return(self.execCmd(cmd))
 
237
        except:
 
238
            # Clear any partial command
 
239
            self.curr_cmd = ''
 
240
            # Flush the output buffers
 
241
            sys.stderr.flush()
 
242
            sys.stdout.flush()
 
243
            # Return the exception
 
244
            tb = format_exc_start(start=3)
 
245
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
246
 
 
247
    def handle_block(self, params):
 
248
        # throw away any partial command.
 
249
        self.curr_cmd = ''
 
250
 
 
251
        # Try to execute a complete block of code
 
252
        try:
 
253
            cmd = compile(params, "<web session>", 'exec');
 
254
            return(self.execCmd(cmd))
 
255
        except:
 
256
            # Flush the output buffers
 
257
            sys.stderr.flush()
 
258
            sys.stdout.flush()
 
259
            # Return the exception
 
260
            tb = format_exc_start(start=1)
 
261
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
262
 
 
263
    def handle_globals(self, params):
 
264
        # Unpickle the new space (if provided)
 
265
        if isinstance(params, dict):
 
266
            self.globs = {}
 
267
            for g in params:
 
268
                try:
 
269
                    self.globs[g] = cPickle.loads(params[g])
 
270
                except:
 
271
                    pass
 
272
 
 
273
        # Return the current globals
 
274
        return({'globals': flatten(self.globs)})
 
275
 
 
276
    def handle_call(self, params):
 
277
        call = {}
 
278
        
 
279
        # throw away any partial command.
 
280
        self.curr_cmd = ''
 
281
 
 
282
        if isinstance(params, dict):
 
283
            try:
 
284
                # Expand parameters
 
285
                if isinstance(params['args'], list):
 
286
                    args = map(self.eval, params['args'])
 
287
                else:
 
288
                    args = []
 
289
                if isinstance(params['kwargs'], dict):
 
290
                    kwargs = {}
 
291
                    for kwarg in params['kwargs']:
 
292
                        kwargs[kwarg] = self.eval(
 
293
                            params['kwargs'][kwarg])
 
294
                else:
 
295
                    kwargs = {}
 
296
 
 
297
                # Run the fuction
 
298
                function = self.eval(params['function'])
 
299
                try:
 
300
                    call['result'] = function(*args, **kwargs)
 
301
                except Exception, e:
 
302
                    exception = {}
 
303
                    tb = format_exc_start(start=1)
 
304
                    exception['traceback'] = \
 
305
                        ''.join(tb).decode('utf-8', 'replace')
 
306
                    exception['except'] = cPickle.dumps(e,
 
307
                        PICKLEVERSION)
 
308
                    call['exception'] = exception
 
309
            except Exception, e:
 
310
                tb = format_exc_start(start=1)
 
311
                call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
 
312
            
 
313
            # Flush the output buffers
 
314
            sys.stderr.flush()
 
315
            sys.stdout.flush()
 
316
 
 
317
            # Write out the inspection object
 
318
            return(call)
 
319
        else:
 
320
            return({'response': 'failure'})
 
321
 
 
322
    def handle_execute(self, params):
 
323
        # throw away any partial command.
 
324
        self.curr_cmd = ''
 
325
        
 
326
        # Like block but return a serialization of the state
 
327
        # throw away partial command
 
328
        response = {'okay': None}
 
329
        try:
 
330
            cmd = compile(params, "<web session>", 'exec');
 
331
            # We don't expect a return value - 'single' symbol prints it.
 
332
            self.eval(cmd)
 
333
        except Exception, e:
 
334
            response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
 
335
           
 
336
        # Flush the output
 
337
        sys.stderr.flush()
 
338
        sys.stdout.flush()
 
339
               
 
340
        # Return the inspection object
 
341
        return(response)
 
342
 
 
343
    def handle_setvars(self, params):
 
344
        # Adds some variables to the global dictionary
 
345
        for var in params['set_vars']:
 
346
            try:
 
347
                self.globs[var] = self.eval(params['set_vars'][var])
 
348
            except Exception, e:
 
349
                tb = format_exc_start(start=1)
 
350
                return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
351
 
 
352
        return({'okay': None})
 
353
 
 
354
    def eval(self, source):
 
355
        """ Evaluates a string in the private global space """
 
356
        return eval(source, self.globs)
 
357
 
 
358
# The global 'magic' is the secret that the client and server share
 
359
# which is used to create and md5 digest to authenticate requests.
 
360
# It is assigned a real value at startup.
 
361
magic = ''
 
362
 
 
363
cmdQ = Queue.Queue()
 
364
lineQ = Queue.Queue()
 
365
interpThread = PythonRunner(cmdQ, lineQ)
 
366
terminate = None
 
367
 
 
368
# Default expiry time of 15 minutes
 
369
expiry = ExpiryTimer(15 * 60)
 
370
 
 
371
def initializer():
 
372
    interpThread.setDaemon(True)
 
373
    interpThread.start()
 
374
    signal.signal(signal.SIGXCPU, sig_handler)
 
375
    expiry.ping()
 
376
 
 
377
def sig_handler(signum, frame):
 
378
    """Handles response from signals"""
 
379
    global terminate
 
380
    if signum == signal.SIGXCPU:
 
381
        terminate = "CPU time limit exceeded"
 
382
 
 
383
def dispatch_msg(msg):
 
384
    global terminate
 
385
    if msg['cmd'] == 'terminate':
 
386
        terminate = "User requested restart"
 
387
    if terminate:
 
388
        raise ivle.chat.Terminate({"terminate":terminate})
 
389
    expiry.ping()
 
390
    lineQ.put((msg['cmd'],msg['text']))
 
391
    response = cmdQ.get()
 
392
    if terminate:
 
393
        raise ivle.chat.Terminate({"terminate":terminate})
 
394
    return response
 
395
 
 
396
def format_exc_start(start=0):
 
397
    etype, value, tb = sys.exc_info()
 
398
    tbbits = traceback.extract_tb(tb)[start:]
 
399
    list = ['Traceback (most recent call last):\n']
 
400
    list = list + traceback.format_list(tbbits)
 
401
    list = list + traceback.format_exception_only(etype, value)
 
402
    return ''.join(list)
 
403
 
 
404
 
 
405
# Takes an object and returns a flattened version suitable for JSON
 
406
def flatten(object):
 
407
    flat = {}
 
408
    for o in object:
 
409
        try:
 
410
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
 
411
        except (TypeError, cPickle.PicklingError):
 
412
            try:
 
413
                o_type = type(object[o]).__name__
 
414
                o_name = object[o].__name__
 
415
                fake_o = ivle.util.FakeObject(o_type, o_name)
 
416
                flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
 
417
            except AttributeError:
 
418
                pass
 
419
    return flat
 
420
 
 
421
if __name__ == "__main__":
 
422
    port = int(sys.argv[1])
 
423
    magic = sys.argv[2]
 
424
    
 
425
    # Sanitise the Enviroment
 
426
    os.environ = {}
 
427
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
 
428
 
 
429
    if len(sys.argv) >= 4:
 
430
        # working_dir
 
431
        os.chdir(sys.argv[3])
 
432
        os.environ['HOME'] = sys.argv[3]
 
433
 
 
434
    # Make python's search path follow the cwd
 
435
    sys.path[0] = ''
 
436
 
 
437
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)