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
36
from common import (chat, util)
39
trampoline_path = os.path.join(conf.ivle_install_dir, "bin/trampoline")
41
python_path = "/usr/bin/python"
42
console_dir = "/opt/ivle/scripts"
43
console_path = "/opt/ivle/scripts/python-console"
45
class ConsoleError(Exception):
46
""" The console failed in some way. This is bad. """
47
def __init__(self, value):
50
return repr(self.value)
52
class ConsoleException(Exception):
53
""" The code being exectuted on the console returned an exception.
55
def __init__(self, value):
58
return repr(self.value)
60
class TruncateStringIO(StringIO.StringIO):
61
""" A class that wraps around StringIO and truncates the buffer when the
62
contents are read (except for when using getvalue).
64
def __init__(self, buffer=None):
65
StringIO.StringIO.__init__(self, buffer)
68
""" Read at most size bytes from the file (less if the read hits EOF
69
before obtaining size bytes).
71
If the size argument is negative or omitted, read all data until EOF is
72
reached. The bytes are returned as a string object. An empty string is
73
returned when EOF is encountered immediately.
79
res = StringIO.StringIO.read(self, n)
83
def readline(self, length=None):
84
""" Read one entire line from the file.
86
A trailing newline character is kept in the string (but may be absent
87
when a file ends with an incomplete line). If the size argument is
88
present and non-negative, it is a maximum byte count (including the
89
trailing newline) and an incomplete line may be returned.
91
An empty string is returned only when EOF is encountered immediately.
93
Note: Unlike stdio's fgets(), the returned string contains null
94
characters ('\0') if they occurred in the input.
96
Removes the line from the buffer.
100
res = StringIO.StringIO.readline(self, length)
101
rest = StringIO.StringIO.read(self)
106
def readlines(self, sizehint=0):
107
""" Read until EOF using readline() and return a list containing the
110
If the optional sizehint argument is present, instead of reading up to
111
EOF, whole lines totalling approximately sizehint bytes (or more to
112
accommodate a final whole line).
114
Truncates the buffer.
118
res = StringIO.StringIO.readlines(self, length)
122
class Console(object):
123
""" Provides a nice python interface to the console
125
def __init__(self, uid, jail_path, working_dir):
126
"""Starts up a console service for user uid, inside chroot jail
127
jail_path with work directory of working_dir
129
super(Console, self).__init__()
132
self.jail_path = jail_path
133
self.working_dir = working_dir
136
self.stdin = TruncateStringIO()
137
self.stdout = TruncateStringIO()
138
self.stderr = TruncateStringIO()
140
# Fire up the console
144
# Empty all the buffers
145
self.stdin.truncate(0)
146
self.stdout.truncate(0)
147
self.stderr.truncate(0)
149
# TODO: Check if we are already running a console. If we are shut it
152
# TODO: Figure out the host name the console server is running on.
153
self.host = socket.gethostname()
157
self.magic = md5.new(uuid.uuid4().bytes).digest().encode('hex')
159
# Try to find a free port on the server.
160
# Just try some random ports in the range [3000,8000)
161
# until we either succeed, or give up. If you think this
162
# sounds risky, it isn't:
163
# For N ports (e.g. 5000) with k (e.g. 100) in use, the
164
# probability of failing to find a free port in t (e.g. 5) tries
165
# is (k / N) ** t (e.g. 3.2*10e-9).
169
self.port = int(random.uniform(3000, 8000))
171
# Start the console server (port, magic)
172
# trampoline usage: tramp uid jail_dir working_dir script_path args
173
# console usage: python-console port magic
194
# If we can't start the console after 5 attemps (can't find a free port
195
# during random probing, syntax errors, segfaults) throw an exception.
197
raise ConsoleError("Unable to start console service!")
199
def __chat(self, cmd, args):
200
""" A wrapper around chat.chat to comunicate directly with the
204
response = chat.chat(self.host, self.port,
205
{'cmd': cmd, 'text': args}, self.magic)
206
except socket.error, (enumber, estring):
207
if enumber == errno.ECONNREFUSED:
210
"Could not establish a connection to the python console")
212
# Some other error - probably serious
213
raise socket.error, (enumber, estring)
214
except cjson.DecodeError:
215
# Couldn't decode the JSON
217
"Could not understand the python console response")
221
def __handle_chat(self, cmd, args):
222
""" A wrapper around self.__chat that handles all the messy responses
223
of chat for higher level interfaces such as inspect
226
response = self.__chat(cmd, args)
228
# Process I/O requests
229
while 'output' in response or 'input' in response:
230
if 'output' in response:
231
self.stdout.write(response['output'])
232
response = self.chat()
233
elif 'input' in response:
234
response = self.chat(self.stdin.readline())
236
# Process user exceptions
237
if 'exc' in response:
238
raise ConsoleException(response['exc'])
242
def chat(self, code=''):
243
""" Executes a partial block of code """
244
return self.__chat('chat', code)
246
def block(self, code):
247
""" Executes a block of code and returns the output """
248
block = self.__handle_chat('block', code)
249
if 'output' in block:
250
return block['output']
251
elif 'okay' in block:
254
raise ConsoleException("Bad response from console: %s"%str(block))
256
def globals(self, globs=None):
257
""" Returns a dictionary of the console's globals and optionally set
262
if globs is not None:
265
pickled_globs[g] = cPickle.dumps(globs[g])
267
globals = self.__handle_chat('globals', pickled_globs)
269
# Unpickle the globals
270
for g in globals['globals']:
271
globals['globals'][g] = cPickle.loads(globals['globals'][g])
273
return globals['globals']
276
def call(self, function, *args, **kwargs):
277
""" Calls a function in the python console. Can take in a list of
278
repr() args and dictionary of repr() values kwargs. These will be
279
evaluated on the server side.
282
'function': function,
285
call = self.__handle_chat('call', call_args)
287
# Unpickle any exceptions
288
if 'exception' in call:
289
call['exception']['except'] = \
290
cPickle.loads(call['exception']['except'])
294
def execute(self, code=''):
295
""" Runs a block of code in the python console.
296
If an exception was thrown then returns an exception object.
298
execute = self.__handle_chat('execute', code)
300
# Unpickle any exceptions
301
if 'exception' in execute:
302
return cPickle.loads(execute['exception'])
307
def set_vars(self, variables):
308
""" Takes a dictionary of varibles to add to the console's global
309
space. These are evaluated in the local space so you can't use this to
310
set a varible to a value to be calculated on the console side.
314
vars[v] = repr(variables[v])
316
set_vars = self.__handle_chat('set_vars', vars)
318
if set_vars.get('response') != 'okay':
319
raise ConsoleError("Could not set variables")
322
""" Causes the console process to terminate """
323
return self.__chat('terminate', None)
325
class ExistingConsole(Console):
326
""" Provides a nice python interface to an existing console.
327
Note: You can't restart an existing console since there is no way to infer
328
all the starting parameters. Just start a new Console instead.
330
def __init__(self, host, port, magic):
336
self.stdin = TruncateStringIO()
337
self.stdout = TruncateStringIO()
338
self.stderr = TruncateStringIO()
341
raise NotImplementedError('You can not restart an existing console')