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

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: mattgiuca
  • Date: 2008-01-09 21:33:56 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:144
Trunk, and all subdirectories with Python files:
    Added to svn:ignore all *.pyc *.pyo, to avoid compiled files
    showing up in svn st / diff / commit lists.
    Added to svn:ignore trampoline/trampoline.

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 Queue
12
 
import signal
13
 
import socket
14
 
import sys
15
 
import traceback
16
 
from threading import Thread
17
 
 
18
 
import ivle.chat
19
 
import ivle.util
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
 
        '''
69
 
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
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
173
 
        self.globs = {'__name__': '__main__'}
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 = {
183
 
            'splash': self.handle_splash,
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_splash(self, params):
203
 
        # Initial console splash screen
204
 
        python_version = '.'.join(str(v) for v in sys.version_info[:3])
205
 
        splash_text = ("""IVLE %s Python Console (Python %s)
206
 
Type "help", "copyright", "credits" or "license" for more information.
207
 
""" % (ivle.__version__, python_version))
208
 
        return {'output': splash_text}
209
 
 
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:
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>')
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):
265
 
            self.globs = {'__name__': '__main__'}
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:
380
 
        terminate = "CPU time limit exceeded"
381
 
 
382
 
def dispatch_msg(msg):
383
 
    global terminate
384
 
    if msg['cmd'] == 'terminate':
385
 
        terminate = "User requested restart"
386
 
    if terminate:
387
 
        raise ivle.chat.Terminate({"terminate":terminate})
388
 
    expiry.ping()
389
 
    lineQ.put((msg['cmd'],msg['text']))
390
 
    response = cmdQ.get()
391
 
    if terminate:
392
 
        raise ivle.chat.Terminate({"terminate":terminate})
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)
410
 
        except (TypeError, cPickle.PicklingError):
411
 
            try:
412
 
                o_type = type(object[o]).__name__
413
 
                o_name = object[o].__name__
414
 
                fake_o = ivle.util.FakeObject(o_type, o_name)
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
 
 
427
 
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)