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. """
41
class ConsoleException(Exception):
42
""" The code being exectuted on the console returned an exception.
46
class TruncateStringIO(StringIO.StringIO):
47
""" A class that wraps around StringIO and truncates the buffer when the
48
contents are read (except for when using getvalue).
50
def __init__(self, buffer=None):
51
StringIO.StringIO.__init__(self, buffer)
54
""" Read at most size bytes from the file (less if the read hits EOF
55
before obtaining size bytes).
57
If the size argument is negative or omitted, read all data until EOF is
58
reached. The bytes are returned as a string object. An empty string is
59
returned when EOF is encountered immediately.
65
res = StringIO.StringIO.read(self, n)
69
def readline(self, length=None):
70
""" Read one entire line from the file.
72
A trailing newline character is kept in the string (but may be absent
73
when a file ends with an incomplete line). If the size argument is
74
present and non-negative, it is a maximum byte count (including the
75
trailing newline) and an incomplete line may be returned.
77
An empty string is returned only when EOF is encountered immediately.
79
Note: Unlike stdio's fgets(), the returned string contains null
80
characters ('\0') if they occurred in the input.
82
Removes the line from the buffer.
86
res = StringIO.StringIO.readline(self, length)
87
rest = StringIO.StringIO.read(self)
92
def readlines(self, sizehint=0):
93
""" Read until EOF using readline() and return a list containing the
96
If the optional sizehint argument is present, instead of reading up to
97
EOF, whole lines totalling approximately sizehint bytes (or more to
98
accommodate a final whole line).
100
Truncates the buffer.
104
res = StringIO.StringIO.readlines(self, length)
108
class Console(object):
109
""" Provides a nice python interface to the console
111
def __init__(self, config, uid, jail_path, working_dir):
112
"""Starts up a console service for user uid, inside chroot jail
113
jail_path with work directory of working_dir
115
super(Console, self).__init__()
119
self.jail_path = jail_path
120
self.working_dir = working_dir
123
self.stdin = TruncateStringIO()
124
self.stdout = TruncateStringIO()
125
self.stderr = TruncateStringIO()
127
# Fire up the console
131
# Empty all the buffers
132
self.stdin.truncate(0)
133
self.stdout.truncate(0)
134
self.stderr.truncate(0)
136
# TODO: Check if we are already running a console. If we are shut it
139
# TODO: Figure out the host name the console server is running on.
140
self.host = socket.gethostname()
144
self.magic = hashlib.md5(uuid.uuid4().bytes).hexdigest()
146
# Try to find a free port on the server.
147
# Just try some random ports in the range [3000,8000)
148
# until we either succeed, or give up. If you think this
149
# sounds risky, it isn't:
150
# For N ports (e.g. 5000) with k (e.g. 100) in use, the
151
# probability of failing to find a free port in t (e.g. 5) tries
152
# is (k / N) ** t (e.g. 3.2*10e-9).
156
self.port = int(random.uniform(3000, 8000))
158
trampoline_path = os.path.join(self.config['paths']['lib'],
161
# Start the console server (port, magic)
162
# trampoline usage: tramp uid jail_dir working_dir script_path args
163
# console usage: python-console port magic
164
res = os.spawnv(os.P_WAIT, trampoline_path, [
167
self.config['paths']['jails']['mounts'],
168
self.config['paths']['jails']['src'],
169
self.config['paths']['jails']['template'],
171
os.path.join(self.config['paths']['share'], 'services'),
173
os.path.join(self.config['paths']['share'],
174
'services/python-console'),
186
# If we can't start the console after 5 attemps (can't find a free port
187
# during random probing, syntax errors, segfaults) throw an exception.
189
raise ConsoleError("Unable to start console service!")
191
def __chat(self, cmd, args):
192
""" A wrapper around chat.chat to comunicate directly with the
196
response = chat.chat(self.host, self.port,
197
{'cmd': cmd, 'text': args}, self.magic)
198
except socket.error, (enumber, estring):
199
if enumber == errno.ECONNREFUSED:
202
"Could not establish a connection to the python console")
204
# Some other error - probably serious
205
raise socket.error, (enumber, estring)
206
except cjson.DecodeError:
207
# Couldn't decode the JSON
209
"Could not understand the python console response")
210
except chat.ProtocolError, e:
211
raise ConsoleError(*e.args)
215
def __handle_chat(self, cmd, args):
216
""" A wrapper around self.__chat that handles all the messy responses
217
of chat for higher level interfaces such as inspect
220
response = self.__chat(cmd, args)
222
# Process I/O requests
223
while 'output' in response or 'input' in response:
224
if 'output' in response:
225
self.stdout.write(response['output'])
226
response = self.chat()
227
elif 'input' in response:
228
response = self.chat(self.stdin.readline())
230
# Process user exceptions
231
if 'exc' in response:
232
raise ConsoleException(response['exc'])
236
def chat(self, code=''):
237
""" Executes a partial block of code """
238
return self.__chat('chat', code)
240
def block(self, code):
241
""" Executes a block of code and returns the output """
242
block = self.__handle_chat('block', code)
243
if 'output' in block:
244
return block['output']
245
elif 'okay' in block:
248
raise ConsoleException("Bad response from console: %s"%str(block))
250
def globals(self, globs=None):
251
""" Returns a dictionary of the console's globals and optionally set
256
if globs is not None:
259
pickled_globs[g] = cPickle.dumps(globs[g])
261
globals = self.__handle_chat('globals', pickled_globs)
263
# Unpickle the globals
264
for g in globals['globals']:
265
globals['globals'][g] = cPickle.loads(globals['globals'][g])
267
return globals['globals']
270
def call(self, function, *args, **kwargs):
271
""" Calls a function in the python console. Can take in a list of
272
repr() args and dictionary of repr() values kwargs. These will be
273
evaluated on the server side.
276
'function': function,
279
call = self.__handle_chat('call', call_args)
281
# Unpickle any exceptions
282
if 'exception' in call:
283
call['exception']['except'] = \
284
cPickle.loads(call['exception']['except'])
288
def execute(self, code=''):
289
""" Runs a block of code in the python console.
290
If an exception was thrown then returns an exception object.
292
execute = self.__handle_chat('execute', code)
294
# Unpickle any exceptions
295
if 'exception' in execute:
296
execute['exception'] = cPickle.loads(execute['exception'])
300
def set_vars(self, variables):
301
""" Takes a dictionary of varibles to add to the console's global
302
space. These are evaluated in the local space so you can't use this to
303
set a varible to a value to be calculated on the console side.
307
vars[v] = repr(variables[v])
309
set_vars = self.__handle_chat('set_vars', vars)
311
if set_vars.get('response') != 'okay':
312
raise ConsoleError("Could not set variables")
315
""" Causes the console process to terminate """
316
return self.__chat('terminate', None)
318
class ExistingConsole(Console):
319
""" Provides a nice python interface to an existing console.
320
Note: You can't restart an existing console since there is no way to infer
321
all the starting parameters. Just start a new Console instead.
323
def __init__(self, host, port, magic):
329
self.stdin = TruncateStringIO()
330
self.stdout = TruncateStringIO()
331
self.stderr = TruncateStringIO()
334
raise NotImplementedError('You can not restart an existing console')