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

« back to all changes in this revision

Viewing changes to ivle/console.py

  • Committer: mattgiuca
  • Date: 2008-01-22 01:47:32 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:260
debuginfo: Added & fixed up display of server config vars.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# IVLE
2
 
# Copyright (C) 2007-2008 The University of Melbourne
3
 
#
4
 
# This program is free software; you can redistribute it and/or modify
5
 
# it under the terms of the GNU General Public License as published by
6
 
# the Free Software Foundation; either version 2 of the License, or
7
 
# (at your option) any later version.
8
 
#
9
 
# This program is distributed in the hope that it will be useful,
10
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
# GNU General Public License for more details.
13
 
#
14
 
# You should have received a copy of the GNU General Public License
15
 
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
 
 
18
 
# Module: Console
19
 
# Author: Matt Giuca, Tom Conway, David Coles (refactor)
20
 
# Date: 13/8/2008
21
 
 
22
 
# Mainly refactored out of consoleservice
23
 
 
24
 
import errno
25
 
import cPickle
26
 
import hashlib
27
 
import os
28
 
import random
29
 
import socket
30
 
import StringIO
31
 
import uuid
32
 
 
33
 
import cjson
34
 
 
35
 
from ivle import chat
36
 
 
37
 
class ConsoleError(Exception):
38
 
    """ The console failed in some way. This is bad. """
39
 
    pass
40
 
 
41
 
class ConsoleException(Exception):
42
 
    """ The code being exectuted on the console returned an exception. 
43
 
    """
44
 
    pass
45
 
 
46
 
class TruncateStringIO(StringIO.StringIO):
47
 
    """ A class that wraps around StringIO and truncates the buffer when the 
48
 
    contents are read (except for when using getvalue).
49
 
    """
50
 
    def __init__(self, buffer=None):
51
 
        StringIO.StringIO.__init__(self, buffer)
52
 
    
53
 
    def read(self, n=-1):
54
 
        """ Read at most size bytes from the file (less if the read hits EOF 
55
 
        before obtaining size bytes).
56
 
 
57
 
        If the size argument is negative or omitted, read all data until EOF is      
58
 
        reached. The bytes are returned as a string object. An empty string is 
59
 
        returned when EOF is encountered immediately.
60
 
 
61
 
        Truncates the buffer.
62
 
        """
63
 
 
64
 
        self.seek(0)
65
 
        res = StringIO.StringIO.read(self, n)
66
 
        self.truncate(0)
67
 
        return res
68
 
 
69
 
    def readline(self, length=None):
70
 
        """ Read one entire line from the file.
71
 
 
72
 
        A trailing newline character is kept in the string (but may be absent 
73
 
        when a file ends with an incomplete line). If the size argument is   
74
 
        present and non-negative, it is a maximum byte count (including the      
75
 
        trailing newline) and an incomplete line may be returned.
76
 
 
77
 
        An empty string is returned only when EOF is encountered immediately.
78
 
        
79
 
        Note: Unlike stdio's fgets(), the returned string contains null   
80
 
        characters ('\0') if they occurred in the input.
81
 
 
82
 
        Removes the line from the buffer.
83
 
        """
84
 
 
85
 
        self.seek(0)
86
 
        res = StringIO.StringIO.readline(self, length)
87
 
        rest = StringIO.StringIO.read(self)
88
 
        self.truncate(0)
89
 
        self.write(rest)
90
 
        return res
91
 
 
92
 
    def readlines(self, sizehint=0):
93
 
        """ Read until EOF using readline() and return a list containing the        
94
 
        lines thus read.
95
 
        
96
 
        If the optional sizehint argument is present, instead of reading up to 
97
 
        EOF, whole lines totalling approximately sizehint bytes (or more to      
98
 
        accommodate a final whole line).
99
 
 
100
 
        Truncates the buffer.
101
 
        """
102
 
 
103
 
        self.seek(0)
104
 
        res = StringIO.StringIO.readlines(self, length)
105
 
        self.truncate(0)
106
 
        return res
107
 
 
108
 
class Console(object):
109
 
    """ Provides a nice python interface to the console
110
 
    """
111
 
    def __init__(self, config, uid, jail_path, working_dir):
112
 
        """Starts up a console service for user uid, inside chroot jail 
113
 
        jail_path with work directory of working_dir
114
 
        """
115
 
        super(Console, self).__init__()
116
 
 
117
 
        self.config = config
118
 
        self.uid = uid
119
 
        self.jail_path = jail_path
120
 
        self.working_dir = working_dir
121
 
 
122
 
        # Set up the buffers
123
 
        self.stdin = TruncateStringIO()
124
 
        self.stdout = TruncateStringIO()
125
 
        self.stderr = TruncateStringIO()
126
 
 
127
 
        # Fire up the console
128
 
        self.restart()
129
 
 
130
 
    def restart(self):
131
 
        # Empty all the buffers
132
 
        self.stdin.truncate(0)
133
 
        self.stdout.truncate(0)
134
 
        self.stderr.truncate(0)
135
 
 
136
 
        # TODO: Check if we are already running a console. If we are shut it 
137
 
        # down first.
138
 
 
139
 
        # TODO: Figure out the host name the console server is running on.
140
 
        self.host = socket.gethostname()
141
 
 
142
 
        # Create magic
143
 
        # TODO
144
 
        self.magic = hashlib.md5(uuid.uuid4().bytes).hexdigest()
145
 
 
146
 
        # Try to find a free port on the server.
147
 
        # Just try some random ports in the range [3000,8000)
148
 
        # until we either succeed, or give up. If you think this
149
 
        # sounds risky, it isn't:
150
 
        # For N ports (e.g. 5000) with k (e.g. 100) in use, the
151
 
        # probability of failing to find a free port in t (e.g. 5) tries
152
 
        # is (k / N) ** t (e.g. 3.2*10e-9).
153
 
 
154
 
        tries = 0
155
 
        while tries < 5:
156
 
            self.port = int(random.uniform(3000, 8000))
157
 
 
158
 
            trampoline_path = os.path.join(self.config['paths']['lib'],
159
 
                                           "trampoline")
160
 
 
161
 
            # Start the console server (port, magic)
162
 
            # trampoline usage: tramp uid jail_dir working_dir script_path args
163
 
            # console usage:    python-console port magic
164
 
            res = os.spawnv(os.P_WAIT, trampoline_path, [
165
 
                trampoline_path,
166
 
                str(self.uid),
167
 
                self.config['paths']['jails']['mounts'],
168
 
                self.config['paths']['jails']['src'],
169
 
                self.config['paths']['jails']['template'],
170
 
                self.jail_path,
171
 
                os.path.join(self.config['paths']['share'], 'services'),
172
 
                "/usr/bin/python",
173
 
                os.path.join(self.config['paths']['share'],
174
 
                             'services/python-console'),
175
 
                str(self.port),
176
 
                str(self.magic),
177
 
                self.working_dir
178
 
                ])
179
 
 
180
 
            if res == 0:
181
 
                # success
182
 
                break;
183
 
 
184
 
            tries += 1
185
 
 
186
 
        # If we can't start the console after 5 attemps (can't find a free port 
187
 
        # during random probing, syntax errors, segfaults) throw an exception.
188
 
        if tries == 5:
189
 
            raise ConsoleError("Unable to start console service!")
190
 
 
191
 
    def __chat(self, cmd, args):
192
 
        """ A wrapper around chat.chat to comunicate directly with the 
193
 
        console.
194
 
        """
195
 
        try:
196
 
            response = chat.chat(self.host, self.port,
197
 
                {'cmd': cmd, 'text': args}, self.magic)
198
 
        except socket.error, (enumber, estring):
199
 
            if enumber == errno.ECONNREFUSED:
200
 
                # Timeout
201
 
                raise ConsoleError(
202
 
                    "Could not establish a connection to the python console")
203
 
            else:
204
 
                # Some other error - probably serious
205
 
                raise socket.error, (enumber, estring)
206
 
        except cjson.DecodeError:
207
 
            # Couldn't decode the JSON
208
 
            raise ConsoleError(
209
 
                "Could not understand the python console response")
210
 
        except chat.ProtocolError, e:
211
 
            raise ConsoleError(*e.args)
212
 
 
213
 
        return response
214
 
 
215
 
    def __handle_chat(self, cmd, args):
216
 
        """ A wrapper around self.__chat that handles all the messy responses 
217
 
        of chat for higher level interfaces such as inspect
218
 
        """
219
 
        # Do the request
220
 
        response = self.__chat(cmd, args)
221
 
 
222
 
        # Process I/O requests
223
 
        while 'output' in response or 'input' in response:
224
 
            if 'output' in response:
225
 
                self.stdout.write(response['output'])
226
 
                response = self.chat()
227
 
            elif 'input' in response:
228
 
                response = self.chat(self.stdin.readline())
229
 
 
230
 
        # Process user exceptions
231
 
        if 'exc' in response:
232
 
            raise ConsoleException(response['exc'])
233
 
 
234
 
        return response
235
 
 
236
 
    def chat(self, code=''):
237
 
        """ Executes a partial block of code """
238
 
        return self.__chat('chat', code)
239
 
 
240
 
    def block(self, code):
241
 
        """ Executes a block of code and returns the output """
242
 
        block = self.__handle_chat('block', code)
243
 
        if 'output' in block:
244
 
            return block['output']
245
 
        elif 'okay' in block:
246
 
            return
247
 
        else:
248
 
            raise ConsoleException("Bad response from console: %s"%str(block))
249
 
 
250
 
    def globals(self, globs=None):
251
 
        """ Returns a dictionary of the console's globals and optionally set 
252
 
        them to a new value
253
 
        """
254
 
        # Pickle the globals
255
 
        pickled_globs = None
256
 
        if globs is not None:
257
 
            pickled_globs = {}
258
 
            for g in globs:
259
 
                pickled_globs[g] = cPickle.dumps(globs[g])
260
 
 
261
 
        globals = self.__handle_chat('globals', pickled_globs)
262
 
 
263
 
        # Unpickle the globals
264
 
        for g in globals['globals']:
265
 
            globals['globals'][g] = cPickle.loads(globals['globals'][g])
266
 
 
267
 
        return globals['globals']
268
 
        
269
 
 
270
 
    def call(self, function, *args, **kwargs):
271
 
        """ Calls a function in the python console. Can take in a list of 
272
 
        repr() args and dictionary of repr() values kwargs. These will be 
273
 
        evaluated on the server side.
274
 
        """
275
 
        call_args = {
276
 
            'function': function,
277
 
            'args': args,
278
 
            'kwargs': kwargs}
279
 
        call = self.__handle_chat('call', call_args)
280
 
 
281
 
        # Unpickle any exceptions
282
 
        if 'exception' in call:
283
 
            call['exception']['except'] = \
284
 
                cPickle.loads(call['exception']['except'])
285
 
 
286
 
        return call
287
 
 
288
 
    def execute(self, code=''):
289
 
        """ Runs a block of code in the python console.
290
 
        If an exception was thrown then returns an exception object.
291
 
        """
292
 
        execute = self.__handle_chat('execute', code)
293
 
              
294
 
        # Unpickle any exceptions
295
 
        if 'exception' in execute:
296
 
            execute['exception'] = cPickle.loads(execute['exception'])
297
 
        return execute
298
 
 
299
 
 
300
 
    def set_vars(self, variables):
301
 
        """ Takes a dictionary of varibles to add to the console's global 
302
 
        space. These are evaluated in the local space so you can't use this to 
303
 
        set a varible to a value to be calculated on the console side.
304
 
        """
305
 
        vars = {}
306
 
        for v in variables:
307
 
            vars[v] = repr(variables[v])
308
 
 
309
 
        set_vars = self.__handle_chat('set_vars', vars)
310
 
 
311
 
        if set_vars.get('response') != 'okay':
312
 
            raise ConsoleError("Could not set variables")
313
 
 
314
 
    def close(self):
315
 
        """ Causes the console process to terminate """
316
 
        return self.__chat('terminate', None)
317
 
    
318
 
class ExistingConsole(Console):
319
 
    """ Provides a nice python interface to an existing console.
320
 
    Note: You can't restart an existing console since there is no way to infer 
321
 
    all the starting parameters. Just start a new Console instead.
322
 
    """
323
 
    def __init__(self, host, port, magic):
324
 
        self.host = host
325
 
        self.port = port
326
 
        self.magic = magic
327
 
 
328
 
        # Set up the buffers
329
 
        self.stdin = TruncateStringIO()
330
 
        self.stdout = TruncateStringIO()
331
 
        self.stderr = TruncateStringIO()
332
 
 
333
 
    def restart():
334
 
        raise NotImplementedError('You can not restart an existing console')
335