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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: dcoles
  • Date: 2008-08-21 04:56:20 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1038
ConsoleService: Fix small regression. Ensure that the console is restarted if 
the response from the console process could not be understood.

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)