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 ivle import (chat, util)
39
trampoline_path = os.path.join(ivle.conf.lib_path, "trampoline")
41
python_path = "/usr/bin/python"
42
console_dir = os.path.join(ivle.conf.share_path, 'services')
43
console_path = os.path.join(console_dir, '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
174
res = os.spawnv(os.P_WAIT, trampoline_path, [
178
ivle.conf.jail_src_base,
179
ivle.conf.jail_system,
195
# If we can't start the console after 5 attemps (can't find a free port
196
# during random probing, syntax errors, segfaults) throw an exception.
198
raise ConsoleError("Unable to start console service!")
200
def __chat(self, cmd, args):
201
""" A wrapper around chat.chat to comunicate directly with the
205
response = chat.chat(self.host, self.port,
206
{'cmd': cmd, 'text': args}, self.magic)
207
except socket.error, (enumber, estring):
208
if enumber == errno.ECONNREFUSED:
211
"Could not establish a connection to the python console")
213
# Some other error - probably serious
214
raise socket.error, (enumber, estring)
215
except cjson.DecodeError:
216
# Couldn't decode the JSON
218
"Could not understand the python console response")
222
def __handle_chat(self, cmd, args):
223
""" A wrapper around self.__chat that handles all the messy responses
224
of chat for higher level interfaces such as inspect
227
response = self.__chat(cmd, args)
229
# Process I/O requests
230
while 'output' in response or 'input' in response:
231
if 'output' in response:
232
self.stdout.write(response['output'])
233
response = self.chat()
234
elif 'input' in response:
235
response = self.chat(self.stdin.readline())
237
# Process user exceptions
238
if 'exc' in response:
239
raise ConsoleException(response['exc'])
243
def chat(self, code=''):
244
""" Executes a partial block of code """
245
return self.__chat('chat', code)
247
def block(self, code):
248
""" Executes a block of code and returns the output """
249
block = self.__handle_chat('block', code)
250
if 'output' in block:
251
return block['output']
252
elif 'okay' in block:
255
raise ConsoleException("Bad response from console: %s"%str(block))
257
def globals(self, globs=None):
258
""" Returns a dictionary of the console's globals and optionally set
263
if globs is not None:
266
pickled_globs[g] = cPickle.dumps(globs[g])
268
globals = self.__handle_chat('globals', pickled_globs)
270
# Unpickle the globals
271
for g in globals['globals']:
272
globals['globals'][g] = cPickle.loads(globals['globals'][g])
274
return globals['globals']
277
def call(self, function, *args, **kwargs):
278
""" Calls a function in the python console. Can take in a list of
279
repr() args and dictionary of repr() values kwargs. These will be
280
evaluated on the server side.
283
'function': function,
286
call = self.__handle_chat('call', call_args)
288
# Unpickle any exceptions
289
if 'exception' in call:
290
call['exception']['except'] = \
291
cPickle.loads(call['exception']['except'])
295
def execute(self, code=''):
296
""" Runs a block of code in the python console.
297
If an exception was thrown then returns an exception object.
299
execute = self.__handle_chat('execute', code)
301
# Unpickle any exceptions
302
if 'exception' in execute:
303
return cPickle.loads(execute['exception'])
308
def set_vars(self, variables):
309
""" Takes a dictionary of varibles to add to the console's global
310
space. These are evaluated in the local space so you can't use this to
311
set a varible to a value to be calculated on the console side.
315
vars[v] = repr(variables[v])
317
set_vars = self.__handle_chat('set_vars', vars)
319
if set_vars.get('response') != 'okay':
320
raise ConsoleError("Could not set variables")
323
""" Causes the console process to terminate """
324
return self.__chat('terminate', None)
326
class ExistingConsole(Console):
327
""" Provides a nice python interface to an existing console.
328
Note: You can't restart an existing console since there is no way to infer
329
all the starting parameters. Just start a new Console instead.
331
def __init__(self, host, port, magic):
337
self.stdin = TruncateStringIO()
338
self.stdout = TruncateStringIO()
339
self.stderr = TruncateStringIO()
342
raise NotImplementedError('You can not restart an existing console')