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

« back to all changes in this revision

Viewing changes to scripts/python-console

  • Committer: dcoles
  • Date: 2008-08-20 08:10:40 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1034
Console: Refactored the code to support supplying of stdin to the console.  
Renamed and unified a few call in the module - inspect is now execute since 
stdout is written to the console.stdout file-like object rather than being 
returned as part of the call. __chat becomes split into __chat which deals with 
the low level chat protocol issues and __handle_chat which deals with console's 
own protocol.

Updated tutorial service to use these new calls. Still have to fix the 
'add_stdin' function but should be very simple now. (Just write to 
console.stdin)

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