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