2
# Copyright (C) 2007-2008 The University of Melbourne
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.
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.
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
19
# Author: Matt Giuca, Tom Conway, David Coles (refactor)
22
# Mainly refactored out of consoleservice
33
from ivle import chat, interpret
35
class ConsoleError(Exception):
36
""" The console failed in some way. This is bad. """
39
class ConsoleException(Exception):
40
""" The code being exectuted on the console returned an exception.
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).
48
def __init__(self, buffer=None):
49
StringIO.StringIO.__init__(self, buffer)
52
""" Read at most size bytes from the file (less if the read hits EOF
53
before obtaining size bytes).
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.
63
res = StringIO.StringIO.read(self, n)
67
def readline(self, length=None):
68
""" Read one entire line from the file.
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.
75
An empty string is returned only when EOF is encountered immediately.
77
Note: Unlike stdio's fgets(), the returned string contains null
78
characters ('\0') if they occurred in the input.
80
Removes the line from the buffer.
84
res = StringIO.StringIO.readline(self, length)
85
rest = StringIO.StringIO.read(self)
90
def readlines(self, sizehint=0):
91
""" Read until EOF using readline() and return a list containing the
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).
102
res = StringIO.StringIO.readlines(self, length)
106
class Console(object):
107
""" Provides a nice python interface to the console
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
113
super(Console, self).__init__()
117
self.jail_path = jail_path
118
self.working_dir = working_dir
121
self.stdin = TruncateStringIO()
122
self.stdout = TruncateStringIO()
123
self.stderr = TruncateStringIO()
125
# Fire up the console
129
# Empty all the buffers
130
self.stdin.truncate(0)
131
self.stdout.truncate(0)
132
self.stderr.truncate(0)
134
# TODO: Check if we are already running a console. If we are shut it
137
# TODO: Figure out the host name the console server is running on.
138
self.host = socket.gethostname()
142
self.magic = hashlib.md5(uuid.uuid4().bytes).hexdigest()
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).
155
self.port = int(random.uniform(3000, 8000))
157
python_console = os.path.join(self.config['paths']['share'],
158
'services/python-console')
159
args = [python_console, str(self.port), str(self.magic)]
162
interpret.execute_raw(self.config, self.user, self.jail_path,
163
self.working_dir, "/usr/bin/python", args)
166
except interpret.ExecutionError, e:
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
174
raise ConsoleError('Unable to start console service: %s'%error)
176
def __chat(self, cmd, args):
177
""" A wrapper around chat.chat to comunicate directly with the
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:
187
"Could not establish a connection to the python console")
189
# Some other error - probably serious
190
raise socket.error, (enumber, estring)
192
# Couldn't decode the JSON
194
"Could not understand the python console response")
195
except chat.ProtocolError, e:
196
raise ConsoleError(*e.args)
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
205
response = self.__chat(cmd, args)
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())
215
# Process user exceptions
216
if 'exc' in response:
217
raise ConsoleException(response['exc'])
221
def chat(self, code=''):
222
""" Executes a partial block of code """
223
return self.__chat('chat', code)
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:
233
raise ConsoleException("Bad response from console: %s"%str(block))
235
def globals(self, globs=None):
236
""" Returns a dictionary of the console's globals and optionally set
241
if globs is not None:
244
pickled_globs[g] = cPickle.dumps(globs[g])
246
globals = self.__handle_chat('globals', pickled_globs)
248
# Unpickle the globals
249
for g in globals['globals']:
250
globals['globals'][g] = cPickle.loads(str(globals['globals'][g]))
252
return globals['globals']
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.
261
'function': function,
264
call = self.__handle_chat('call', call_args)
266
# Unpickle any exceptions
267
if 'exception' in call:
268
call['exception']['except'] = \
269
cPickle.loads(str(call['exception']['except']))
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.
277
execute = self.__handle_chat('execute', code)
279
# Unpickle any exceptions
280
if 'exception' in execute:
281
execute['exception'] = cPickle.loads(str(execute['exception']))
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.
292
vars[v] = repr(variables[v])
294
set_vars = self.__handle_chat('set_vars', vars)
296
if set_vars.get('response') != 'okay':
297
raise ConsoleError("Could not set variables")
300
""" Causes the console process to terminate """
301
return self.__chat('terminate', None)
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.
308
def __init__(self, host, port, magic):
314
self.stdin = TruncateStringIO()
315
self.stdout = TruncateStringIO()
316
self.stderr = TruncateStringIO()
319
raise NotImplementedError('You can not restart an existing console')