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

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: William Grant
  • Date: 2012-06-28 01:52:02 UTC
  • Revision ID: me@williamgrant.id.au-20120628015202-f6ru7o367gt6nvgz
Hah

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 codeop
 
7
import cPickle
 
8
import cStringIO
 
9
import md5
 
10
import Queue
 
11
import signal
 
12
import socket
 
13
import sys
 
14
import traceback
 
15
from threading import Thread
 
16
 
 
17
import ivle.chat
 
18
import ivle.util
 
19
 
 
20
# This version must be supported by both the local and remote code
 
21
PICKLEVERSION = 0
 
22
 
 
23
class Interrupt(Exception):
 
24
    def __init__(self):
 
25
        Exception.__init__(self, "Interrupted!")
 
26
 
 
27
class ExpiryTimer(object):
 
28
    def __init__(self, idle):
 
29
        self.idle = idle
 
30
        signal.signal(signal.SIGALRM, self.timeout)
 
31
 
 
32
    def ping(self):
 
33
        signal.alarm(self.idle)
 
34
 
 
35
    def start(self, time):
 
36
        signal.alarm(time)
 
37
 
 
38
    def stop(self):
 
39
        self.ping()
 
40
 
 
41
    def timeout(self, signum, frame):
 
42
        sys.exit(1)
 
43
 
 
44
class StdinFromWeb(object):
 
45
    def __init__(self, cmdQ, lineQ):
 
46
        self.cmdQ = cmdQ
 
47
        self.lineQ = lineQ
 
48
 
 
49
    def readline(self):
 
50
        self.cmdQ.put({"input":None})
 
51
        expiry.ping()
 
52
        action, params = self.lineQ.get()
 
53
        if action == 'chat':
 
54
            return params
 
55
        elif action == 'interrupt':
 
56
            raise Interrupt()
 
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 = ivle.util.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
            action, params = 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
            action, params = self.lineQ.get()
 
110
            if action == 'interrupt':
 
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
            action, params = 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 action == 'interrupt':
 
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
    # FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to 
 
131
    # be able to deal with the streams individually.
 
132
    
 
133
    def __init__(self, cmdQ, lineQ):
 
134
        self.cmdQ = cmdQ
 
135
        self.lineQ = lineQ
 
136
        self.stdin = StdinFromWeb(self.cmdQ, self.lineQ)
 
137
        self.stdout = StdoutToWeb(self.cmdQ, self.lineQ)
 
138
 
 
139
    def write(self, stuff):
 
140
        self.stdout.write(stuff)
 
141
 
 
142
    def flush(self):
 
143
        self.stdout.flush()
 
144
 
 
145
    def readline(self):
 
146
        self.stdout.flush()
 
147
        return self.stdin.readline()
 
148
 
 
149
class PythonRunner(Thread):
 
150
    def __init__(self, cmdQ, lineQ):
 
151
        self.cmdQ = cmdQ
 
152
        self.lineQ = lineQ
 
153
        self.webio = WebIO(self.cmdQ, self.lineQ)
 
154
        self.cc = codeop.CommandCompiler()
 
155
        Thread.__init__(self)
 
156
 
 
157
    def execCmd(self, cmd):
 
158
        try:
 
159
            # We don't expect a return value - 'single' symbol prints it.
 
160
            self.eval(cmd)
 
161
            self.curr_cmd = ''
 
162
            self.webio.flush()
 
163
            return({"okay": None})
 
164
        except:
 
165
            self.curr_cmd = ''
 
166
            self.webio.flush()
 
167
            tb = format_exc_start(start=2)
 
168
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
169
 
 
170
    def run(self):
 
171
        # Set up global space and partial command buffer
 
172
        self.globs = {'__name__': '__main__'}
 
173
        self.curr_cmd = ''
 
174
 
 
175
        # Set up I/O to use web interface
 
176
        sys.stdin = self.webio
 
177
        sys.stdout = self.webio
 
178
        sys.stderr = self.webio
 
179
 
 
180
        # Handlers for each action
 
181
        actions = {
 
182
            'splash': self.handle_splash,
 
183
            'chat': self.handle_chat,
 
184
            'block': self.handle_block,
 
185
            'globals': self.handle_globals,
 
186
            'call': self.handle_call,
 
187
            'execute': self.handle_execute,
 
188
            'setvars': self.handle_setvars,
 
189
            }
 
190
 
 
191
        # Run the processing loop
 
192
        while True:
 
193
            action, params = self.lineQ.get()
 
194
            try:
 
195
                response = actions[action](params)
 
196
            except Exception, e:
 
197
                response = {'error': repr(e)}
 
198
            finally:
 
199
                self.cmdQ.put(response)
 
200
 
 
201
    def handle_splash(self, params):
 
202
        # Initial console splash screen
 
203
        python_version = '.'.join(str(v) for v in sys.version_info[:3])
 
204
        splash_text = ("""IVLE %s Python Console (Python %s)
 
205
Type "help", "copyright", "credits" or "license" for more information.
 
206
""" % (ivle.__version__, python_version))
 
207
        return {'output': splash_text}
 
208
 
 
209
    def handle_chat(self, params):
 
210
        # Set up the partial cmd buffer
 
211
        if self.curr_cmd == '':
 
212
            self.curr_cmd = params
 
213
        else:
 
214
            self.curr_cmd = self.curr_cmd + '\n' + params
 
215
 
 
216
        # Try to execute the buffer
 
217
        try:
 
218
            # A single trailing newline simply indicates that the line is
 
219
            # finished. Two trailing newlines indicate the end of a block.
 
220
            # Unfortunately, codeop.CommandCompiler causes even one to
 
221
            # terminate a block.
 
222
            # Thus we need to remove a trailing newline from the command,
 
223
            # unless there are *two* trailing newlines, or multi-line indented
 
224
            # blocks are impossible. See Google Code issue 105.
 
225
            cmd_text = self.curr_cmd
 
226
            if cmd_text.endswith('\n') and not cmd_text.endswith('\n\n'):
 
227
                cmd_text = cmd_text[:-1]
 
228
            cmd = self.cc(cmd_text, '<web session>')
 
229
            if cmd is None:
 
230
                # The command was incomplete, so send back a None, so the              
 
231
                # client can print a '...'
 
232
                return({"more":None})
 
233
            else:
 
234
                return(self.execCmd(cmd))
 
235
        except:
 
236
            # Clear any partial command
 
237
            self.curr_cmd = ''
 
238
            # Flush the output buffers
 
239
            sys.stderr.flush()
 
240
            sys.stdout.flush()
 
241
            # Return the exception
 
242
            tb = format_exc_start(start=3)
 
243
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
244
 
 
245
    def handle_block(self, params):
 
246
        # throw away any partial command.
 
247
        self.curr_cmd = ''
 
248
 
 
249
        # Try to execute a complete block of code
 
250
        try:
 
251
            cmd = compile(params, "<web session>", 'exec');
 
252
            return(self.execCmd(cmd))
 
253
        except:
 
254
            # Flush the output buffers
 
255
            sys.stderr.flush()
 
256
            sys.stdout.flush()
 
257
            # Return the exception
 
258
            tb = format_exc_start(start=1)
 
259
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
260
 
 
261
    def handle_globals(self, params):
 
262
        # Unpickle the new space (if provided)
 
263
        if isinstance(params, dict):
 
264
            self.globs = {'__name__': '__main__'}
 
265
            for g in params:
 
266
                try:
 
267
                    self.globs[g] = cPickle.loads(str(params[g]))
 
268
                except cPickle.UnpicklingError:
 
269
                    pass
 
270
 
 
271
        # Return the current globals
 
272
        return({'globals': flatten(self.globs)})
 
273
 
 
274
    def handle_call(self, params):
 
275
        call = {}
 
276
        
 
277
        # throw away any partial command.
 
278
        self.curr_cmd = ''
 
279
 
 
280
        if isinstance(params, dict):
 
281
            try:
 
282
                # Expand parameters
 
283
                if isinstance(params['args'], list):
 
284
                    args = map(self.eval, params['args'])
 
285
                else:
 
286
                    args = []
 
287
                if isinstance(params['kwargs'], dict):
 
288
                    kwargs = {}
 
289
                    for kwarg in params['kwargs']:
 
290
                        kwargs[kwarg] = self.eval(
 
291
                            params['kwargs'][kwarg])
 
292
                else:
 
293
                    kwargs = {}
 
294
 
 
295
                # Run the fuction
 
296
                function = self.eval(params['function'])
 
297
                try:
 
298
                    call['result'] = function(*args, **kwargs)
 
299
                except Exception, e:
 
300
                    exception = {}
 
301
                    tb = format_exc_start(start=1)
 
302
                    exception['traceback'] = \
 
303
                        ''.join(tb).decode('utf-8', 'replace')
 
304
                    exception['except'] = cPickle.dumps(e,
 
305
                        PICKLEVERSION)
 
306
                    call['exception'] = exception
 
307
            except Exception, e:
 
308
                tb = format_exc_start(start=1)
 
309
                call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
 
310
            
 
311
            # Flush the output buffers
 
312
            sys.stderr.flush()
 
313
            sys.stdout.flush()
 
314
 
 
315
            # Write out the inspection object
 
316
            return(call)
 
317
        else:
 
318
            return({'response': 'failure'})
 
319
 
 
320
    def handle_execute(self, params):
 
321
        # throw away any partial command.
 
322
        self.curr_cmd = ''
 
323
        
 
324
        # Like block but return a serialization of the state
 
325
        # throw away partial command
 
326
        response = {'okay': None}
 
327
        try:
 
328
            cmd = compile(params, "<web session>", 'exec');
 
329
            # We don't expect a return value - 'single' symbol prints it.
 
330
            self.eval(cmd)
 
331
        except Exception, e:
 
332
            response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
 
333
           
 
334
        # Flush the output
 
335
        sys.stderr.flush()
 
336
        sys.stdout.flush()
 
337
               
 
338
        # Return the inspection object
 
339
        return(response)
 
340
 
 
341
    def handle_setvars(self, params):
 
342
        # Adds some variables to the global dictionary
 
343
        for var in params['set_vars']:
 
344
            try:
 
345
                self.globs[var] = self.eval(params['set_vars'][var])
 
346
            except Exception, e:
 
347
                tb = format_exc_start(start=1)
 
348
                return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
349
 
 
350
        return({'okay': None})
 
351
 
 
352
    def eval(self, source):
 
353
        """ Evaluates a string in the private global space """
 
354
        return eval(source, self.globs)
 
355
 
 
356
# The global 'magic' is the secret that the client and server share
 
357
# which is used to create and md5 digest to authenticate requests.
 
358
# It is assigned a real value at startup.
 
359
magic = ''
 
360
 
 
361
cmdQ = Queue.Queue()
 
362
lineQ = Queue.Queue()
 
363
interpThread = PythonRunner(cmdQ, lineQ)
 
364
terminate = None
 
365
 
 
366
# Default expiry time of 15 minutes
 
367
expiry = ExpiryTimer(15 * 60)
 
368
 
 
369
def initializer():
 
370
    interpThread.setDaemon(True)
 
371
    interpThread.start()
 
372
    signal.signal(signal.SIGXCPU, sig_handler)
 
373
    expiry.ping()
 
374
 
 
375
def sig_handler(signum, frame):
 
376
    """Handles response from signals"""
 
377
    global terminate
 
378
    if signum == signal.SIGXCPU:
 
379
        terminate = "CPU time limit exceeded"
 
380
 
 
381
def dispatch_msg(msg):
 
382
    global terminate
 
383
    if msg['cmd'] == 'terminate':
 
384
        terminate = "User requested restart"
 
385
    if terminate:
 
386
        raise ivle.chat.Terminate({"terminate":terminate})
 
387
    expiry.ping()
 
388
    lineQ.put((msg['cmd'],msg['text']))
 
389
    response = cmdQ.get()
 
390
    if terminate:
 
391
        raise ivle.chat.Terminate({"terminate":terminate})
 
392
    return response
 
393
 
 
394
def format_exc_start(start=0):
 
395
    etype, value, tb = sys.exc_info()
 
396
    tbbits = traceback.extract_tb(tb)[start:]
 
397
    list = ['Traceback (most recent call last):\n']
 
398
    list = list + traceback.format_list(tbbits)
 
399
    list = list + traceback.format_exception_only(etype, value)
 
400
    return ''.join(list)
 
401
 
 
402
 
 
403
# Takes an object and returns a flattened version suitable for JSON
 
404
def flatten(object):
 
405
    flat = {}
 
406
    for o in object:
 
407
        try:
 
408
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
 
409
        except (TypeError, cPickle.PicklingError):
 
410
            try:
 
411
                o_type = type(object[o]).__name__
 
412
                o_name = object[o].__name__
 
413
                fake_o = ivle.util.FakeObject(o_type, o_name)
 
414
                flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
 
415
            except AttributeError:
 
416
                pass
 
417
    return flat
 
418
 
 
419
if __name__ == "__main__":
 
420
    port = int(sys.argv[1])
 
421
    magic = sys.argv[2]
 
422
 
 
423
    # Make python's search path follow the cwd
 
424
    sys.path[0] = ''
 
425
 
 
426
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)