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

« back to all changes in this revision

Viewing changes to ivle/console.py

  • Committer: mattgiuca
  • Date: 2007-12-20 05:24:36 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:102
Added: Makefile (temporary, until a real setup comes along).
    This builds bin/trampoline and chmods it properly. Requires sudo access.

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