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

« back to all changes in this revision

Viewing changes to ivle/console.py

  • Committer: Matt Giuca
  • Date: 2010-02-18 08:13:32 UTC
  • Revision ID: matt.giuca@gmail.com-20100218081332-2tw8i5sa5j3lxtlb
fileservice_lib.listing: Deleted a stupid line which took a path, treated it as a URL, and took the path segment out. This fixes launchpad bug #523654 -- files with # in their name, and presumably other characters, can't open a dir listing.

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