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

418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
1
#!/usr/bin/python
2
3
# usage:
603 by mattgiuca
Console now starts up in the user's home directory.
4
#   python-console <port> <magic> [<working-dir>]
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
5
6
import cjson
7
import codeop
1016 by dcoles
Console: Added a 'inspect' mode to the console to allow a bit of code to be run
8
import cPickle
9
import cStringIO
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
10
import md5
11
import os
12
import Queue
13
import signal
14
import socket
15
import sys
846 by wagrant
python-console: Print proper tracebacks on exceptions. They actually
16
import traceback
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
17
from threading import Thread
18
432 by drtomc
usrmgt: more work on this. Still some work to go.
19
import common.chat
1029 by dcoles
Tutorial Service: Ported the tutorial service to the console so that all
20
import common.util
432 by drtomc
usrmgt: more work on this. Still some work to go.
21
1016 by dcoles
Console: Added a 'inspect' mode to the console to allow a bit of code to be run
22
# This version must be supported by both the local and remote code
1017 by dcoles
Console: Refactored a lot of the console stuff into a class to make calls to
23
PICKLEVERSION = 0
1016 by dcoles
Console: Added a 'inspect' mode to the console to allow a bit of code to be run
24
598 by drtomc
console: send output back to the browser progressively.
25
class Interrupt(Exception):
628 by drtomc
console: Add output based interrupt. This allows users to interrupt long
26
    def __init__(self):
27
        Exception.__init__(self, "Interrupted!")
598 by drtomc
console: send output back to the browser progressively.
28
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
29
class ExpiryTimer(object):
30
    def __init__(self, idle):
31
        self.idle = idle
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
32
        signal.signal(signal.SIGALRM, self.timeout)
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
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)
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
45
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
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})
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
53
        expiry.ping()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
54
        action, params = self.lineQ.get()
55
        if action == 'chat':
56
            return params
57
        elif action == 'interrupt':
648 by drtomc
console: fix a trivial bug which caused it to loop if you got a syntax error in your first command.
58
            raise Interrupt()
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
59
598 by drtomc
console: send output back to the browser progressively.
60
class StdoutToWeb(object):
61
    def __init__(self, cmdQ, lineQ):
62
        self.cmdQ = cmdQ
63
        self.lineQ = lineQ
64
        self.remainder = ''
65
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
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
        '''
1033 by wagrant
python-console: Move incomplete_utf8_sequence() into common.util, where it
70
        tokill = common.util.incomplete_utf8_sequence(stuff)
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
71
        if tokill == 0:
72
            return (stuff, tokill)
73
        else:
74
            return (stuff[:-tokill], tokill)
75
598 by drtomc
console: send output back to the browser progressively.
76
    def write(self, stuff):
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
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')
660 by drtomc
console: buffering now tries to buffer enough, but not too much.
84
        self.remainder = self.remainder + stuff
85
86
        # if there's less than 128 bytes, buffer
87
        if len(self.remainder) < 128:
641 by drtomc
console: slightly more aggressive output buffering - wait till we've at least
88
            return
89
660 by drtomc
console: buffering now tries to buffer enough, but not too much.
90
        # if there's lots, then send it in 1/2K blocks
91
        while len(self.remainder) > 512:
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
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')})
660 by drtomc
console: buffering now tries to buffer enough, but not too much.
96
            expiry.ping()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
97
            action, params = self.lineQ.get()
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
98
            self.remainder = self.remainder[512 - count:]
660 by drtomc
console: buffering now tries to buffer enough, but not too much.
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")
598 by drtomc
console: send output back to the browser progressively.
103
        self.remainder = lines[-1]
104
        del lines[-1]
105
106
        if len(lines) > 0:
599 by drtomc
console: improve end of line handling.
107
            lines.append('')
108
            text = "\n".join(lines)
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
109
            self.cmdQ.put({"output":text.decode('utf-8', 'replace')})
598 by drtomc
console: send output back to the browser progressively.
110
            expiry.ping()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
111
            action, params = self.lineQ.get()
112
            if action == 'interrupt':
598 by drtomc
console: send output back to the browser progressively.
113
                raise Interrupt()
114
115
    def flush(self):
599 by drtomc
console: improve end of line handling.
116
        if len(self.remainder) > 0:
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
117
            (out, count) = self._trim_incomplete_final(self.remainder)
118
            self.cmdQ.put({"output":out.decode('utf-8', 'replace')})
599 by drtomc
console: improve end of line handling.
119
            expiry.ping()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
120
            action, params = self.lineQ.get()
874 by wagrant
python-console: We are now a shiny UTF-8 console. Don't send anything
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:]
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
125
            if action == 'interrupt':
599 by drtomc
console: improve end of line handling.
126
                raise Interrupt()
598 by drtomc
console: send output back to the browser progressively.
127
750 by dcoles
Console: Flush current output before requesting input from Web
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()"""
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
132
    # FIXME: Clean up the whole stdin, stdout, stderr mess. We really need to 
133
    # be able to deal with the streams individually.
750 by dcoles
Console: Flush current output before requesting input from Web
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
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
151
class PythonRunner(Thread):
152
    def __init__(self, cmdQ, lineQ):
153
        self.cmdQ = cmdQ
154
        self.lineQ = lineQ
750 by dcoles
Console: Flush current output before requesting input from Web
155
        self.webio = WebIO(self.cmdQ, self.lineQ)
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
156
        Thread.__init__(self)
157
158
    def execCmd(self, cmd):
159
        try:
869 by wagrant
python-console: Don't assume that our command will give a return value,
160
            # We don't expect a return value - 'single' symbol prints it.
1018 by dcoles
Console: Improvements to the console python library so we can attempt to call
161
            self.eval(cmd)
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
162
            self.curr_cmd = ''
750 by dcoles
Console: Flush current output before requesting input from Web
163
            self.webio.flush()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
164
            return({"okay": None})
846 by wagrant
python-console: Print proper tracebacks on exceptions. They actually
165
        except:
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
166
            self.curr_cmd = ''
750 by dcoles
Console: Flush current output before requesting input from Web
167
            self.webio.flush()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
168
            tb = format_exc_start(start=2)
169
            return({"exc": ''.join(tb).decode('utf-8', 'replace')})
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
170
171
    def run(self):
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
172
        # Set up global space and partial command buffer
598 by drtomc
console: send output back to the browser progressively.
173
        self.globs = {}
174
        self.curr_cmd = ''
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
175
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
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
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
192
        while True:
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
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 = {}
867 by wagrant
python-console: Fix traceback trimming when in block mode (worksheets).
283
                    tb = format_exc_start(start=1)
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
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()
1034 by dcoles
Console: Refactored the code to support supplying of stdin to the console.
319
               
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
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})
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
333
1018 by dcoles
Console: Improvements to the console python library so we can attempt to call
334
    def eval(self, source):
335
        """ Evaluates a string in the private global space """
336
        return eval(source, self.globs)
337
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
338
# The global 'magic' is the secret that the client and server share
339
# which is used to create and md5 digest to authenticate requests.
340
# It is assigned a real value at startup.
341
magic = ''
342
343
cmdQ = Queue.Queue()
344
lineQ = Queue.Queue()
345
interpThread = PythonRunner(cmdQ, lineQ)
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
346
terminate = None
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
347
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
348
# Default expiry time of 15 minutes
349
expiry = ExpiryTimer(15 * 60)
350
432 by drtomc
usrmgt: more work on this. Still some work to go.
351
def initializer():
352
    interpThread.setDaemon(True)
353
    interpThread.start()
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
354
    signal.signal(signal.SIGXCPU, sig_handler)
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
355
    expiry.ping()
432 by drtomc
usrmgt: more work on this. Still some work to go.
356
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
357
def sig_handler(signum, frame):
358
    """Handles response from signals"""
359
    global terminate
360
    if signum == signal.SIGXCPU:
361
        terminate = "CPU Time Limit Exceeded"
362
432 by drtomc
usrmgt: more work on this. Still some work to go.
363
def dispatch_msg(msg):
991 by dcoles
Console: Some improvements to the python console code - most notably the
364
    global terminate
1034 by dcoles
Console: Refactored the code to support supplying of stdin to the console.
365
    if msg['cmd'] == 'terminate':
366
        terminate = "User requested console be terminated"
991 by dcoles
Console: Some improvements to the python console code - most notably the
367
    if terminate:
1034 by dcoles
Console: Refactored the code to support supplying of stdin to the console.
368
        raise common.chat.Terminate({"terminate":terminate})
522 by drtomc
Add quite a lot of stuff to get usrmgt happening.
369
    expiry.ping()
1037 by dcoles
Console: More clean up work to try and prevent coding errors from getting cmdQ
370
    lineQ.put((msg['cmd'],msg['text']))
1043 by dcoles
Console: Minor fixes. ConsoleService will now also restart the python-console
371
    response = cmdQ.get()
977 by dcoles
Console: Work on making console more responsive to signals (rather than just
372
    if terminate:
1034 by dcoles
Console: Refactored the code to support supplying of stdin to the console.
373
        raise common.chat.Terminate({"terminate":terminate})
1043 by dcoles
Console: Minor fixes. ConsoleService will now also restart the python-console
374
    return response
432 by drtomc
usrmgt: more work on this. Still some work to go.
375
846 by wagrant
python-console: Print proper tracebacks on exceptions. They actually
376
def format_exc_start(start=0):
377
    etype, value, tb = sys.exc_info()
378
    tbbits = traceback.extract_tb(tb)[start:]
379
    list = ['Traceback (most recent call last):\n']
380
    list = list + traceback.format_list(tbbits)
381
    list = list + traceback.format_exception_only(etype, value)
382
    return ''.join(list)
383
871 by mattgiuca
scripts/python-console: Added function incomplete_utf8_sequence, for use with
384
1016 by dcoles
Console: Added a 'inspect' mode to the console to allow a bit of code to be run
385
# Takes an object and returns a flattened version suitable for JSON
386
def flatten(object):
387
    flat = {}
388
    for o in object:
389
        try:
390
            flat[o] = cPickle.dumps(object[o], PICKLEVERSION)
1029 by dcoles
Tutorial Service: Ported the tutorial service to the console so that all
391
        except TypeError:
392
            try:
393
                o_type = type(object[o]).__name__
394
                o_name = object[o].__name__
395
                fake_o = common.util.FakeObject(o_type, o_name)
396
                flat[o] = cPickle.dumps(fake_o, PICKLEVERSION)
397
            except AttributeError:
398
                pass
1016 by dcoles
Console: Added a 'inspect' mode to the console to allow a bit of code to be run
399
    return flat
400
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
401
if __name__ == "__main__":
402
    port = int(sys.argv[1])
403
    magic = sys.argv[2]
974 by dcoles
Console: Sanitise Environment varibles. We now only give PATH and HOME (and
404
    
405
    # Sanitise the Enviroment
406
    os.environ = {}
407
    os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
408
603 by mattgiuca
Console now starts up in the user's home directory.
409
    if len(sys.argv) >= 4:
410
        # working_dir
411
        os.chdir(sys.argv[3])
662 by drtomc
console: set the environment variable HOME so matplotlib works in the console.
412
        os.environ['HOME'] = sys.argv[3]
418 by mattgiuca
Renamed trunk/console to trunk/scripts. We are now able to put more scripts in
413
974 by dcoles
Console: Sanitise Environment varibles. We now only give PATH and HOME (and
414
    # Make python's search path follow the cwd
415
    sys.path[0] = ''
416
432 by drtomc
usrmgt: more work on this. Still some work to go.
417
    common.chat.start_server(port, magic, True, dispatch_msg, initializer)