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

1072 by matt.giuca
Renamed scripts to services.
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
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
19
import ivle.chat
20
import ivle.util
1072 by matt.giuca
Renamed scripts to services.
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
        '''
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
70
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
1072 by matt.giuca
Renamed scripts to services.
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:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
369
        raise ivle.chat.Terminate({"terminate":terminate})
1072 by matt.giuca
Renamed scripts to services.
370
    expiry.ping()
371
    lineQ.put((msg['cmd'],msg['text']))
372
    response = cmdQ.get()
373
    if terminate:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
374
        raise ivle.chat.Terminate({"terminate":terminate})
1072 by matt.giuca
Renamed scripts to services.
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__
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
396
                fake_o = ivle.util.FakeObject(o_type, o_name)
1072 by matt.giuca
Renamed scripts to services.
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
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
418
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)