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

« back to all changes in this revision

Viewing changes to services/python-console

Port ivle.webapp.filesystem.{diff,svnlog}'s media to the new framework.

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
            'chat': self.handle_chat,
 
185
            'block': self.handle_block,
 
186
            'globals': self.handle_globals,
 
187
            'call': self.handle_call,
 
188
            'execute': self.handle_execute,
 
189
            'setvars': self.handle_setvars,
 
190
            }
 
191
 
 
192
        # Run the processing loop
 
193
        while True:
 
194
            action, params = self.lineQ.get()
 
195
            try:
 
196
                response = actions[action](params)
 
197
            except Exception, e:
 
198
                response = {'error': repr(e)}
 
199
            finally:
 
200
                self.cmdQ.put(response)
 
201
                   
 
202
    def handle_chat(self, params):
 
203
        # Set up the partial cmd buffer
 
204
        if self.curr_cmd == '':
 
205
            self.curr_cmd = params
 
206
        else:
 
207
            self.curr_cmd = self.curr_cmd + '\n' + params
 
208
 
 
209
        # Try to execute the buffer
 
210
        try:
 
211
            cmd = self.cc(self.curr_cmd, '<web session>')
 
212
            if cmd is None:
 
213
                # The command was incomplete, so send back a None, so the              
 
214
                # client can print a '...'
 
215
                return({"more":None})
 
216
            else:
 
217
                return(self.execCmd(cmd))
 
218
        except:
 
219
            # Clear any partial command
 
220
            self.curr_cmd = ''
 
221
            # Flush the output buffers
 
222
            sys.stderr.flush()
 
223
            sys.stdout.flush()
 
224
            # Return the exception
 
225
            tb = format_exc_start(start=3)
 
226
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
227
 
 
228
    def handle_block(self, params):
 
229
        # throw away any partial command.
 
230
        self.curr_cmd = ''
 
231
 
 
232
        # Try to execute a complete block of code
 
233
        try:
 
234
            cmd = compile(params, "<web session>", 'exec');
 
235
            return(self.execCmd(cmd))
 
236
        except:
 
237
            # Flush the output buffers
 
238
            sys.stderr.flush()
 
239
            sys.stdout.flush()
 
240
            # Return the exception
 
241
            tb = format_exc_start(start=1)
 
242
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
243
 
 
244
    def handle_globals(self, params):
 
245
        # Unpickle the new space (if provided)
 
246
        if isinstance(params, dict):
 
247
            self.globs = {}
 
248
            for g in params:
 
249
                try:
 
250
                    self.globs[g] = cPickle.loads(params[g])
 
251
                except:
 
252
                    pass
 
253
 
 
254
        # Return the current globals
 
255
        return({'globals': flatten(self.globs)})
 
256
 
 
257
    def handle_call(self, params):
 
258
        call = {}
 
259
        
 
260
        # throw away any partial command.
 
261
        self.curr_cmd = ''
 
262
 
 
263
        if isinstance(params, dict):
 
264
            try:
 
265
                # Expand parameters
 
266
                if isinstance(params['args'], list):
 
267
                    args = map(self.eval, params['args'])
 
268
                else:
 
269
                    args = []
 
270
                if isinstance(params['kwargs'], dict):
 
271
                    kwargs = {}
 
272
                    for kwarg in params['kwargs']:
 
273
                        kwargs[kwarg] = self.eval(
 
274
                            params['kwargs'][kwarg])
 
275
                else:
 
276
                    kwargs = {}
 
277
 
 
278
                # Run the fuction
 
279
                function = self.eval(params['function'])
 
280
                try:
 
281
                    call['result'] = function(*args, **kwargs)
 
282
                except Exception, e:
 
283
                    exception = {}
 
284
                    tb = format_exc_start(start=1)
 
285
                    exception['traceback'] = \
 
286
                        ''.join(tb).decode('utf-8', 'replace')
 
287
                    exception['except'] = cPickle.dumps(e,
 
288
                        PICKLEVERSION)
 
289
                    call['exception'] = exception
 
290
            except Exception, e:
 
291
                tb = format_exc_start(start=1)
 
292
                call = {"exc": ''.join(tb).decode('utf-8', 'replace')}
 
293
            
 
294
            # Flush the output buffers
 
295
            sys.stderr.flush()
 
296
            sys.stdout.flush()
 
297
 
 
298
            # Write out the inspection object
 
299
            return(call)
 
300
        else:
 
301
            return({'response': 'failure'})
 
302
 
 
303
    def handle_execute(self, params):
 
304
        # throw away any partial command.
 
305
        self.curr_cmd = ''
 
306
        
 
307
        # Like block but return a serialization of the state
 
308
        # throw away partial command
 
309
        response = {'okay': None}
 
310
        try:
 
311
            cmd = compile(params, "<web session>", 'exec');
 
312
            # We don't expect a return value - 'single' symbol prints it.
 
313
            self.eval(cmd)
 
314
        except Exception, e:
 
315
            response = {'exception': cPickle.dumps(e, PICKLEVERSION)}
 
316
           
 
317
        # Flush the output
 
318
        sys.stderr.flush()
 
319
        sys.stdout.flush()
 
320
               
 
321
        # Return the inspection object
 
322
        return(response)
 
323
 
 
324
    def handle_setvars(self, params):
 
325
        # Adds some variables to the global dictionary
 
326
        for var in params['set_vars']:
 
327
            try:
 
328
                self.globs[var] = self.eval(params['set_vars'][var])
 
329
            except Exception, e:
 
330
                tb = format_exc_start(start=1)
 
331
                return({"exc": ''.join(tb).decode('utf-8', 'replace')})
 
332
 
 
333
        return({'okay': None})
 
334
 
 
335
    def eval(self, source):
 
336
        """ Evaluates a string in the private global space """
 
337
        return eval(source, self.globs)
 
338
 
 
339
# The global 'magic' is the secret that the client and server share
 
340
# which is used to create and md5 digest to authenticate requests.
 
341
# It is assigned a real value at startup.
 
342
magic = ''
 
343
 
 
344
cmdQ = Queue.Queue()
 
345
lineQ = Queue.Queue()
 
346
interpThread = PythonRunner(cmdQ, lineQ)
 
347
terminate = None
 
348
 
 
349
# Default expiry time of 15 minutes
 
350
expiry = ExpiryTimer(15 * 60)
 
351
 
 
352
def initializer():
 
353
    interpThread.setDaemon(True)
 
354
    interpThread.start()
 
355
    signal.signal(signal.SIGXCPU, sig_handler)
 
356
    expiry.ping()
 
357
 
 
358
def sig_handler(signum, frame):
 
359
    """Handles response from signals"""
 
360
    global terminate
 
361
    if signum == signal.SIGXCPU:
 
362
        terminate = "CPU Time Limit Exceeded"
 
363
 
 
364
def dispatch_msg(msg):
 
365
    global terminate
 
366
    if msg['cmd'] == 'terminate':
 
367
        terminate = "User requested console be terminated"
 
368
    if terminate:
 
369
        raise ivle.chat.Terminate({"terminate":terminate})
 
370
    expiry.ping()
 
371
    lineQ.put((msg['cmd'],msg['text']))
 
372
    response = cmdQ.get()
 
373
    if terminate:
 
374
        raise ivle.chat.Terminate({"terminate":terminate})
 
375
    return response
 
376
 
 
377
def format_exc_start(start=0):
 
378
    etype, value, tb = sys.exc_info()
 
379
    tbbits = traceback.extract_tb(tb)[start:]
 
380
    list = ['Traceback (most recent call last):\n']
 
381
    list = list + traceback.format_list(tbbits)
 
382
    list = list + traceback.format_exception_only(etype, value)
 
383
    return ''.join(list)
 
384
 
 
385
 
 
386
# Takes an object and returns a flattened version suitable for JSON
 
387
def flatten(object):
 
388
    flat = {}
 
389
    for o in object:
 
390
        try:
 
391
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
 
392
        except TypeError:
 
393
            try:
 
394
                o_type = type(object[o]).__name__
 
395
                o_name = object[o].__name__
 
396
                fake_o = ivle.util.FakeObject(o_type, o_name)
 
397
                flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
 
398
            except AttributeError:
 
399
                pass
 
400
    return flat
 
401
 
 
402
if __name__ == "__main__":
 
403
    port = int(sys.argv[1])
 
404
    magic = sys.argv[2]
 
405
    
 
406
    # Sanitise the Enviroment
 
407
    os.environ = {}
 
408
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
 
409
 
 
410
    if len(sys.argv) >= 4:
 
411
        # working_dir
 
412
        os.chdir(sys.argv[3])
 
413
        os.environ['HOME'] = sys.argv[3]
 
414
 
 
415
    # Make python's search path follow the cwd
 
416
    sys.path[0] = ''
 
417
 
 
418
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)