Annotation of /linden_release/linden/indra/lib/python/indra/ipc/saranwrap.py
Parent Directory
|
Revision Log
Revision 58 - (view) (download) (as text)
| 1 : | mjm | 57 | """\ |
| 2 : | @file saranwrap.py | ||
| 3 : | @author Phoenix | ||
| 4 : | @date 2007-07-13 | ||
| 5 : | @brief A simple, pickle based rpc mechanism which reflects python | ||
| 6 : | objects and callables. | ||
| 7 : | |||
| 8 : | $LicenseInfo:firstyear=2007&license=mit$ | ||
| 9 : | |||
| 10 : | Copyright (c) 2007-2008, Linden Research, Inc. | ||
| 11 : | |||
| 12 : | Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 13 : | of this software and associated documentation files (the "Software"), to deal | ||
| 14 : | in the Software without restriction, including without limitation the rights | ||
| 15 : | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 16 : | copies of the Software, and to permit persons to whom the Software is | ||
| 17 : | furnished to do so, subject to the following conditions: | ||
| 18 : | |||
| 19 : | The above copyright notice and this permission notice shall be included in | ||
| 20 : | all copies or substantial portions of the Software. | ||
| 21 : | |||
| 22 : | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 23 : | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 24 : | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 25 : | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 26 : | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 27 : | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| 28 : | THE SOFTWARE. | ||
| 29 : | $/LicenseInfo$ | ||
| 30 : | |||
| 31 : | This file provides classes and exceptions used for simple python level | ||
| 32 : | remote procedure calls. This is achieved by intercepting the basic | ||
| 33 : | getattr and setattr calls in a client proxy, which commnicates those | ||
| 34 : | down to the server which will dispatch them to objects in it's process | ||
| 35 : | space. | ||
| 36 : | |||
| 37 : | The basic protocol to get and set attributes is for the client proxy | ||
| 38 : | to issue the command: | ||
| 39 : | |||
| 40 : | getattr $id $name | ||
| 41 : | setattr $id $name $value | ||
| 42 : | |||
| 43 : | getitem $id $item | ||
| 44 : | setitem $id $item $value | ||
| 45 : | eq $id $rhs | ||
| 46 : | del $id | ||
| 47 : | |||
| 48 : | When the get returns a callable, the client proxy will provide a | ||
| 49 : | callable proxy which will invoke a remote procedure call. The command | ||
| 50 : | issued from the callable proxy to server is: | ||
| 51 : | |||
| 52 : | call $id $name $args $kwargs | ||
| 53 : | |||
| 54 : | If the client supplies an id of None, then the get/set/call is applied | ||
| 55 : | to the object(s) exported from the server. | ||
| 56 : | |||
| 57 : | The server will parse the get/set/call, take the action indicated, and | ||
| 58 : | return back to the caller one of: | ||
| 59 : | |||
| 60 : | value $val | ||
| 61 : | callable | ||
| 62 : | object $id | ||
| 63 : | exception $excp | ||
| 64 : | |||
| 65 : | To handle object expiration, the proxy will instruct the rpc server to | ||
| 66 : | discard objects which are no longer in use. This is handled by | ||
| 67 : | catching proxy deletion and sending the command: | ||
| 68 : | |||
| 69 : | del $id | ||
| 70 : | |||
| 71 : | The server will handle this by removing clearing it's own internal | ||
| 72 : | references. This does not mean that the object will necessarily be | ||
| 73 : | cleaned from the server, but no artificial references will remain | ||
| 74 : | after successfully completing. On completion, the server will return | ||
| 75 : | one of: | ||
| 76 : | |||
| 77 : | value None | ||
| 78 : | exception $excp | ||
| 79 : | |||
| 80 : | The server also accepts a special command for debugging purposes: | ||
| 81 : | |||
| 82 : | status | ||
| 83 : | |||
| 84 : | Which will be intercepted by the server to write back: | ||
| 85 : | |||
| 86 : | status {...} | ||
| 87 : | |||
| 88 : | The wire protocol is to pickle the Request class in this file. The | ||
| 89 : | request class is basically an action and a map of parameters' | ||
| 90 : | """ | ||
| 91 : | |||
| 92 : | import os | ||
| 93 : | import cPickle | ||
| 94 : | import struct | ||
| 95 : | import sys | ||
| 96 : | |||
| 97 : | try: | ||
| 98 : | set = set | ||
| 99 : | frozenset = frozenset | ||
| 100 : | except NameError: | ||
| 101 : | from sets import Set as set, ImmutableSet as frozenset | ||
| 102 : | |||
| 103 : | from eventlet.processes import Process | ||
| 104 : | from eventlet import api | ||
| 105 : | |||
| 106 : | # | ||
| 107 : | # debugging hooks | ||
| 108 : | # | ||
| 109 : | _g_debug_mode = False | ||
| 110 : | if _g_debug_mode: | ||
| 111 : | import traceback | ||
| 112 : | |||
| 113 : | def pythonpath_sync(): | ||
| 114 : | """ | ||
| 115 : | @brief apply the current sys.path to the environment variable PYTHONPATH, so that child processes have the same paths as the caller does. | ||
| 116 : | """ | ||
| 117 : | pypath = os.pathsep.join(sys.path) | ||
| 118 : | os.environ['PYTHONPATH'] = pypath | ||
| 119 : | |||
| 120 : | def wrap(obj, dead_callback = None): | ||
| 121 : | """ | ||
| 122 : | @brief wrap in object in another process through a saranwrap proxy | ||
| 123 : | @param object The object to wrap. | ||
| 124 : | @param dead_callback A callable to invoke if the process exits.""" | ||
| 125 : | |||
| 126 : | if type(obj).__name__ == 'module': | ||
| 127 : | return wrap_module(obj.__name__, dead_callback) | ||
| 128 : | pythonpath_sync() | ||
| 129 : | p = Process('python', [__file__, '--child'], dead_callback) | ||
| 130 : | prox = Proxy(p, p) | ||
| 131 : | prox.obj = obj | ||
| 132 : | return prox.obj | ||
| 133 : | |||
| 134 : | def wrap_module(fqname, dead_callback = None): | ||
| 135 : | """ | ||
| 136 : | @brief wrap a module in another process through a saranwrap proxy | ||
| 137 : | @param fqname The fully qualified name of the module. | ||
| 138 : | @param dead_callback A callable to invoke if the process exits.""" | ||
| 139 : | pythonpath_sync() | ||
| 140 : | global _g_debug_mode | ||
| 141 : | if _g_debug_mode: | ||
| 142 : | p = Process('python', [__file__, '--module', fqname, '--logfile', '/tmp/saranwrap.log'], dead_callback) | ||
| 143 : | else: | ||
| 144 : | p = Process('python', [__file__, '--module', fqname,], dead_callback) | ||
| 145 : | prox = Proxy(p, p) | ||
| 146 : | return prox | ||
| 147 : | |||
| 148 : | def status(proxy): | ||
| 149 : | """ | ||
| 150 : | @brief get the status from the server through a proxy | ||
| 151 : | @param proxy a saranwrap.Proxy object connected to a server.""" | ||
| 152 : | _write_request(Request('status', {}), proxy.__local_dict['_out']) | ||
| 153 : | return _read_response(None, None, proxy.__local_dict['_in'], proxy.__local_dict['_out'], None) | ||
| 154 : | |||
| 155 : | class BadResponse(Exception): | ||
| 156 : | """"This exception is raised by an saranwrap client when it could | ||
| 157 : | parse but cannot understand the response from the server.""" | ||
| 158 : | pass | ||
| 159 : | |||
| 160 : | class BadRequest(Exception): | ||
| 161 : | """"This exception is raised by a saranwrap server when it could parse | ||
| 162 : | but cannot understand the response from the server.""" | ||
| 163 : | pass | ||
| 164 : | |||
| 165 : | class UnrecoverableError(Exception): | ||
| 166 : | pass | ||
| 167 : | |||
| 168 : | class Request(object): | ||
| 169 : | "@brief A wrapper class for proxy requests to the server." | ||
| 170 : | def __init__(self, action, param): | ||
| 171 : | self._action = action | ||
| 172 : | self._param = param | ||
| 173 : | def __str__(self): | ||
| 174 : | return "Request `"+self._action+"` "+str(self._param) | ||
| 175 : | def __getitem__(self, name): | ||
| 176 : | return self._param[name] | ||
| 177 : | def action(self): | ||
| 178 : | return self._action | ||
| 179 : | |||
| 180 : | def _read_lp_hunk(stream): | ||
| 181 : | len_bytes = stream.read(4) | ||
| 182 : | length = struct.unpack('I', len_bytes)[0] | ||
| 183 : | body = stream.read(length) | ||
| 184 : | return body | ||
| 185 : | |||
| 186 : | def _read_response(id, attribute, input, output, dead_list): | ||
| 187 : | """@brief local helper method to read respones from the rpc server.""" | ||
| 188 : | try: | ||
| 189 : | str = _read_lp_hunk(input) | ||
| 190 : | _prnt(`str`) | ||
| 191 : | response = cPickle.loads(str) | ||
| 192 : | except AttributeError, e: | ||
| 193 : | raise UnrecoverableError(e) | ||
| 194 : | _prnt("response: %s" % response) | ||
| 195 : | if response[0] == 'value': | ||
| 196 : | return response[1] | ||
| 197 : | elif response[0] == 'callable': | ||
| 198 : | return CallableProxy(id, attribute, input, output, dead_list) | ||
| 199 : | elif response[0] == 'object': | ||
| 200 : | return ObjectProxy(input, output, response[1], dead_list) | ||
| 201 : | elif response[0] == 'exception': | ||
| 202 : | exp = response[1] | ||
| 203 : | raise exp | ||
| 204 : | else: | ||
| 205 : | raise BadResponse(response[0]) | ||
| 206 : | |||
| 207 : | def _write_lp_hunk(stream, hunk): | ||
| 208 : | write_length = struct.pack('I', len(hunk)) | ||
| 209 : | stream.write(write_length + hunk) | ||
| 210 : | if hasattr(stream, 'flush'): | ||
| 211 : | stream.flush() | ||
| 212 : | |||
| 213 : | def _write_request(param, output): | ||
| 214 : | _prnt("request: %s" % param) | ||
| 215 : | str = cPickle.dumps(param) | ||
| 216 : | _write_lp_hunk(output, str) | ||
| 217 : | |||
| 218 : | def _is_local(attribute): | ||
| 219 : | "Return true if the attribute should be handled locally" | ||
| 220 : | # return attribute in ('_in', '_out', '_id', '__getattribute__', '__setattr__', '__dict__') | ||
| 221 : | # good enough for now. :) | ||
| 222 : | if '__local_dict' in attribute: | ||
| 223 : | return True | ||
| 224 : | return False | ||
| 225 : | |||
| 226 : | def _prnt(message): | ||
| 227 : | global _g_debug_mode | ||
| 228 : | if _g_debug_mode: | ||
| 229 : | print message | ||
| 230 : | |||
| 231 : | _g_logfile = None | ||
| 232 : | def _log(message): | ||
| 233 : | global _g_logfile | ||
| 234 : | if _g_logfile: | ||
| 235 : | _g_logfile.write(str(os.getpid()) + ' ' + message) | ||
| 236 : | _g_logfile.write('\n') | ||
| 237 : | _g_logfile.flush() | ||
| 238 : | |||
| 239 : | def _unmunge_attr_name(name): | ||
| 240 : | """ Sometimes attribute names come in with classname prepended, not sure why. | ||
| 241 : | This function removes said classname, because we're huge hackers and we didn't | ||
| 242 : | find out what the true right thing to do is. *FIX: find out. """ | ||
| 243 : | if(name.startswith('_Proxy')): | ||
| 244 : | name = name[len('_Proxy'):] | ||
| 245 : | if(name.startswith('_ObjectProxy')): | ||
| 246 : | name = name[len('_ObjectProxy'):] | ||
| 247 : | return name | ||
| 248 : | |||
| 249 : | |||
| 250 : | class Proxy(object): | ||
| 251 : | """\ | ||
| 252 : | @class Proxy | ||
| 253 : | @brief This class wraps a remote python process, presumably available | ||
| 254 : | in an instance of an Server. | ||
| 255 : | |||
| 256 : | This is the class you will typically use as a client to a child | ||
| 257 : | process. Simply instantiate one around a file-like interface and start | ||
| 258 : | calling methods on the thing that is exported. The dir() builtin is | ||
| 259 : | not supported, so you have to know what has been exported. | ||
| 260 : | """ | ||
| 261 : | def __init__(self, input, output, dead_list = None): | ||
| 262 : | """\ | ||
| 263 : | @param input a file-like object which supports read(). | ||
| 264 : | @param output a file-like object which supports write() and flush(). | ||
| 265 : | @param id an identifier for the remote object. humans do not provide this. | ||
| 266 : | """ | ||
| 267 : | # default dead_list inside the function because all objects in method | ||
| 268 : | # argument lists are init-ed only once globally | ||
| 269 : | if dead_list is None: | ||
| 270 : | dead_list = set() | ||
| 271 : | #_prnt("Proxy::__init__") | ||
| 272 : | self.__local_dict = dict( | ||
| 273 : | _in = input, | ||
| 274 : | _out = output, | ||
| 275 : | _dead_list = dead_list, | ||
| 276 : | _id = None) | ||
| 277 : | |||
| 278 : | def __getattribute__(self, attribute): | ||
| 279 : | #_prnt("Proxy::__getattr__: %s" % attribute) | ||
| 280 : | if _is_local(attribute): | ||
| 281 : | # call base class getattribute so we actually get the local variable | ||
| 282 : | attribute = _unmunge_attr_name(attribute) | ||
| 283 : | return super(Proxy, self).__getattribute__(attribute) | ||
| 284 : | else: | ||
| 285 : | my_in = self.__local_dict['_in'] | ||
| 286 : | my_out = self.__local_dict['_out'] | ||
| 287 : | my_id = self.__local_dict['_id'] | ||
| 288 : | |||
| 289 : | _dead_list = self.__local_dict['_dead_list'] | ||
| 290 : | for dead_object in _dead_list.copy(): | ||
| 291 : | request = Request('del', {'id':dead_object}) | ||
| 292 : | _write_request(request, my_out) | ||
| 293 : | response = _read_response(my_id, attribute, my_in, my_out, _dead_list) | ||
| 294 : | _dead_list.remove(dead_object) | ||
| 295 : | |||
| 296 : | # Pass all public attributes across to find out if it is | ||
| 297 : | # callable or a simple attribute. | ||
| 298 : | request = Request('getattr', {'id':my_id, 'attribute':attribute}) | ||
| 299 : | _write_request(request, my_out) | ||
| 300 : | return _read_response(my_id, attribute, my_in, my_out, _dead_list) | ||
| 301 : | |||
| 302 : | def __setattr__(self, attribute, value): | ||
| 303 : | #_prnt("Proxy::__setattr__: %s" % attribute) | ||
| 304 : | if _is_local(attribute): | ||
| 305 : | # It must be local to this actual object, so we have to apply | ||
| 306 : | # it to the dict in a roundabout way | ||
| 307 : | attribute = _unmunge_attr_name(attribute) | ||
| 308 : | super(Proxy, self).__getattribute__('__dict__')[attribute]=value | ||
| 309 : | else: | ||
| 310 : | my_in = self.__local_dict['_in'] | ||
| 311 : | my_out = self.__local_dict['_out'] | ||
| 312 : | my_id = self.__local_dict['_id'] | ||
| 313 : | _dead_list = self.__local_dict['_dead_list'] | ||
| 314 : | # Pass the set attribute across | ||
| 315 : | request = Request('setattr', {'id':my_id, 'attribute':attribute, 'value':value}) | ||
| 316 : | _write_request(request, my_out) | ||
| 317 : | return _read_response(my_id, attribute, my_in, my_out, _dead_list) | ||
| 318 : | |||
| 319 : | class ObjectProxy(Proxy): | ||
| 320 : | """\ | ||
| 321 : | @class ObjectProxy | ||
| 322 : | @brief This class wraps a remote object in the Server | ||
| 323 : | |||
| 324 : | This class will be created during normal operation, and users should | ||
| 325 : | not need to deal with this class directly.""" | ||
| 326 : | |||
| 327 : | def __init__(self, input, output, id, dead_list): | ||
| 328 : | """\ | ||
| 329 : | @param input a file-like object which supports read(). | ||
| 330 : | @param output a file-like object which supports write() and flush(). | ||
| 331 : | @param id an identifier for the remote object. humans do not provide this. | ||
| 332 : | """ | ||
| 333 : | Proxy.__init__(self, input, output, dead_list) | ||
| 334 : | self.__local_dict['_id'] = id | ||
| 335 : | #_prnt("ObjectProxy::__init__ %s" % self._id) | ||
| 336 : | |||
| 337 : | def __del__(self): | ||
| 338 : | my_id = self.__local_dict['_id'] | ||
| 339 : | _prnt("ObjectProxy::__del__ %s" % my_id) | ||
| 340 : | self.__local_dict['_dead_list'].add(my_id) | ||
| 341 : | |||
| 342 : | def __getitem__(self, key): | ||
| 343 : | my_in = self.__local_dict['_in'] | ||
| 344 : | my_out = self.__local_dict['_out'] | ||
| 345 : | my_id = self.__local_dict['_id'] | ||
| 346 : | _dead_list = self.__local_dict['_dead_list'] | ||
| 347 : | request = Request('getitem', {'id':my_id, 'key':key}) | ||
| 348 : | _write_request(request, my_out) | ||
| 349 : | return _read_response(my_id, key, my_in, my_out, _dead_list) | ||
| 350 : | |||
| 351 : | def __setitem__(self, key, value): | ||
| 352 : | my_in = self.__local_dict['_in'] | ||
| 353 : | my_out = self.__local_dict['_out'] | ||
| 354 : | my_id = self.__local_dict['_id'] | ||
| 355 : | _dead_list = self.__local_dict['_dead_list'] | ||
| 356 : | request = Request('setitem', {'id':my_id, 'key':key, 'value':value}) | ||
| 357 : | _write_request(request, my_out) | ||
| 358 : | return _read_response(my_id, key, my_in, my_out, _dead_list) | ||
| 359 : | |||
| 360 : | def __eq__(self, rhs): | ||
| 361 : | my_in = self.__local_dict['_in'] | ||
| 362 : | my_out = self.__local_dict['_out'] | ||
| 363 : | my_id = self.__local_dict['_id'] | ||
| 364 : | _dead_list = self.__local_dict['_dead_list'] | ||
| 365 : | request = Request('eq', {'id':my_id, 'rhs':rhs.__local_dict['_id']}) | ||
| 366 : | _write_request(request, my_out) | ||
| 367 : | return _read_response(my_id, None, my_in, my_out, _dead_list) | ||
| 368 : | |||
| 369 : | def __repr__(self): | ||
| 370 : | # apparently repr(obj) skips the whole getattribute thing and just calls __repr__ | ||
| 371 : | # directly. Therefore we just pass it through the normal call pipeline, and | ||
| 372 : | # tack on a little header so that you can tell it's an object proxy. | ||
| 373 : | val = self.__repr__() | ||
| 374 : | return "saran:%s" % val | ||
| 375 : | |||
| 376 : | def __str__(self): | ||
| 377 : | # see description for __repr__, because str(obj) works the same. We don't | ||
| 378 : | # tack anything on to the return value here because str values are used as data. | ||
| 379 : | return self.__str__() | ||
| 380 : | |||
| 381 : | def __len__(self): | ||
| 382 : | # see description for __repr__, len(obj) is the same. Unfortunately, __len__ is also | ||
| 383 : | # used when determining whether an object is boolean or not, e.g. if proxied_object: | ||
| 384 : | return self.__len__() | ||
| 385 : | |||
| 386 : | def proxied_type(self): | ||
| 387 : | if type(self) is not ObjectProxy: | ||
| 388 : | return type(self) | ||
| 389 : | |||
| 390 : | my_in = self.__local_dict['_in'] | ||
| 391 : | my_out = self.__local_dict['_out'] | ||
| 392 : | my_id = self.__local_dict['_id'] | ||
| 393 : | request = Request('type', {'id':my_id}) | ||
| 394 : | _write_request(request, my_out) | ||
| 395 : | # dead list can be none because we know the result will always be | ||
| 396 : | # a value and not an ObjectProxy itself | ||
| 397 : | return _read_response(my_id, None, my_in, my_out, None) | ||
| 398 : | |||
| 399 : | class CallableProxy(object): | ||
| 400 : | """\ | ||
| 401 : | @class CallableProxy | ||
| 402 : | @brief This class wraps a remote function in the Server | ||
| 403 : | |||
| 404 : | This class will be created by an Proxy during normal operation, | ||
| 405 : | and users should not need to deal with this class directly.""" | ||
| 406 : | |||
| 407 : | def __init__(self, object_id, name, input, output, dead_list): | ||
| 408 : | #_prnt("CallableProxy::__init__: %s, %s" % (object_id, name)) | ||
| 409 : | self._object_id = object_id | ||
| 410 : | self._name = name | ||
| 411 : | self._in = input | ||
| 412 : | self._out = output | ||
| 413 : | self._dead_list = dead_list | ||
| 414 : | |||
| 415 : | def __call__(self, *args, **kwargs): | ||
| 416 : | #_prnt("CallableProxy::__call__: %s, %s" % (args, kwargs)) | ||
| 417 : | |||
| 418 : | # Pass the call across. We never build a callable without | ||
| 419 : | # having already checked if the method starts with '_' so we | ||
| 420 : | # can safely pass this one to the remote object. | ||
| 421 : | #_prnt("calling %s %s" % (self._object_id, self._name) | ||
| 422 : | request = Request('call', {'id':self._object_id, 'name':self._name, 'args':args, 'kwargs':kwargs}) | ||
| 423 : | _write_request(request, self._out) | ||
| 424 : | return _read_response(self._object_id, self._name, self._in, self._out, self._dead_list) | ||
| 425 : | |||
| 426 : | class Server(object): | ||
| 427 : | def __init__(self, input, output, export): | ||
| 428 : | """\ | ||
| 429 : | @param input a file-like object which supports read(). | ||
| 430 : | @param output a file-like object which supports write() and flush(). | ||
| 431 : | @param export an object, function, or map which is exported to clients | ||
| 432 : | when the id is None.""" | ||
| 433 : | #_log("Server::__init__") | ||
| 434 : | self._in = input | ||
| 435 : | self._out = output | ||
| 436 : | self._export = export | ||
| 437 : | self._next_id = 1 | ||
| 438 : | self._objects = {} | ||
| 439 : | |||
| 440 : | def handle_status(self, object, req): | ||
| 441 : | return { | ||
| 442 : | 'object_count':len(self._objects), | ||
| 443 : | 'next_id':self._next_id, | ||
| 444 : | 'pid':os.getpid()} | ||
| 445 : | |||
| 446 : | def handle_getattr(self, object, req): | ||
| 447 : | try: | ||
| 448 : | return getattr(object, req['attribute']) | ||
| 449 : | except AttributeError, e: | ||
| 450 : | if hasattr(object, "__getitem__"): | ||
| 451 : | return object[req['attribute']] | ||
| 452 : | else: | ||
| 453 : | raise e | ||
| 454 : | #_log('getattr: %s' % str(response)) | ||
| 455 : | |||
| 456 : | def handle_setattr(self, object, req): | ||
| 457 : | try: | ||
| 458 : | return setattr(object, req['attribute'], req['value']) | ||
| 459 : | except AttributeError, e: | ||
| 460 : | if hasattr(object, "__setitem__"): | ||
| 461 : | return object.__setitem__(req['attribute'], req['value']) | ||
| 462 : | else: | ||
| 463 : | raise e | ||
| 464 : | |||
| 465 : | def handle_getitem(self, object, req): | ||
| 466 : | return object[req['key']] | ||
| 467 : | |||
| 468 : | def handle_setitem(self, object, req): | ||
| 469 : | object[req['key']] = req['value'] | ||
| 470 : | return None # *TODO figure out what the actual return value of __setitem__ should be | ||
| 471 : | |||
| 472 : | def handle_eq(self, object, req): | ||
| 473 : | #_log("__eq__ %s %s" % (object, req)) | ||
| 474 : | rhs = None | ||
| 475 : | try: | ||
| 476 : | rhs = self._objects[req['rhs']] | ||
| 477 : | except KeyError, e: | ||
| 478 : | return False | ||
| 479 : | return (object == rhs) | ||
| 480 : | |||
| 481 : | def handle_call(self, object, req): | ||
| 482 : | #_log("calling %s " % (req['name'])) | ||
| 483 : | try: | ||
| 484 : | fn = getattr(object, req['name']) | ||
| 485 : | except AttributeError, e: | ||
| 486 : | if hasattr(object, "__setitem__"): | ||
| 487 : | fn = object[req['name']] | ||
| 488 : | else: | ||
| 489 : | raise e | ||
| 490 : | |||
| 491 : | return fn(*req['args'],**req['kwargs']) | ||
| 492 : | |||
| 493 : | def handle_del(self, object, req): | ||
| 494 : | id = req['id'] | ||
| 495 : | _log("del %s from %s" % (id, self._objects)) | ||
| 496 : | |||
| 497 : | # *TODO what does __del__ actually return? | ||
| 498 : | del self._objects[id] | ||
| 499 : | return None | ||
| 500 : | |||
| 501 : | def handle_type(self, object, req): | ||
| 502 : | return type(object) | ||
| 503 : | |||
| 504 : | def loop(self): | ||
| 505 : | """@brief Loop forever and respond to all requests.""" | ||
| 506 : | _log("Server::loop") | ||
| 507 : | while True: | ||
| 508 : | try: | ||
| 509 : | try: | ||
| 510 : | str = _read_lp_hunk(self._in) | ||
| 511 : | except EOFError: | ||
| 512 : | sys.exit(0) # normal exit | ||
| 513 : | request = cPickle.loads(str) | ||
| 514 : | _log("request: %s (%s)" % (request, self._objects)) | ||
| 515 : | req = request | ||
| 516 : | id = None | ||
| 517 : | object = None | ||
| 518 : | try: | ||
| 519 : | id = req['id'] | ||
| 520 : | if id: | ||
| 521 : | id = int(id) | ||
| 522 : | object = self._objects[id] | ||
| 523 : | #_log("id, object: %d %s" % (id, object)) | ||
| 524 : | except Exception, e: | ||
| 525 : | #_log("Exception %s" % str(e)) | ||
| 526 : | pass | ||
| 527 : | if object is None or id is None: | ||
| 528 : | id = None | ||
| 529 : | object = self._export | ||
| 530 : | #_log("found object %s" % str(object)) | ||
| 531 : | |||
| 532 : | # Handle the request via a method with a special name on the server | ||
| 533 : | handler_name = 'handle_%s' % request.action() | ||
| 534 : | |||
| 535 : | try: | ||
| 536 : | handler = getattr(self, handler_name) | ||
| 537 : | except AttributeError: | ||
| 538 : | raise BadRequest, request.action() | ||
| 539 : | |||
| 540 : | response = handler(object, request) | ||
| 541 : | |||
| 542 : | # figure out what to do with the response, and respond | ||
| 543 : | # apprpriately. | ||
| 544 : | if request.action() in ['status', 'type']: | ||
| 545 : | # have to handle these specially since we want to | ||
| 546 : | # pickle up the actual value and not return a proxy | ||
| 547 : | self.respond(['value', response]) | ||
| 548 : | elif callable(response): | ||
| 549 : | #_log("callable %s" % response) | ||
| 550 : | self.respond(['callable']) | ||
| 551 : | elif self.is_value(response): | ||
| 552 : | self.respond(['value', response]) | ||
| 553 : | else: | ||
| 554 : | self._objects[self._next_id] = response | ||
| 555 : | #_log("objects: %s" % str(self._objects)) | ||
| 556 : | self.respond(['object', self._next_id]) | ||
| 557 : | self._next_id += 1 | ||
| 558 : | except SystemExit, e: | ||
| 559 : | raise e | ||
| 560 : | except Exception, e: | ||
| 561 : | self.write_exception(e) | ||
| 562 : | except: | ||
| 563 : | self.write_exception(sys.exc_info()[0]) | ||
| 564 : | |||
| 565 : | def is_value(self, value): | ||
| 566 : | """\ | ||
| 567 : | @brief Test if value should be serialized as a simple dataset. | ||
| 568 : | @param value The value to test. | ||
| 569 : | @return Returns true if value is a simple serializeable set of data. | ||
| 570 : | """ | ||
| 571 : | return type(value) in (str,unicode,int,float,long,bool,type(None)) | ||
| 572 : | |||
| 573 : | def respond(self, body): | ||
| 574 : | _log("responding with: %s" % body) | ||
| 575 : | #_log("objects: %s" % self._objects) | ||
| 576 : | s = cPickle.dumps(body) | ||
| 577 : | _log(`s`) | ||
| 578 : | str = _write_lp_hunk(self._out, s) | ||
| 579 : | |||
| 580 : | def write_exception(self, e): | ||
| 581 : | """@brief Helper method to respond with an exception.""" | ||
| 582 : | #_log("exception: %s" % sys.exc_info()[0]) | ||
| 583 : | # TODO: serialize traceback using generalization of code from mulib.htmlexception | ||
| 584 : | self.respond(['exception', e]) | ||
| 585 : | global _g_debug_mode | ||
| 586 : | if _g_debug_mode: | ||
| 587 : | _log("traceback: %s" % traceback.format_tb(sys.exc_info()[2])) | ||
| 588 : | |||
| 589 : | |||
| 590 : | # test function used for testing that final except clause | ||
| 591 : | def raise_a_weird_error(): | ||
| 592 : | raise "oh noes you can raise a string" | ||
| 593 : | |||
| 594 : | # test function used for testing return of unpicklable exceptions | ||
| 595 : | def raise_an_unpicklable_error(): | ||
| 596 : | class Unpicklable(Exception): | ||
| 597 : | pass | ||
| 598 : | raise Unpicklable() | ||
| 599 : | |||
| 600 : | # test function used for testing return of picklable exceptions | ||
| 601 : | def raise_standard_error(): | ||
| 602 : | raise FloatingPointError() | ||
| 603 : | |||
| 604 : | # test function to make sure print doesn't break the wrapper | ||
| 605 : | def print_string(str): | ||
| 606 : | print str | ||
| 607 : | |||
| 608 : | # test function to make sure printing on stdout doesn't break the | ||
| 609 : | # wrapper | ||
| 610 : | def err_string(str): | ||
| 611 : | print >>sys.stderr, str | ||
| 612 : | |||
| 613 : | def main(): | ||
| 614 : | import optparse | ||
| 615 : | parser = optparse.OptionParser( | ||
| 616 : | usage="usage: %prog [options]", | ||
| 617 : | description="Simple saranwrap.Server wrapper") | ||
| 618 : | parser.add_option( | ||
| 619 : | '-c', '--child', default=False, action='store_true', | ||
| 620 : | help='Wrap an object serialed via setattr.') | ||
| 621 : | parser.add_option( | ||
| 622 : | '-m', '--module', type='string', dest='module', default=None, | ||
| 623 : | help='a module to load and export.') | ||
| 624 : | parser.add_option( | ||
| 625 : | '-l', '--logfile', type='string', dest='logfile', default=None, | ||
| 626 : | help='file to log to.') | ||
| 627 : | options, args = parser.parse_args() | ||
| 628 : | global _g_logfile | ||
| 629 : | if options.logfile: | ||
| 630 : | _g_logfile = open(options.logfile, 'a') | ||
| 631 : | if options.module: | ||
| 632 : | export = api.named(options.module) | ||
| 633 : | server = Server(sys.stdin, sys.stdout, export) | ||
| 634 : | elif options.child: | ||
| 635 : | server = Server(sys.stdin, sys.stdout, {}) | ||
| 636 : | |||
| 637 : | # *HACK: some modules may emit on stderr, which breaks everything. | ||
| 638 : | class NullSTDOut(object): | ||
| 639 : | def write(a, b): | ||
| 640 : | pass | ||
| 641 : | sys.stderr = NullSTDOut() | ||
| 642 : | sys.stdout = NullSTDOut() | ||
| 643 : | |||
| 644 : | # Loop until EOF | ||
| 645 : | server.loop() | ||
| 646 : | if _g_logfile: | ||
| 647 : | _g_logfile.close() | ||
| 648 : | |||
| 649 : | |||
| 650 : | if __name__ == "__main__": | ||
| 651 : | main() |
| ViewVC Help | |
| Powered by ViewVC 1.0.0 |

