~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 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
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
17
import ivle.chat
18
import ivle.util
1072 by matt.giuca
Renamed scripts to services.
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
        '''
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
68
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
1072 by matt.giuca
Renamed scripts to services.
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
1801 by David Coles
Put "__name__" = "__main__" into globals for console.
172
        self.globs = {'__name__': '__main__'}
1072 by matt.giuca
Renamed scripts to services.
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 = {
1738 by William Grant
Print a CPythonesque version on console startup.
182
            'splash': self.handle_splash,
1072 by matt.giuca
Renamed scripts to services.
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)
1738 by William Grant
Print a CPythonesque version on console startup.
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])
1749 by William Grant
Display the IVLE version too in the console splash.
204
        splash_text = ("""IVLE %s Python Console (Python %s)
1738 by William Grant
Print a CPythonesque version on console startup.
205
Type "help", "copyright", "credits" or "license" for more information.
1749 by William Grant
Display the IVLE version too in the console splash.
206
""" % (ivle.__version__, python_version))
1738 by William Grant
Print a CPythonesque version on console startup.
207
        return {'output': splash_text}
208
1072 by matt.giuca
Renamed scripts to services.
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:
1134 by William Grant
python-console: Fix Google Code issue 105. Multi-line indented blocks are now
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>')
1072 by matt.giuca
Renamed scripts to services.
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):
1801 by David Coles
Put "__name__" = "__main__" into globals for console.
264
            self.globs = {'__name__': '__main__'}
1072 by matt.giuca
Renamed scripts to services.
265
            for g in params:
266
                try:
1836 by David Coles
python-console: Fix globals broken with new JSON library.
267
                    self.globs[g] = cPickle.loads(str(params[g]))
268
                except cPickle.UnpicklingError:
1072 by matt.giuca
Renamed scripts to services.
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:
1744 by William Grant
Improve console error messages.
379
        terminate = "CPU time limit exceeded"
1072 by matt.giuca
Renamed scripts to services.
380
381
def dispatch_msg(msg):
382
    global terminate
383
    if msg['cmd'] == 'terminate':
1744 by William Grant
Improve console error messages.
384
        terminate = "User requested restart"
1072 by matt.giuca
Renamed scripts to services.
385
    if terminate:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
386
        raise ivle.chat.Terminate({"terminate":terminate})
1072 by matt.giuca
Renamed scripts to services.
387
    expiry.ping()
388
    lineQ.put((msg['cmd'],msg['text']))
389
    response = cmdQ.get()
390
    if terminate:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
391
        raise ivle.chat.Terminate({"terminate":terminate})
1072 by matt.giuca
Renamed scripts to services.
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)
1397.1.2 by William Grant
python-console will now substitute a FakeObject when it sees a PicklingError, not just a TypeError.
409
        except (TypeError, cPickle.PicklingError):
1072 by matt.giuca
Renamed scripts to services.
410
            try:
411
                o_type = type(object[o]).__name__
412
                o_name = object[o].__name__
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
413
                fake_o = ivle.util.FakeObject(o_type, o_name)
1072 by matt.giuca
Renamed scripts to services.
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
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
426
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)