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

« back to all changes in this revision

Viewing changes to scripts/python-console

Moved groups over to the new class-based xhtml templating way of being
displayed

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