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
37
class ConsoleError(Exception):
38
""" The console failed in some way. This is bad. """
39
def __init__(self, value):
42
return repr(self.value)
44
class ConsoleException(Exception):
45
""" The code being exectuted on the console returned an exception.
47
def __init__(self, value):
50
return repr(self.value)
52
class TruncateStringIO(StringIO.StringIO):
53
""" A class that wraps around StringIO and truncates the buffer when the
54
contents are read (except for when using getvalue).
56
def __init__(self, buffer=None):
57
StringIO.StringIO.__init__(self, buffer)
60
""" Read at most size bytes from the file (less if the read hits EOF
61
before obtaining size bytes).
63
If the size argument is negative or omitted, read all data until EOF is
64
reached. The bytes are returned as a string object. An empty string is
65
returned when EOF is encountered immediately.
71
res = StringIO.StringIO.read(self, n)
75
def readline(self, length=None):
76
""" Read one entire line from the file.
78
A trailing newline character is kept in the string (but may be absent
79
when a file ends with an incomplete line). If the size argument is
80
present and non-negative, it is a maximum byte count (including the
81
trailing newline) and an incomplete line may be returned.
83
An empty string is returned only when EOF is encountered immediately.
85
Note: Unlike stdio's fgets(), the returned string contains null
86
characters ('\0') if they occurred in the input.
88
Removes the line from the buffer.
92
res = StringIO.StringIO.readline(self, length)
93
rest = StringIO.StringIO.read(self)
98
def readlines(self, sizehint=0):
99
""" Read until EOF using readline() and return a list containing the
102
If the optional sizehint argument is present, instead of reading up to
103
EOF, whole lines totalling approximately sizehint bytes (or more to
104
accommodate a final whole line).
106
Truncates the buffer.
110
res = StringIO.StringIO.readlines(self, length)
114
class Console(object):
115
""" Provides a nice python interface to the console
117
def __init__(self, config, uid, jail_path, working_dir):
118
"""Starts up a console service for user uid, inside chroot jail
119
jail_path with work directory of working_dir
121
super(Console, self).__init__()
125
self.jail_path = jail_path
126
self.working_dir = working_dir
129
self.stdin = TruncateStringIO()
130
self.stdout = TruncateStringIO()
131
self.stderr = TruncateStringIO()
133
# Fire up the console
137
# Empty all the buffers
138
self.stdin.truncate(0)
139
self.stdout.truncate(0)
140
self.stderr.truncate(0)
142
# TODO: Check if we are already running a console. If we are shut it
145
# TODO: Figure out the host name the console server is running on.
146
self.host = socket.gethostname()
150
self.magic = hashlib.md5(uuid.uuid4().bytes).hexdigest()
152
# Try to find a free port on the server.
153
# Just try some random ports in the range [3000,8000)
154
# until we either succeed, or give up. If you think this
155
# sounds risky, it isn't:
156
# For N ports (e.g. 5000) with k (e.g. 100) in use, the
157
# probability of failing to find a free port in t (e.g. 5) tries
158
# is (k / N) ** t (e.g. 3.2*10e-9).
162
self.port = int(random.uniform(3000, 8000))
164
trampoline_path = os.path.join(self.config['paths']['lib'],
167
# Start the console server (port, magic)
168
# trampoline usage: tramp uid jail_dir working_dir script_path args
169
# console usage: python-console port magic
170
res = os.spawnv(os.P_WAIT, trampoline_path, [
173
self.config['paths']['jails']['mounts'],
174
self.config['paths']['jails']['src'],
175
self.config['paths']['jails']['template'],
177
os.path.join(self.config['paths']['share'], 'services'),
179
os.path.join(self.config['paths']['share'],
180
'services/python-console'),
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
execute['exception'] = cPickle.loads(execute['exception'])
304
def set_vars(self, variables):
305
""" Takes a dictionary of varibles to add to the console's global
306
space. These are evaluated in the local space so you can't use this to
307
set a varible to a value to be calculated on the console side.
311
vars[v] = repr(variables[v])
313
set_vars = self.__handle_chat('set_vars', vars)
315
if set_vars.get('response') != 'okay':
316
raise ConsoleError("Could not set variables")
319
""" Causes the console process to terminate """
320
return self.__chat('terminate', None)
322
class ExistingConsole(Console):
323
""" Provides a nice python interface to an existing console.
324
Note: You can't restart an existing console since there is no way to infer
325
all the starting parameters. Just start a new Console instead.
327
def __init__(self, host, port, magic):
333
self.stdin = TruncateStringIO()
334
self.stdout = TruncateStringIO()
335
self.stderr = TruncateStringIO()
338
raise NotImplementedError('You can not restart an existing console')