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