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
35
from ivle import chat, interpret
37
class ConsoleError(Exception):
38
""" The console failed in some way. This is bad. """
41
class ConsoleException(Exception):
42
""" The code being exectuted on the console returned an exception.
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).
50
def __init__(self, buffer=None):
51
StringIO.StringIO.__init__(self, buffer)
54
""" Read at most size bytes from the file (less if the read hits EOF
55
before obtaining size bytes).
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.
65
res = StringIO.StringIO.read(self, n)
69
def readline(self, length=None):
70
""" Read one entire line from the file.
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.
77
An empty string is returned only when EOF is encountered immediately.
79
Note: Unlike stdio's fgets(), the returned string contains null
80
characters ('\0') if they occurred in the input.
82
Removes the line from the buffer.
86
res = StringIO.StringIO.readline(self, length)
87
rest = StringIO.StringIO.read(self)
92
def readlines(self, sizehint=0):
93
""" Read until EOF using readline() and return a list containing the
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).
100
Truncates the buffer.
104
res = StringIO.StringIO.readlines(self, length)
108
class Console(object):
109
""" Provides a nice python interface to the console
111
def __init__(self, config, user, 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
115
super(Console, self).__init__()
119
self.jail_path = jail_path
120
self.working_dir = working_dir
123
self.stdin = TruncateStringIO()
124
self.stdout = TruncateStringIO()
125
self.stderr = TruncateStringIO()
127
# Fire up the console
131
# Empty all the buffers
132
self.stdin.truncate(0)
133
self.stdout.truncate(0)
134
self.stderr.truncate(0)
136
# TODO: Check if we are already running a console. If we are shut it
139
# TODO: Figure out the host name the console server is running on.
140
self.host = socket.gethostname()
144
self.magic = hashlib.md5(uuid.uuid4().bytes).hexdigest()
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).
157
self.port = int(random.uniform(3000, 8000))
159
python_console = os.path.join(self.config['paths']['share'],
160
'services/python-console')
161
args = [python_console, str(self.port), str(self.magic)]
164
interpret.execute_raw(self.config, self.user, self.jail_path,
165
self.working_dir, "/usr/bin/python", args)
168
except interpret.ExecutionError, e:
172
# If we can't start the console after 5 attemps (can't find a free
173
# port during random probing, syntax errors, segfaults) throw an
176
raise ConsoleError('Unable to start console service: %s'%error)
178
def __chat(self, cmd, args):
179
""" A wrapper around chat.chat to comunicate directly with the
183
response = chat.chat(self.host, self.port,
184
{'cmd': cmd, 'text': args}, self.magic)
185
except socket.error, (enumber, estring):
186
if enumber == errno.ECONNREFUSED:
189
"Could not establish a connection to the python console")
191
# Some other error - probably serious
192
raise socket.error, (enumber, estring)
193
except cjson.DecodeError:
194
# Couldn't decode the JSON
196
"Could not understand the python console response")
197
except chat.ProtocolError, e:
198
raise ConsoleError(*e.args)
202
def __handle_chat(self, cmd, args):
203
""" A wrapper around self.__chat that handles all the messy responses
204
of chat for higher level interfaces such as inspect
207
response = self.__chat(cmd, args)
209
# Process I/O requests
210
while 'output' in response or 'input' in response:
211
if 'output' in response:
212
self.stdout.write(response['output'])
213
response = self.chat()
214
elif 'input' in response:
215
response = self.chat(self.stdin.readline())
217
# Process user exceptions
218
if 'exc' in response:
219
raise ConsoleException(response['exc'])
223
def chat(self, code=''):
224
""" Executes a partial block of code """
225
return self.__chat('chat', code)
227
def block(self, code):
228
""" Executes a block of code and returns the output """
229
block = self.__handle_chat('block', code)
230
if 'output' in block:
231
return block['output']
232
elif 'okay' in block:
235
raise ConsoleException("Bad response from console: %s"%str(block))
237
def globals(self, globs=None):
238
""" Returns a dictionary of the console's globals and optionally set
243
if globs is not None:
246
pickled_globs[g] = cPickle.dumps(globs[g])
248
globals = self.__handle_chat('globals', pickled_globs)
250
# Unpickle the globals
251
for g in globals['globals']:
252
globals['globals'][g] = cPickle.loads(globals['globals'][g])
254
return globals['globals']
257
def call(self, function, *args, **kwargs):
258
""" Calls a function in the python console. Can take in a list of
259
repr() args and dictionary of repr() values kwargs. These will be
260
evaluated on the server side.
263
'function': function,
266
call = self.__handle_chat('call', call_args)
268
# Unpickle any exceptions
269
if 'exception' in call:
270
call['exception']['except'] = \
271
cPickle.loads(call['exception']['except'])
275
def execute(self, code=''):
276
""" Runs a block of code in the python console.
277
If an exception was thrown then returns an exception object.
279
execute = self.__handle_chat('execute', code)
281
# Unpickle any exceptions
282
if 'exception' in execute:
283
execute['exception'] = cPickle.loads(execute['exception'])
287
def set_vars(self, variables):
288
""" Takes a dictionary of varibles to add to the console's global
289
space. These are evaluated in the local space so you can't use this to
290
set a varible to a value to be calculated on the console side.
294
vars[v] = repr(variables[v])
296
set_vars = self.__handle_chat('set_vars', vars)
298
if set_vars.get('response') != 'okay':
299
raise ConsoleError("Could not set variables")
302
""" Causes the console process to terminate """
303
return self.__chat('terminate', None)
305
class ExistingConsole(Console):
306
""" Provides a nice python interface to an existing console.
307
Note: You can't restart an existing console since there is no way to infer
308
all the starting parameters. Just start a new Console instead.
310
def __init__(self, host, port, magic):
316
self.stdin = TruncateStringIO()
317
self.stdout = TruncateStringIO()
318
self.stderr = TruncateStringIO()
321
raise NotImplementedError('You can not restart an existing console')