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

« back to all changes in this revision

Viewing changes to services/python-console

  • Committer: drtomc
  • Date: 2007-12-04 00:18:29 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1
AnĀ initialĀ checkin.

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 os
12
 
import Queue
13
 
import signal
14
 
import socket
15
 
import sys
16
 
import traceback
17
 
from threading import Thread
18
 
 
19
 
import ivle.chat
20
 
import ivle.util
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
 
        '''
70
 
        tokill = ivle.util.incomplete_utf8_sequence(stuff)
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
 
            # 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>')
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:
379
 
        raise ivle.chat.Terminate({"terminate":terminate})
380
 
    expiry.ping()
381
 
    lineQ.put((msg['cmd'],msg['text']))
382
 
    response = cmdQ.get()
383
 
    if terminate:
384
 
        raise ivle.chat.Terminate({"terminate":terminate})
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)
402
 
        except TypeError:
403
 
            try:
404
 
                o_type = type(object[o]).__name__
405
 
                o_name = object[o].__name__
406
 
                fake_o = ivle.util.FakeObject(o_type, o_name)
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
 
 
428
 
    ivle.chat.start_server(port, magic, True, dispatch_msg, initializer)