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
32
from common import chat
34
trampoline_path = os.path.join(conf.ivle_install_dir, "bin/trampoline")
35
trampoline_path = os.path.join(conf.ivle_install_dir, "bin/trampoline")
36
python_path = "/usr/bin/python" # Within jail
37
console_dir = "/opt/ivle/scripts" # Within jail
38
console_path = "/opt/ivle/scripts/python-console" # Within jail
40
class ConsoleError(Exception):
41
""" The console failed in some way. This is bad. """
42
def __init__(self, value):
45
return repr(self.value)
47
class ConsoleException(Exception):
48
""" The code being exectuted on the console returned an exception.
50
def __init__(self, value):
53
return repr(self.value)
55
class Console(object):
56
""" Provides a nice python interface to the console
58
def __init__(self, uid, jail_path, working_dir):
59
"""Starts up a console service for user uid, inside chroot jail
60
jail_path with work directory of working_dir
63
self.jail_path = jail_path
64
self.working_dir = working_dir
68
# TODO: Check if we are already running a console. If we are shut it
71
# TODO: Figure out the host name the console server is running on.
72
self.host = socket.gethostname()
76
self.magic = md5.new(uuid.uuid4().bytes).digest().encode('hex')
78
# Try to find a free port on the server.
79
# Just try some random ports in the range [3000,8000)
80
# until we either succeed, or give up. If you think this
81
# sounds risky, it isn't:
82
# For N ports (e.g. 5000) with k (e.g. 100) in use, the
83
# probability of failing to find a free port in t (e.g. 5) tries
84
# is (k / N) ** t (e.g. 3.2*10e-9).
88
self.port = int(random.uniform(3000, 8000))
90
# Start the console server (port, magic)
91
# trampoline usage: tramp uid jail_dir working_dir script_path args
92
# console usage: python-console port magic
93
cmd = ' '.join([trampoline_path, str(self.uid), self.jail_path,
94
console_dir, python_path, console_path,
95
str(self.port), str(self.magic),
106
# If we can't start the console after 5 attemps (can't find a free port
107
# during random probing, syntax errors, segfaults) throw an exception.
109
raise ConsoleError("Unable to start console service!")
111
def __chat(self, cmd, args):
112
""" A wrapper around chat.chat to comunicate directly with the
116
response = chat.chat(self.host, self.port,
117
{'cmd': cmd, 'text': args}, self.magic)
118
except socket.error, (enumber, estring):
119
if enumber == errno.ECONNREFUSED:
122
"The IVLE console has timed out due to inactivity")
124
# Some other error - probably serious
125
raise socket.error, (enumber, estring)
126
except cjson.DecodeError:
127
# Couldn't decode the JSON
129
"Communication to console process lost")
131
# Look for user errors
132
if 'exc' in response:
133
raise ConsoleException(response['exc'])
137
def chat(self, code=''):
138
""" Executes a partial block of code """
139
return self.__chat('chat', code)
141
def block(self, code):
142
""" Executes a block of code and returns the output """
143
block = self.__chat('block', code)
144
if 'output' in block:
145
return block['output']
146
elif 'okay' in block:
149
raise ConsoleException("Bad response from console: %s"%str(block))
151
def flush(self, globs=None):
152
""" Resets the consoles globals() to the default and optionally augment
153
them with a dictionary simple globals. (Must be able to be pickled)
157
if globs is not None:
159
pickled_globs[g] = cPickle.dumps(globs[g])
161
flush = self.__chat('flush', pickled_globs)
162
if 'response' in flush and flush['response'] == 'okay':
165
raise ConsoleError("Bad response from console: %s"%str(flush))
167
def call(self, function, *args, **kwargs):
168
""" Calls a function in the python console """
170
'function': cPickle.dumps(function),
173
response = self.__chat('call', call_args)
174
if 'output' in response:
175
return response['output']
178
"Bad response from console: %s"%str(response))
180
def inspect(self, code=''):
181
""" Runs a block of code in the python console returning a dictionary
182
summary of the evaluation. Currently this includes the values of
183
stdout, stderr, simple global varibles.
184
If an exception was thrown then this dictionary also includes a
185
exception dictionary containg a traceback string and the exception
188
inspection = self.__chat('inspect', code)
190
# Unpickle the globals
191
for g in inspection['globals']:
192
inspection['globals'][g] = cPickle.loads(inspection['globals'][g])
194
# Unpickle any exceptions
195
if 'exception' in inspection:
196
inspection['exception']['except'] = \
197
cPickle.loads(inspection['exception']['except'])