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