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, [
192
# If we can't start the console after 5 attemps (can't find a free port
193
# during random probing, syntax errors, segfaults) throw an exception.
195
raise ConsoleError("Unable to start console service!")
197
def __chat(self, cmd, args):
198
""" A wrapper around chat.chat to comunicate directly with the
202
response = chat.chat(self.host, self.port,
203
{'cmd': cmd, 'text': args}, self.magic)
204
except socket.error, (enumber, estring):
205
if enumber == errno.ECONNREFUSED:
208
"Could not establish a connection to the python console")
210
# Some other error - probably serious
211
raise socket.error, (enumber, estring)
212
except cjson.DecodeError:
213
# Couldn't decode the JSON
215
"Could not understand the python console response")
219
def __handle_chat(self, cmd, args):
220
""" A wrapper around self.__chat that handles all the messy responses
221
of chat for higher level interfaces such as inspect
224
response = self.__chat(cmd, args)
226
# Process I/O requests
227
while 'output' in response or 'input' in response:
228
if 'output' in response:
229
self.stdout.write(response['output'])
230
response = self.chat()
231
elif 'input' in response:
232
response = self.chat(self.stdin.readline())
234
# Process user exceptions
235
if 'exc' in response:
236
raise ConsoleException(response['exc'])
240
def chat(self, code=''):
241
""" Executes a partial block of code """
242
return self.__chat('chat', code)
244
def block(self, code):
245
""" Executes a block of code and returns the output """
246
block = self.__handle_chat('block', code)
247
if 'output' in block:
248
return block['output']
249
elif 'okay' in block:
252
raise ConsoleException("Bad response from console: %s"%str(block))
254
def globals(self, globs=None):
255
""" Returns a dictionary of the console's globals and optionally set
260
if globs is not None:
263
pickled_globs[g] = cPickle.dumps(globs[g])
265
globals = self.__handle_chat('globals', pickled_globs)
267
# Unpickle the globals
268
for g in globals['globals']:
269
globals['globals'][g] = cPickle.loads(globals['globals'][g])
271
return globals['globals']
274
def call(self, function, *args, **kwargs):
275
""" Calls a function in the python console. Can take in a list of
276
repr() args and dictionary of repr() values kwargs. These will be
277
evaluated on the server side.
280
'function': function,
283
call = self.__handle_chat('call', call_args)
285
# Unpickle any exceptions
286
if 'exception' in call:
287
call['exception']['except'] = \
288
cPickle.loads(call['exception']['except'])
292
def execute(self, code=''):
293
""" Runs a block of code in the python console.
294
If an exception was thrown then returns an exception object.
296
execute = self.__handle_chat('execute', code)
298
# Unpickle any exceptions
299
if 'exception' in execute:
300
return cPickle.loads(execute['exception'])
305
def set_vars(self, variables):
306
""" Takes a dictionary of varibles to add to the console's global
307
space. These are evaluated in the local space so you can't use this to
308
set a varible to a value to be calculated on the console side.
312
vars[v] = repr(variables[v])
314
set_vars = self.__handle_chat('set_vars', vars)
316
if set_vars.get('response') != 'okay':
317
raise ConsoleError("Could not set variables")
320
""" Causes the console process to terminate """
321
return self.__chat('terminate', None)
323
class ExistingConsole(Console):
324
""" Provides a nice python interface to an existing console.
325
Note: You can't restart an existing console since there is no way to infer
326
all the starting parameters. Just start a new Console instead.
328
def __init__(self, host, port, magic):
334
self.stdin = TruncateStringIO()
335
self.stdout = TruncateStringIO()
336
self.stderr = TruncateStringIO()
339
raise NotImplementedError('You can not restart an existing console')