From 215f423cbe18fe9ca14a26caef918d303bad28ff Mon Sep 17 00:00:00 2001 From: Jacek Antonelli Date: Fri, 15 Aug 2008 23:45:11 -0500 Subject: Second Life viewer sources 1.18.4.0-RC --- linden/indra/lib/python/indra/__init__.py | 33 +- linden/indra/lib/python/indra/base/__init__.py | 27 + linden/indra/lib/python/indra/base/config.py | 74 ++ linden/indra/lib/python/indra/base/llsd.py | 870 +++++++++++++++++++++ linden/indra/lib/python/indra/base/lluuid.py | 290 +++++++ linden/indra/lib/python/indra/ipc/__init__.py | 40 +- linden/indra/lib/python/indra/ipc/compatibility.py | 40 +- linden/indra/lib/python/indra/ipc/httputil.py | 9 + linden/indra/lib/python/indra/ipc/llmessage.py | 40 +- linden/indra/lib/python/indra/ipc/llsdhttp.py | 84 ++ linden/indra/lib/python/indra/ipc/mysql_pool.py | 103 +++ linden/indra/lib/python/indra/ipc/russ.py | 157 ++++ linden/indra/lib/python/indra/ipc/saranwrap.py | 637 +++++++++++++++ .../indra/lib/python/indra/ipc/servicebuilder.py | 93 +++ linden/indra/lib/python/indra/ipc/tokenstream.py | 40 +- linden/indra/lib/python/indra/ipc/webdav.py | 597 ++++++++++++++ linden/indra/lib/python/indra/ipc/xml_rpc.py | 273 +++++++ linden/indra/lib/python/indra/util/__init__.py | 40 +- .../indra/lib/python/indra/util/helpformatter.py | 52 ++ linden/indra/lib/python/indra/util/llmanifest.py | 40 +- linden/indra/lib/python/indra/util/llversion.py | 95 +++ linden/indra/lib/python/indra/util/named_query.py | 215 +++++ 22 files changed, 3709 insertions(+), 140 deletions(-) create mode 100644 linden/indra/lib/python/indra/base/__init__.py create mode 100644 linden/indra/lib/python/indra/base/config.py create mode 100644 linden/indra/lib/python/indra/base/llsd.py create mode 100644 linden/indra/lib/python/indra/base/lluuid.py create mode 100644 linden/indra/lib/python/indra/ipc/httputil.py create mode 100644 linden/indra/lib/python/indra/ipc/llsdhttp.py create mode 100644 linden/indra/lib/python/indra/ipc/mysql_pool.py create mode 100644 linden/indra/lib/python/indra/ipc/russ.py create mode 100644 linden/indra/lib/python/indra/ipc/saranwrap.py create mode 100644 linden/indra/lib/python/indra/ipc/servicebuilder.py create mode 100644 linden/indra/lib/python/indra/ipc/webdav.py create mode 100644 linden/indra/lib/python/indra/ipc/xml_rpc.py create mode 100644 linden/indra/lib/python/indra/util/helpformatter.py create mode 100644 linden/indra/lib/python/indra/util/llversion.py create mode 100644 linden/indra/lib/python/indra/util/named_query.py (limited to 'linden/indra/lib') diff --git a/linden/indra/lib/python/indra/__init__.py b/linden/indra/lib/python/indra/__init__.py index 220768f..353a93f 100644 --- a/linden/indra/lib/python/indra/__init__.py +++ b/linden/indra/lib/python/indra/__init__.py @@ -2,26 +2,19 @@ @file __init__.py @brief Initialization file for the indra module. +$LicenseInfo:firstyear=2006&license=internal$ + Copyright (c) 2006-2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +The following source code is PROPRIETARY AND CONFIDENTIAL. Use of +this source code is governed by the Linden Lab Source Code Disclosure +Agreement ("Agreement") previously entered between you and Linden +Lab. By accessing, using, copying, modifying or distributing this +software, you acknowledge that you have been informed of your +obligations under the Agreement and agree to abide by those obligations. + +ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO +WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, +COMPLETENESS OR PERFORMANCE. +$/LicenseInfo$ """ diff --git a/linden/indra/lib/python/indra/base/__init__.py b/linden/indra/lib/python/indra/base/__init__.py new file mode 100644 index 0000000..913164d --- /dev/null +++ b/linden/indra/lib/python/indra/base/__init__.py @@ -0,0 +1,27 @@ +"""\ +@file __init__.py +@brief Initialization file for the indra.base module. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" diff --git a/linden/indra/lib/python/indra/base/config.py b/linden/indra/lib/python/indra/base/config.py new file mode 100644 index 0000000..c6872fa --- /dev/null +++ b/linden/indra/lib/python/indra/base/config.py @@ -0,0 +1,74 @@ +"""\ +@file config.py +@brief Utility module for parsing and accessing the indra.xml config file. + +$LicenseInfo:firstyear=2006&license=mit$ + +Copyright (c) 2006-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +from os.path import dirname, join, realpath +import types +from indra.base import llsd + +_g_config_dict = None + +def load(indra_xml_file=None): + global _g_config_dict + if _g_config_dict == None: + if indra_xml_file is None: + ## going from: + ## "/opt/linden/indra/lib/python/indra/base/config.py" + ## to: + ## "/opt/linden/etc/indra.xml" + indra_xml_file = realpath( + dirname(realpath(__file__)) + "../../../../../../etc/indra.xml") + config_file = file(indra_xml_file) + _g_config_dict = llsd.LLSD().parse(config_file.read()) + config_file.close() + #print "loaded config from",indra_xml_file,"into",_g_config_dict + +def update(new_conf): + """Load an XML file and apply its map as overrides or additions + to the existing config. The dataserver does this with indra.xml + and dataserver.xml.""" + global _g_config_dict + if _g_config_dict == None: + _g_config_dict = {} + if isinstance(new_conf, dict): + overrides = new_conf + else: + config_file = file(new_conf) + overrides = llsd.LLSD().parse(config_file.read()) + config_file.close() + + _g_config_dict.update(overrides) + +def get(key, default = None): + global _g_config_dict + if _g_config_dict == None: + load() + return _g_config_dict.get(key, default) + +def as_dict(): + global _g_config_dict + return _g_config_dict diff --git a/linden/indra/lib/python/indra/base/llsd.py b/linden/indra/lib/python/indra/base/llsd.py new file mode 100644 index 0000000..9e636ea --- /dev/null +++ b/linden/indra/lib/python/indra/base/llsd.py @@ -0,0 +1,870 @@ +"""\ +@file llsd.py +@brief Types as well as parsing and formatting functions for handling LLSD. + +$LicenseInfo:firstyear=2006&license=mit$ + +Copyright (c) 2006-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import datetime +import base64 +import struct +import time +import types +import re + +#from cElementTree import fromstring ## This does not work under Windows +try: + ## This is the old name of elementtree, for use with 2.3 + from elementtree.ElementTree import fromstring +except ImportError: + ## This is the name of elementtree under python 2.5 + from xml.etree.ElementTree import fromstring + +from indra.base import lluuid + +int_regex = re.compile("[-+]?\d+") +real_regex = re.compile("[-+]?(\d+(\.\d*)?|\d*\.\d+)([eE][-+]?\d+)?") +alpha_regex = re.compile("[a-zA-Z]+") +date_regex = re.compile("(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(?P\.\d{2})?Z") +#date: d"YYYY-MM-DDTHH:MM:SS.FFZ" + +class LLSDParseError(Exception): + pass + +class LLSDSerializationError(Exception): + pass + + +class binary(str): + pass + +class uri(str): + pass + + +BOOL_TRUE = ('1', '1.0', 'true') +BOOL_FALSE = ('0', '0.0', 'false', '') + + +def bool_to_python(node): + val = node.text or '' + if val in BOOL_TRUE: + return True + else: + return False + +def int_to_python(node): + val = node.text or '' + if not val.strip(): + return 0 + return int(val) + +def real_to_python(node): + val = node.text or '' + if not val.strip(): + return 0.0 + return float(val) + +def uuid_to_python(node): + return lluuid.UUID(node.text) + +def str_to_python(node): + return unicode(node.text or '').encode('utf8', 'replace') + +def bin_to_python(node): + return binary(base64.decodestring(node.text or '')) + +def date_to_python(node): + val = node.text or '' + if not val: + val = "1970-01-01T00:00:00Z" + return datetime.datetime( + *time.strptime(val, '%Y-%m-%dT%H:%M:%SZ')[:6]) + +def uri_to_python(node): + val = node.text or '' + if not val: + return None + return uri(val) + +def map_to_python(node): + result = {} + for index in range(len(node))[::2]: + result[node[index].text] = to_python(node[index+1]) + return result + +def array_to_python(node): + return [to_python(child) for child in node] + + +NODE_HANDLERS = dict( + undef=lambda x: None, + boolean=bool_to_python, + integer=int_to_python, + real=real_to_python, + uuid=uuid_to_python, + string=str_to_python, + binary=bin_to_python, + date=date_to_python, + uri=uri_to_python, + map=map_to_python, + array=array_to_python, + ) + +def to_python(node): + return NODE_HANDLERS[node.tag](node) + +class Nothing(object): + pass + + +class LLSDXMLFormatter(object): + def __init__(self): + self.type_map = { + type(None) : self.UNDEF, + bool : self.BOOLEAN, + int : self.INTEGER, + long : self.INTEGER, + float : self.REAL, + lluuid.UUID : self.UUID, + binary : self.BINARY, + str : self.STRING, + unicode : self.STRING, + uri : self.URI, + datetime.datetime : self.DATE, + list : self.ARRAY, + tuple : self.ARRAY, + types.GeneratorType : self.ARRAY, + dict : self.MAP, + LLSD : self.LLSD + } + + def elt(self, name, contents=None): + if(contents is None or contents is ''): + return "<%s />" % (name,) + else: + return "<%s>%s" % (name, contents, name) + + def xml_esc(self, v): + return v.replace('&', '&').replace('<', '<').replace('>', '>') + + def LLSD(self, v): + return self.generate(v.thing) + def UNDEF(self, v): + return self.elt('undef') + def BOOLEAN(self, v): + if v: + return self.elt('boolean', 'true') + else: + return self.elt('boolean', 'false') + def INTEGER(self, v): + return self.elt('integer', v) + def REAL(self, v): + return self.elt('real', v) + def UUID(self, v): + if(v.isNull()): + return self.elt('uuid') + else: + return self.elt('uuid', v) + def BINARY(self, v): + return self.elt('binary', base64.encodestring(v)) + def STRING(self, v): + return self.elt('string', self.xml_esc(v)) + def URI(self, v): + return self.elt('uri', self.xml_esc(str(v))) + def DATE(self, v): + return self.elt('date', v.strftime('%Y-%m-%dT%H:%M:%SZ')) + def ARRAY(self, v): + return self.elt('array', ''.join([self.generate(item) for item in v])) + def MAP(self, v): + return self.elt( + 'map', + ''.join(["%s%s" % (self.elt('key', key), self.generate(value)) + for key, value in v.items()])) + + typeof = type + def generate(self, something): + t = self.typeof(something) + if self.type_map.has_key(t): + return self.type_map[t](something) + else: + raise LLSDSerializationError("Cannot serialize unknown type: %s (%s)" % ( + t, something)) + + def format(self, something): + return '' + self.elt("llsd", self.generate(something)) + +def format_xml(something): + return LLSDXMLFormatter().format(something) + +class LLSDNotationFormatter(object): + def __init__(self): + self.type_map = { + type(None) : self.UNDEF, + bool : self.BOOLEAN, + int : self.INTEGER, + long : self.INTEGER, + float : self.REAL, + lluuid.UUID : self.UUID, + binary : self.BINARY, + str : self.STRING, + unicode : self.STRING, + uri : self.URI, + datetime.datetime : self.DATE, + list : self.ARRAY, + tuple : self.ARRAY, + types.GeneratorType : self.ARRAY, + dict : self.MAP, + LLSD : self.LLSD + } + + def LLSD(self, v): + return self.generate(v.thing) + def UNDEF(self, v): + return '!' + def BOOLEAN(self, v): + if v: + return 'true' + else: + return 'false' + def INTEGER(self, v): + return "i%s" % v + def REAL(self, v): + return "r%s" % v + def UUID(self, v): + return "u%s" % v + def BINARY(self, v): + raise LLSDSerializationError("binary notation not yet supported") + def STRING(self, v): + return "'%s'" % v.replace("\\", "\\\\").replace("'", "\\'") + def URI(self, v): + return 'l"%s"' % str(v).replace("\\", "\\\\").replace('"', '\\"') + def DATE(self, v): + second_str = "" + if v.microsecond > 0: + seconds = v.second + float(v.microsecond) / 1000000 + second_str = "%05.2f" % seconds + else: + second_str = "%d" % v.second + return 'd"%s%sZ"' % (v.strftime('%Y-%m-%dT%H:%M:'), second_str) + def ARRAY(self, v): + return "[%s]" % ','.join([self.generate(item) for item in v]) + def MAP(self, v): + return "{%s}" % ','.join(["'%s':%s" % (key.replace("\\", "\\\\").replace("'", "\\'"), self.generate(value)) + for key, value in v.items()]) + + def generate(self, something): + t = type(something) + if self.type_map.has_key(t): + return self.type_map[t](something) + else: + raise LLSDSerializationError("Cannot serialize unknown type: %s (%s)" % ( + t, something)) + + def format(self, something): + return self.generate(something) + +def format_notation(something): + return LLSDNotationFormatter().format(something) + +def _hex_as_nybble(hex): + if (hex >= '0') and (hex <= '9'): + return ord(hex) - ord('0') + elif (hex >= 'a') and (hex <='f'): + return 10 + ord(hex) - ord('a') + elif (hex >= 'A') and (hex <='F'): + return 10 + ord(hex) - ord('A'); + +class LLSDBinaryParser(object): + def __init__(self): + pass + + def parse(self, buffer, ignore_binary = False): + """ + This is the basic public interface for parsing. + + @param buffer the binary data to parse in an indexable sequence. + @param ignore_binary parser throws away data in llsd binary nodes. + @return returns a python object. + """ + self._buffer = buffer + self._index = 0 + self._keep_binary = not ignore_binary + return self._parse() + + def _parse(self): + cc = self._buffer[self._index] + self._index += 1 + if cc == '{': + return self._parse_map() + elif cc == '[': + return self._parse_array() + elif cc == '!': + return None + elif cc == '0': + return False + elif cc == '1': + return True + elif cc == 'i': + # 'i' = integer + idx = self._index + self._index += 4 + return struct.unpack("!i", self._buffer[idx:idx+4])[0] + elif cc == ('r'): + # 'r' = real number + idx = self._index + self._index += 8 + return struct.unpack("!d", self._buffer[idx:idx+8])[0] + elif cc == 'u': + # 'u' = uuid + idx = self._index + self._index += 16 + return lluuid.uuid_bits_to_uuid(self._buffer[idx:idx+16]) + elif cc == 's': + # 's' = string + return self._parse_string() + elif cc in ("'", '"'): + # delimited/escaped string + return self._parse_string_delim(cc) + elif cc == 'l': + # 'l' = uri + return uri(self._parse_string()) + elif cc == ('d'): + # 'd' = date in seconds since epoch + idx = self._index + self._index += 8 + seconds = struct.unpack("!d", self._buffer[idx:idx+8])[0] + return datetime.datetime.fromtimestamp(seconds) + elif cc == 'b': + binary = self._parse_string() + if self._keep_binary: + return binary + # *NOTE: maybe have a binary placeholder which has the + # length. + return None + else: + raise LLSDParseError("invalid binary token at byte %d: %d" % ( + self._index - 1, ord(cc))) + + def _parse_map(self): + rv = {} + size = struct.unpack("!i", self._buffer[self._index:self._index+4])[0] + self._index += 4 + count = 0 + cc = self._buffer[self._index] + self._index += 1 + key = '' + while (cc != '}') and (count < size): + if cc == 'k': + key = self._parse_string() + elif cc in ("'", '"'): + key = self._parse_string_delim(cc) + else: + raise LLSDParseError("invalid map key at byte %d." % ( + self._index - 1,)) + value = self._parse() + #print "kv:",key,value + rv[key] = value + count += 1 + cc = self._buffer[self._index] + self._index += 1 + if cc != '}': + raise LLSDParseError("invalid map close token at byte %d." % ( + self._index,)) + return rv + + def _parse_array(self): + rv = [] + size = struct.unpack("!i", self._buffer[self._index:self._index+4])[0] + self._index += 4 + count = 0 + cc = self._buffer[self._index] + while (cc != ']') and (count < size): + rv.append(self._parse()) + count += 1 + cc = self._buffer[self._index] + if cc != ']': + raise LLSDParseError("invalid array close token at byte %d." % ( + self._index,)) + self._index += 1 + return rv + + def _parse_string(self): + size = struct.unpack("!i", self._buffer[self._index:self._index+4])[0] + self._index += 4 + rv = self._buffer[self._index:self._index+size] + self._index += size + return rv + + def _parse_string_delim(self, delim): + list = [] + found_escape = False + found_hex = False + found_digit = False + byte = 0 + while True: + cc = self._buffer[self._index] + self._index += 1 + if found_escape: + if found_hex: + if found_digit: + found_escape = False + found_hex = False + found_digit = False + byte <<= 4 + byte |= _hex_as_nybble(cc) + list.append(chr(byte)) + byte = 0 + else: + found_digit = True + byte = _hex_as_nybble(cc) + elif cc == 'x': + found_hex = True + else: + if cc == 'a': + list.append('\a') + elif cc == 'b': + list.append('\b') + elif cc == 'f': + list.append('\f') + elif cc == 'n': + list.append('\n') + elif cc == 'r': + list.append('\r') + elif cc == 't': + list.append('\t') + elif cc == 'v': + list.append('\v') + else: + list.append(cc) + found_escape = False + elif cc == '\\': + found_escape = True + elif cc == delim: + break + else: + list.append(cc) + return ''.join(list) + +class LLSDNotationParser(object): + """ Parse LLSD notation: + map: { string:object, string:object } + array: [ object, object, object ] + undef: ! + boolean: true | false | 1 | 0 | T | F | t | f | TRUE | FALSE + integer: i#### + real: r#### + uuid: u#### + string: "g'day" | 'have a "nice" day' | s(size)"raw data" + uri: l"escaped" + date: d"YYYY-MM-DDTHH:MM:SS.FFZ" + binary: b##"ff3120ab1" | b(size)"raw data" """ + def __init__(self): + pass + + def parse(self, buffer, ignore_binary = False): + """ + This is the basic public interface for parsing. + + @param buffer the notation string to parse. + @param ignore_binary parser throws away data in llsd binary nodes. + @return returns a python object. + """ + if buffer == "": + return False + + self._buffer = buffer + self._index = 0 + return self._parse() + + def _parse(self): + cc = self._buffer[self._index] + self._index += 1 + if cc == '{': + return self._parse_map() + elif cc == '[': + return self._parse_array() + elif cc == '!': + return None + elif cc == '0': + return False + elif cc == '1': + return True + elif cc in ('F', 'f'): + self._skip_alpha() + return False + elif cc in ('T', 't'): + self._skip_alpha() + return True + elif cc == 'i': + # 'i' = integer + return self._parse_integer() + elif cc == ('r'): + # 'r' = real number + return self._parse_real() + elif cc == 'u': + # 'u' = uuid + return self._parse_uuid() + elif cc in ("'", '"', 's'): + return self._parse_string(cc) + elif cc == 'l': + # 'l' = uri + delim = self._buffer[self._index] + self._index += 1 + val = uri(self._parse_string(delim)) + if len(val) == 0: + return None + return val + elif cc == ('d'): + # 'd' = date in seconds since epoch + return self._parse_date() + elif cc == 'b': + raise LLSDParseError("binary notation not yet supported") + else: + print cc + raise LLSDParseError("invalid token at index %d: %d" % ( + self._index - 1, ord(cc))) + + def _parse_map(self): + """ map: { string:object, string:object } """ + rv = {} + cc = self._buffer[self._index] + self._index += 1 + key = '' + found_key = False + while (cc != '}'): + if not found_key: + if cc in ("'", '"', 's'): + key = self._parse_string(cc) + found_key = True + #print "key:",key + elif cc.isspace() or cc == ',': + cc = self._buffer[self._index] + self._index += 1 + else: + raise LLSDParseError("invalid map key at byte %d." % ( + self._index - 1,)) + else: + if cc.isspace() or cc == ':': + #print "skipping whitespace '%s'" % cc + cc = self._buffer[self._index] + self._index += 1 + continue + self._index += 1 + value = self._parse() + #print "kv:",key,value + rv[key] = value + found_key = False + cc = self._buffer[self._index] + self._index += 1 + #if cc == '}': + # break + #cc = self._buffer[self._index] + #self._index += 1 + + return rv + + def _parse_array(self): + """ array: [ object, object, object ] """ + rv = [] + cc = self._buffer[self._index] + while (cc != ']'): + if cc.isspace() or cc == ',': + self._index += 1 + cc = self._buffer[self._index] + continue + rv.append(self._parse()) + cc = self._buffer[self._index] + + if cc != ']': + raise LLSDParseError("invalid array close token at index %d." % ( + self._index,)) + self._index += 1 + return rv + + def _parse_uuid(self): + match = re.match(lluuid.UUID.uuid_regex, self._buffer[self._index:]) + if not match: + raise LLSDParseError("invalid uuid token at index %d." % self._index) + + (start, end) = match.span() + start += self._index + end += self._index + self._index = end + return lluuid.UUID(self._buffer[start:end]) + + def _skip_alpha(self): + match = re.match(alpha_regex, self._buffer[self._index:]) + if match: + self._index += match.end() + + def _parse_date(self): + delim = self._buffer[self._index] + self._index += 1 + datestr = self._parse_string(delim) + + if datestr == "": + return datetime.datetime(1970, 1, 1) + + match = re.match(date_regex, datestr) + if not match: + raise LLSDParseError("invalid date string '%s'." % datestr) + + year = int(match.group('year')) + month = int(match.group('month')) + day = int(match.group('day')) + hour = int(match.group('hour')) + minute = int(match.group('minute')) + second = int(match.group('second')) + seconds_float = match.group('second_float') + microsecond = 0 + if seconds_float: + microsecond = int(seconds_float[1:]) * 10000 + return datetime.datetime(year, month, day, hour, minute, second, microsecond) + + def _parse_real(self): + match = re.match(real_regex, self._buffer[self._index:]) + if not match: + raise LLSDParseError("invalid real token at index %d." % self._index) + + (start, end) = match.span() + start += self._index + end += self._index + self._index = end + return float( self._buffer[start:end] ) + + def _parse_integer(self): + match = re.match(int_regex, self._buffer[self._index:]) + if not match: + raise LLSDParseError("invalid integer token at index %d." % self._index) + + (start, end) = match.span() + start += self._index + end += self._index + self._index = end + return int( self._buffer[start:end] ) + + def _parse_string(self, delim): + """ string: "g'day" | 'have a "nice" day' | s(size)"raw data" """ + rv = "" + + if delim in ("'", '"'): + rv = self._parse_string_delim(delim) + elif delim == 's': + rv = self._parse_string_raw() + else: + raise LLSDParseError("invalid string token at index %d." % self._index) + + return rv + + + def _parse_string_delim(self, delim): + """ string: "g'day 'un" | 'have a "nice" day' """ + list = [] + found_escape = False + found_hex = False + found_digit = False + byte = 0 + while True: + cc = self._buffer[self._index] + self._index += 1 + if found_escape: + if found_hex: + if found_digit: + found_escape = False + found_hex = False + found_digit = False + byte <<= 4 + byte |= _hex_as_nybble(cc) + list.append(chr(byte)) + byte = 0 + else: + found_digit = True + byte = _hex_as_nybble(cc) + elif cc == 'x': + found_hex = True + else: + if cc == 'a': + list.append('\a') + elif cc == 'b': + list.append('\b') + elif cc == 'f': + list.append('\f') + elif cc == 'n': + list.append('\n') + elif cc == 'r': + list.append('\r') + elif cc == 't': + list.append('\t') + elif cc == 'v': + list.append('\v') + else: + list.append(cc) + found_escape = False + elif cc == '\\': + found_escape = True + elif cc == delim: + break + else: + list.append(cc) + return ''.join(list) + + def _parse_string_raw(self): + """ string: s(size)"raw data" """ + # Read the (size) portion. + cc = self._buffer[self._index] + self._index += 1 + if cc != '(': + raise LLSDParseError("invalid string token at index %d." % self._index) + + rparen = self._buffer.find(')', self._index) + if rparen == -1: + raise LLSDParseError("invalid string token at index %d." % self._index) + + size = int(self._buffer[self._index:rparen]) + + self._index = rparen + 1 + delim = self._buffer[self._index] + self._index += 1 + if delim not in ("'", '"'): + raise LLSDParseError("invalid string token at index %d." % self._index) + + rv = self._buffer[self._index:(self._index + size)] + self._index += size + cc = self._buffer[self._index] + self._index += 1 + if cc != delim: + raise LLSDParseError("invalid string token at index %d." % self._index) + + return rv + +def format_binary(something): + return '\n' + _format_binary_recurse(something) + +def _format_binary_recurse(something): + if something is None: + return '!' + elif isinstance(something, LLSD): + return _format_binary_recurse(something.thing) + elif isinstance(something, bool): + if something: + return '1' + else: + return '0' + elif isinstance(something, (int, long)): + return 'i' + struct.pack('!i', something) + elif isinstance(something, float): + return 'r' + struct.pack('!d', something) + elif isinstance(something, lluuid.UUID): + return 'u' + something._bits + elif isinstance(something, binary): + return 'b' + struct.pack('!i', len(something)) + something + elif isinstance(something, (str, unicode)): + return 's' + struct.pack('!i', len(something)) + something + elif isinstance(something, uri): + return 'l' + struct.pack('!i', len(something)) + something + elif isinstance(something, datetime.datetime): + seconds_since_epoch = time.mktime(something.timetuple()) + return 'd' + struct.pack('!d', seconds_since_epoch) + elif isinstance(something, (list, tuple)): + array_builder = [] + array_builder.append('[' + struct.pack('!i', len(something))) + for item in something: + array_builder.append(_format_binary_recurse(item)) + array_builder.append(']') + return ''.join(array_builder) + elif isinstance(something, dict): + map_builder = [] + map_builder.append('{' + struct.pack('!i', len(something))) + for key, value in something.items(): + map_builder.append('k' + struct.pack('!i', len(key)) + key) + map_builder.append(_format_binary_recurse(value)) + map_builder.append('}') + return ''.join(map_builder) + else: + raise LLSDSerializationError("Cannot serialize unknown type: %s (%s)" % ( + type(something), something)) + + +def parse(something): + try: + if something.startswith(''): + just_binary = something.split('\n', 1)[1] + return LLSDBinaryParser().parse(just_binary) + # This should be better. + elif something.startswith('<'): + return to_python(fromstring(something)[0]) + else: + return LLSDNotationParser().parse(something) + except KeyError, e: + raise Exception('LLSD could not be parsed: %s' % (e,)) + +class LLSD(object): + def __init__(self, thing=None): + self.thing = thing + + def __str__(self): + return self.toXML(self.thing) + + parse = staticmethod(parse) + toXML = staticmethod(format_xml) + toBinary = staticmethod(format_binary) + toNotation = staticmethod(format_notation) + + +undef = LLSD(None) + +# register converters for stacked, if stacked is available +try: + from mulib import stacked + stacked.NoProducer() # just to exercise stacked +except: + print "Couldn't import mulib.stacked, not registering LLSD converters" +else: + def llsd_convert_json(llsd_stuff, request): + callback = request.get_header('callback') + if callback is not None: + ## See Yahoo's ajax documentation for information about using this + ## callback style of programming + ## http://developer.yahoo.com/common/json.html#callbackparam + req.write("%s(%s)" % (callback, simplejson.dumps(llsd_stuff))) + else: + req.write(simplejson.dumps(llsd_stuff)) + + def llsd_convert_xml(llsd_stuff, request): + request.write(format_xml(llsd_stuff)) + + def llsd_convert_binary(llsd_stuff, request): + request.write(format_binary(llsd_stuff)) + + for typ in [LLSD, dict, list, tuple, str, int, float, bool, unicode, type(None)]: + stacked.add_producer(typ, llsd_convert_json, 'application/json') + + stacked.add_producer(typ, llsd_convert_xml, 'application/llsd+xml') + stacked.add_producer(typ, llsd_convert_xml, 'application/xml') + stacked.add_producer(typ, llsd_convert_xml, 'text/xml') + + stacked.add_producer(typ, llsd_convert_binary, 'application/llsd+binary') + + stacked.add_producer(LLSD, llsd_convert_xml, '*/*') diff --git a/linden/indra/lib/python/indra/base/lluuid.py b/linden/indra/lib/python/indra/base/lluuid.py new file mode 100644 index 0000000..f302f8b --- /dev/null +++ b/linden/indra/lib/python/indra/base/lluuid.py @@ -0,0 +1,290 @@ +"""\ +@file lluuid.py +@brief UUID parser/generator. + +$LicenseInfo:firstyear=2004&license=mit$ + +Copyright (c) 2004-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import md5, random, socket, string, time, re + +def _int2binstr(i,l): + s='' + for a in range(l): + s=chr(i&0xFF)+s + i>>=8 + return s + +def _binstr2int(s): + i = long(0) + for c in s: + i = (i<<8) + ord(c) + return i + +class UUID(object): + """ + A class which represents a 16 byte integer. Stored as a 16 byte 8 + bit character string. + + The string version is to be of the form: + AAAAAAAA-AAAA-BBBB-BBBB-BBBBBBCCCCCC (a 128-bit number in hex) + where A=network address, B=timestamp, C=random. + """ + + NULL_STR = "00000000-0000-0000-0000-000000000000" + + # the UUIDREGEX_STRING is helpful for parsing UUID's in text + hex_wildcard = r"[0-9a-fA-F]" + word = hex_wildcard + r"{4,4}-" + long_word = hex_wildcard + r"{8,8}-" + very_long_word = hex_wildcard + r"{12,12}" + UUID_REGEX_STRING = long_word + word + word + word + very_long_word + uuid_regex = re.compile(UUID_REGEX_STRING) + + rand = random.Random() + ip = '' + try: + ip = socket.gethostbyname(socket.gethostname()) + except(socket.gaierror): + # no ip address, so just default to somewhere in 10.x.x.x + ip = '10' + for i in range(3): + ip += '.' + str(rand.randrange(1,254)) + hexip = ''.join(["%04x" % long(i) for i in ip.split('.')]) + lastid = '' + + def __init__(self, string_with_uuid=None): + """ + Initialize to first valid UUID in string argument, + or to null UUID if none found or string is not supplied. + """ + self._bits = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + if string_with_uuid: + uuid_match = UUID.uuid_regex.search(string_with_uuid) + if uuid_match: + uuid_string = uuid_match.group() + s = string.replace(uuid_string, '-', '') + self._bits = _int2binstr(string.atol(s[:8],16),4) + \ + _int2binstr(string.atol(s[8:16],16),4) + \ + _int2binstr(string.atol(s[16:24],16),4) + \ + _int2binstr(string.atol(s[24:],16),4) + + def __len__(self): + """ + Used by the len() builtin. + """ + return 36 + + def __nonzero__(self): + return self._bits != "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + + def __str__(self): + uuid_string = self.toString() + return uuid_string + + __repr__ = __str__ + + def __getitem__(self, index): + return str(self)[index] + + def __eq__(self, other): + if isinstance(other, (str, unicode)): + return other == str(self) + return self._bits == getattr(other, '_bits', '') + + def __ne__(self, other): + return not self.__eq__(other) + + def __le__(self, other): + return self._bits <= other._bits + + def __ge__(self, other): + return self._bits >= other._bits + + def __lt__(self, other): + return self._bits < other._bits + + def __gt__(self, other): + return self._bits > other._bits + + def __hash__(self): + return hash(self._bits) + + def set(self, uuid): + self._bits = uuid._bits + + def setFromString(self, uuid_string): + """ + Given a string version of a uuid, set self bits + appropriately. Returns self. + """ + s = string.replace(uuid_string, '-', '') + self._bits = _int2binstr(string.atol(s[:8],16),4) + \ + _int2binstr(string.atol(s[8:16],16),4) + \ + _int2binstr(string.atol(s[16:24],16),4) + \ + _int2binstr(string.atol(s[24:],16),4) + return self + + def setFromMemoryDump(self, gdb_string): + """ + We expect to get gdb_string as four hex units. eg: + 0x147d54db 0xc34b3f1b 0x714f989b 0x0a892fd2 + Which will be translated to: + db547d14-1b3f4bc3-9b984f71-d22f890a + Returns self. + """ + s = string.replace(gdb_string, '0x', '') + s = string.replace(s, ' ', '') + t = '' + for i in range(8,40,8): + for j in range(0,8,2): + t = t + s[i-j-2:i-j] + self.setFromString(t) + + def toString(self): + """ + Return as a string matching the LL standard + AAAAAAAA-AAAA-BBBB-BBBB-BBBBBBCCCCCC (a 128-bit number in hex) + where A=network address, B=timestamp, C=random. + """ + return uuid_bits_to_string(self._bits) + + def getAsString(self): + """ + Return a different string representation of the form + AAAAAAAA-AAAABBBB-BBBBBBBB-BBCCCCCC (a 128-bit number in hex) + where A=network address, B=timestamp, C=random. + """ + i1 = _binstr2int(self._bits[0:4]) + i2 = _binstr2int(self._bits[4:8]) + i3 = _binstr2int(self._bits[8:12]) + i4 = _binstr2int(self._bits[12:16]) + return '%08lx-%08lx-%08lx-%08lx' % (i1,i2,i3,i4) + + def generate(self): + """ + Generate a new uuid. This algorithm is slightly different + from c++ implementation for portability reasons. + Returns self. + """ + newid = self.__class__.lastid + while newid == self.__class__.lastid: + now = long(time.time() * 1000) + newid = ("%016x" % now) + self.__class__.hexip + newid += ("%03x" % (self.__class__.rand.randrange(0,4095))) + self.__class__.lastid = newid + m = md5.new() + m.update(newid) + self._bits = m.digest() + return self + + def isNull(self): + """ + Returns 1 if the uuid is null - ie, equal to default uuid. + """ + return (self._bits == "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0") + + def xor(self, rhs): + """ + xors self with rhs. + """ + v1 = _binstr2int(self._bits[0:4]) ^ _binstr2int(rhs._bits[0:4]) + v2 = _binstr2int(self._bits[4:8]) ^ _binstr2int(rhs._bits[4:8]) + v3 = _binstr2int(self._bits[8:12]) ^ _binstr2int(rhs._bits[8:12]) + v4 = _binstr2int(self._bits[12:16]) ^ _binstr2int(rhs._bits[12:16]) + self._bits = _int2binstr(v1,4) + \ + _int2binstr(v2,4) + \ + _int2binstr(v3,4) + \ + _int2binstr(v4,4) + +def printTranslatedMemory(four_hex_uints): + """ + We expect to get the string as four hex units. eg: + 0x147d54db 0xc34b3f1b 0x714f989b 0x0a892fd2 + Which will be translated to: + db547d14-1b3f4bc3-9b984f71-d22f890a + """ + uuid = UUID() + uuid.setFromMemoryDump(four_hex_uints) + print uuid.toString() + +def isPossiblyID(id_str): + """ + This function returns 1 if the string passed has some uuid-like + characteristics. Otherwise returns 0. + """ + if not id_str or len(id_str) < 5 or len(id_str) > 36: + return 0 + + if isinstance(id_str, UUID) or UUID.uuid_regex.match(id_str): + return 1 + # build a string which matches every character. + hex_wildcard = r"[0-9a-fA-F]" + chars = len(id_str) + next = min(chars, 8) + matcher = hex_wildcard+"{"+str(next)+","+str(next)+"}" + chars = chars - next + if chars > 0: + matcher = matcher + "-" + chars = chars - 1 + for block in range(3): + next = max(min(chars, 4), 0) + if next: + matcher = matcher + hex_wildcard+"{"+str(next)+","+str(next)+"}" + chars = chars - next + if chars > 0: + matcher = matcher + "-" + chars = chars - 1 + if chars > 0: + next = min(chars, 12) + matcher = matcher + hex_wildcard+"{"+str(next)+","+str(next)+"}" + #print matcher + uuid_matcher = re.compile(matcher) + if uuid_matcher.match(id_str): + return 1 + return 0 + +def uuid_bits_to_string(bits): + i1 = _binstr2int(bits[0:4]) + i2 = _binstr2int(bits[4:6]) + i3 = _binstr2int(bits[6:8]) + i4 = _binstr2int(bits[8:10]) + i5 = _binstr2int(bits[10:12]) + i6 = _binstr2int(bits[12:16]) + return '%08lx-%04lx-%04lx-%04lx-%04lx%08lx' % (i1,i2,i3,i4,i5,i6) + +def uuid_bits_to_uuid(bits): + return UUID(uuid_bits_to_string(bits)) + + +try: + from mulib import stacked + stacked.NoProducer() # just to exercise stacked +except: + print "Couldn't import mulib.stacked, not registering UUID converter" +else: + def convertUUID(uuid, req): + req.write(str(uuid)) + + stacked.add_producer(UUID, convertUUID, "*/*") + stacked.add_producer(UUID, convertUUID, "text/html") diff --git a/linden/indra/lib/python/indra/ipc/__init__.py b/linden/indra/lib/python/indra/ipc/__init__.py index 92c9416..4395361 100644 --- a/linden/indra/lib/python/indra/ipc/__init__.py +++ b/linden/indra/lib/python/indra/ipc/__init__.py @@ -2,26 +2,26 @@ @file __init__.py @brief Initialization file for the indra ipc module. +$LicenseInfo:firstyear=2006&license=mit$ + Copyright (c) 2006-2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ diff --git a/linden/indra/lib/python/indra/ipc/compatibility.py b/linden/indra/lib/python/indra/ipc/compatibility.py index 358820c..8435528 100644 --- a/linden/indra/lib/python/indra/ipc/compatibility.py +++ b/linden/indra/lib/python/indra/ipc/compatibility.py @@ -2,28 +2,28 @@ @file compatibility.py @brief Classes that manage compatibility states. +$LicenseInfo:firstyear=2007&license=mit$ + Copyright (c) 2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ diff --git a/linden/indra/lib/python/indra/ipc/httputil.py b/linden/indra/lib/python/indra/ipc/httputil.py new file mode 100644 index 0000000..c4ac0a3 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/httputil.py @@ -0,0 +1,9 @@ + +import warnings + +warnings.warn("indra.ipc.httputil has been deprecated; use eventlet.httpc instead", DeprecationWarning, 2) + +from eventlet.httpc import * + + +makeConnection = make_connection diff --git a/linden/indra/lib/python/indra/ipc/llmessage.py b/linden/indra/lib/python/indra/ipc/llmessage.py index de6fd3b..2497393 100644 --- a/linden/indra/lib/python/indra/ipc/llmessage.py +++ b/linden/indra/lib/python/indra/ipc/llmessage.py @@ -2,28 +2,28 @@ @file llmessage.py @brief Message template parsing and compatiblity +$LicenseInfo:firstyear=2007&license=mit$ + Copyright (c) 2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ from sets import Set, ImmutableSet diff --git a/linden/indra/lib/python/indra/ipc/llsdhttp.py b/linden/indra/lib/python/indra/ipc/llsdhttp.py new file mode 100644 index 0000000..0d1a1c6 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/llsdhttp.py @@ -0,0 +1,84 @@ +"""\ +@file llsdhttp.py +@brief Functions to ease moving llsd over http + +$LicenseInfo:firstyear=2006&license=mit$ + +Copyright (c) 2006-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import os.path +import os +import urlparse + +from indra.base import llsd + +from eventlet import httpc + + +get, put, delete, post = httpc.make_suite( + llsd.format_xml, llsd.parse, 'application/xml+llsd') + + +for x in (httpc.ConnectionError, httpc.NotFound, httpc.Forbidden): + globals()[x.__name__] = x + + +def postFile(url, filename, verbose=False): + f = open(filename) + body = f.read() + f.close() + llsd_body = llsd.parse(body) + return post(url, llsd_body, verbose=verbose) + + +def getStatus(url, use_proxy=False): + status, _headers, _body = get(url, use_proxy=use_proxy, verbose=True) + return status + + +def putStatus(url, data): + status, _headers, _body = put(url, data, verbose=True) + return status + + +def deleteStatus(url): + status, _headers, _body = delete(url, verbose=True) + return status + + +def postStatus(url, data): + status, _headers, _body = post(url, data, verbose=True) + return status + + +def postFileStatus(url, filename): + status, _headers, body = postFile(url, filename, verbose=True) + return status, body + + +def getFromSimulator(path, use_proxy=False): + return get('http://' + simulatorHostAndPort + path, use_proxy=use_proxy) + + +def postToSimulator(path, data=None): + return post('http://' + simulatorHostAndPort + path, data) diff --git a/linden/indra/lib/python/indra/ipc/mysql_pool.py b/linden/indra/lib/python/indra/ipc/mysql_pool.py new file mode 100644 index 0000000..4a265a1 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/mysql_pool.py @@ -0,0 +1,103 @@ +"""\ +@file mysql_pool.py +@brief Uses saranwrap to implement a pool of nonblocking database connections to a mysql server. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import os + +from eventlet.pools import Pool +from eventlet.processes import DeadProcess +from indra.ipc import saranwrap + +import MySQLdb + +# method 2: better -- admits the existence of the pool +# dbp = my_db_connector.get() +# dbh = dbp.get() +# dbc = dbh.cursor() +# dbc.execute(named_query) +# dbc.close() +# dbp.put(dbh) + +class DatabaseConnector(object): + """\ +@brief This is an object which will maintain a collection of database +connection pools keyed on host,databasename""" + def __init__(self, credentials, min_size = 0, max_size = 4, *args, **kwargs): + """\ + @brief constructor + @param min_size the minimum size of a child pool. + @param max_size the maximum size of a child pool.""" + self._min_size = min_size + self._max_size = max_size + self._args = args + self._kwargs = kwargs + self._credentials = credentials # this is a map of hostname to username/password + self._databases = {} + + def credentials_for(self, host): + if host in self._credentials: + return self._credentials[host] + else: + return self._credentials.get('default', None) + + def get(self, host, dbname): + key = (host, dbname) + if key not in self._databases: + new_kwargs = self._kwargs.copy() + new_kwargs['db'] = dbname + new_kwargs['host'] = host + new_kwargs.update(self.credentials_for(host)) + dbpool = ConnectionPool(self._min_size, self._max_size, *self._args, **new_kwargs) + self._databases[key] = dbpool + + return self._databases[key] + + +class ConnectionPool(Pool): + """A pool which gives out saranwrapped MySQLdb connections from a pool + """ + def __init__(self, min_size = 0, max_size = 4, *args, **kwargs): + self._args = args + self._kwargs = kwargs + Pool.__init__(self, min_size, max_size) + + def create(self): + return saranwrap.wrap(MySQLdb).connect(*self._args, **self._kwargs) + + def put(self, conn): + # rollback any uncommitted changes, so that the next process + # has a clean slate. This also pokes the process to see if + # it's dead or None + try: + conn.rollback() + except (AttributeError, DeadProcess), e: + conn = self.create() + # TODO figure out if we're still connected to the database + if conn: + Pool.put(self, conn) + else: + self.current_size -= 1 diff --git a/linden/indra/lib/python/indra/ipc/russ.py b/linden/indra/lib/python/indra/ipc/russ.py new file mode 100644 index 0000000..1ef5562 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/russ.py @@ -0,0 +1,157 @@ +"""\ +@file russ.py +@brief Recursive URL Substitution Syntax helpers +@author Phoenix + +Many details on how this should work is available on the wiki: +https://wiki.secondlife.com/wiki/Recursive_URL_Substitution_Syntax + +Adding features to this should be reflected in that page in the +implementations section. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import urllib +from indra.ipc import llsdhttp + +class UnbalancedBraces(Exception): + pass + +class UnknownDirective(Exception): + pass + +class BadDirective(Exception): + pass + +def format_value_for_path(value): + if type(value) in [list, tuple]: + # *NOTE: treat lists as unquoted path components so that the quoting + # doesn't get out-of-hand. This is a workaround for the fact that + # russ always quotes, even if the data it's given is already quoted, + # and it's not safe to simply unquote a path directly, so if we want + # russ to substitute urls parts inside other url parts we always + # have to do so via lists of unquoted path components. + return '/'.join([urllib.quote(str(item)) for item in value]) + else: + return urllib.quote(str(value)) + +def format(format_str, context): + """@brief Format format string according to rules for RUSS. +@see https://osiris.lindenlab.com/mediawiki/index.php/Recursive_URL_Substitution_Syntax +@param format_str The input string to format. +@param context A map used for string substitutions. +@return Returns the formatted string. If no match, the braces remain intact. +""" + while True: + #print "format_str:", format_str + all_matches = _find_sub_matches(format_str) + if not all_matches: + break + substitutions = 0 + while True: + matches = all_matches.pop() + # we work from right to left to make sure we do not + # invalidate positions earlier in format_str + matches.reverse() + for pos in matches: + # Use index since _find_sub_matches should have raised + # an exception, and failure to find now is an exception. + end = format_str.index('}', pos) + #print "directive:", format_str[pos+1:pos+5] + if format_str[pos + 1] == '$': + value = context[format_str[pos + 2:end]] + if value is not None: + value = format_value_for_path(value) + elif format_str[pos + 1] == '%': + value = _build_query_string( + context.get(format_str[pos + 2:end])) + elif format_str[pos+1:pos+5] == 'http' or format_str[pos+1:pos+5] == 'file': + value = _fetch_url_directive(format_str[pos + 1:end]) + else: + raise UnknownDirective, format_str[pos:end + 1] + if value is not None: + format_str = format_str[:pos]+str(value)+format_str[end+1:] + substitutions += 1 + + # If there were any substitutions at this depth, re-parse + # since this may have revealed new things to substitute + if substitutions: + break + if not all_matches: + break + + # If there were no substitutions at all, and we have exhausted + # the possible matches, bail. + if not substitutions: + break + return format_str + +def _find_sub_matches(format_str): + """@brief Find all of the substitution matches. +@param format_str the RUSS conformant format string. +@return Returns an array of depths of arrays of positional matches in input. +""" + depth = 0 + matches = [] + for pos in range(len(format_str)): + if format_str[pos] == '{': + depth += 1 + if not len(matches) == depth: + matches.append([]) + matches[depth - 1].append(pos) + continue + if format_str[pos] == '}': + depth -= 1 + continue + if not depth == 0: + raise UnbalancedBraces, format_str + return matches + +def _build_query_string(query_dict): + """\ + @breif given a dict, return a query string. utility wrapper for urllib. + @param query_dict input query dict + @returns Returns an urlencoded query string including leading '?'. + """ + if query_dict: + return '?' + urllib.urlencode(query_dict) + else: + return '' + +def _fetch_url_directive(directive): + "*FIX: This only supports GET" + commands = directive.split('|') + resource = llsdhttp.get(commands[0]) + if len(commands) == 3: + resource = _walk_resource(resource, commands[2]) + return resource + +def _walk_resource(resource, path): + path = path.split('/') + for child in path: + if not child: + continue + resource = resource[child] + return resource diff --git a/linden/indra/lib/python/indra/ipc/saranwrap.py b/linden/indra/lib/python/indra/ipc/saranwrap.py new file mode 100644 index 0000000..31705c4 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/saranwrap.py @@ -0,0 +1,637 @@ +"""\ +@file saranwrap.py +@author Phoenix +@date 2007-07-13 +@brief A simple, pickle based rpc mechanism which reflects python +objects and callables. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ + +This file provides classes and exceptions used for simple python level +remote procedure calls. This is achieved by intercepting the basic +getattr and setattr calls in a client proxy, which commnicates those +down to the server which will dispatch them to objects in it's process +space. + +The basic protocol to get and set attributes is for the client proxy +to issue the command: + +getattr $id $name +setattr $id $name $value + +getitem $id $item +setitem $id $item $value +eq $id $rhs +del $id + +When the get returns a callable, the client proxy will provide a +callable proxy which will invoke a remote procedure call. The command +issued from the callable proxy to server is: + +call $id $name $args $kwargs + +If the client supplies an id of None, then the get/set/call is applied +to the object(s) exported from the server. + +The server will parse the get/set/call, take the action indicated, and +return back to the caller one of: + +value $val +callable +object $id +exception $excp + +To handle object expiration, the proxy will instruct the rpc server to +discard objects which are no longer in use. This is handled by +catching proxy deletion and sending the command: + +del $id + +The server will handle this by removing clearing it's own internal +references. This does not mean that the object will necessarily be +cleaned from the server, but no artificial references will remain +after successfully completing. On completion, the server will return +one of: + +value None +exception $excp + +The server also accepts a special command for debugging purposes: + +status + +Which will be intercepted by the server to write back: + +status {...} + +The wire protocol is to pickle the Request class in this file. The +request class is basically an action and a map of parameters' +""" + +import os +import cPickle +import struct +import sys + +try: + set = set + frozenset = frozenset +except NameError: + from sets import Set as set, ImmutableSet as frozenset + +from eventlet.processes import Process +from eventlet import api + +# +# debugging hooks +# +_g_debug_mode = False +if _g_debug_mode: + import traceback + + +def wrap(obj, dead_callback = None): + """ +@brief wrap in object in another process through a saranwrap proxy +@param object The object to wrap. +@param dead_callback A callable to invoke if the process exits.""" + + if type(obj).__name__ == 'module': + return wrap_module(obj.__name__, dead_callback) + p = Process('python', [__file__, '--child'], dead_callback) + prox = Proxy(p, p) + prox.obj = obj + return prox.obj + +def wrap_module(fqname, dead_callback = None): + """ +@brief wrap a module in another process through a saranwrap proxy +@param fqname The fully qualified name of the module. +@param dead_callback A callable to invoke if the process exits.""" + global _g_debug_mode + if _g_debug_mode: + p = Process('python', [__file__, '--module', fqname, '--logfile', '/tmp/saranwrap.log'], dead_callback) + else: + p = Process('python', [__file__, '--module', fqname,], dead_callback) + prox = Proxy(p, p) + return prox + +def status(proxy): + """ +@brief get the status from the server through a proxy +@param proxy a saranwrap.Proxy object connected to a server.""" + _write_request(Request('status', {}), proxy.__local_dict['_out']) + return _read_response(None, None, proxy.__local_dict['_in'], proxy.__local_dict['_out'], None) + +class BadResponse(Exception): + """"This exception is raised by an saranwrap client when it could + parse but cannot understand the response from the server.""" + pass + +class BadRequest(Exception): + """"This exception is raised by a saranwrap server when it could parse + but cannot understand the response from the server.""" + pass + +class UnrecoverableError(Exception): + pass + +class Request(object): + "@brief A wrapper class for proxy requests to the server." + def __init__(self, action, param): + self._action = action + self._param = param + def __str__(self): + return "Request `"+self._action+"` "+str(self._param) + def __getitem__(self, name): + return self._param[name] + def action(self): + return self._action + +def _read_lp_hunk(stream): + len_bytes = stream.read(4) + length = struct.unpack('I', len_bytes)[0] + body = stream.read(length) + return body + +def _read_response(id, attribute, input, output, dead_list): + """@brief local helper method to read respones from the rpc server.""" + try: + str = _read_lp_hunk(input) + _prnt(`str`) + response = cPickle.loads(str) + except AttributeError, e: + raise UnrecoverableError(e) + _prnt("response: %s" % response) + if response[0] == 'value': + return response[1] + elif response[0] == 'callable': + return CallableProxy(id, attribute, input, output, dead_list) + elif response[0] == 'object': + return ObjectProxy(input, output, response[1], dead_list) + elif response[0] == 'exception': + exp = response[1] + raise exp + else: + raise BadResponse(response[0]) + +def _write_lp_hunk(stream, hunk): + write_length = struct.pack('I', len(hunk)) + stream.write(write_length + hunk) + if hasattr(stream, 'flush'): + stream.flush() + +def _write_request(param, output): + _prnt("request: %s" % param) + str = cPickle.dumps(param) + _write_lp_hunk(output, str) + +def _is_local(attribute): + "Return true if the attribute should be handled locally" +# return attribute in ('_in', '_out', '_id', '__getattribute__', '__setattr__', '__dict__') + # good enough for now. :) + if '__local_dict' in attribute: + return True + return False + +def _prnt(message): + global _g_debug_mode + if _g_debug_mode: + print message + +_g_logfile = None +def _log(message): + global _g_logfile + if _g_logfile: + _g_logfile.write(str(os.getpid()) + ' ' + message) + _g_logfile.write('\n') + _g_logfile.flush() + +def _unmunge_attr_name(name): + """ Sometimes attribute names come in with classname prepended, not sure why. + This function removes said classname, because we're huge hackers and we didn't + find out what the true right thing to do is. *FIX: find out. """ + if(name.startswith('_Proxy')): + name = name[len('_Proxy'):] + if(name.startswith('_ObjectProxy')): + name = name[len('_ObjectProxy'):] + return name + + +class Proxy(object): + """\ +@class Proxy +@brief This class wraps a remote python process, presumably available +in an instance of an Server. + +This is the class you will typically use as a client to a child +process. Simply instantiate one around a file-like interface and start +calling methods on the thing that is exported. The dir() builtin is +not supported, so you have to know what has been exported. +""" + def __init__(self, input, output, dead_list = None): + """\ +@param input a file-like object which supports read(). +@param output a file-like object which supports write() and flush(). +@param id an identifier for the remote object. humans do not provide this. +""" + # default dead_list inside the function because all objects in method + # argument lists are init-ed only once globally + if dead_list is None: + dead_list = set() + #_prnt("Proxy::__init__") + self.__local_dict = dict( + _in = input, + _out = output, + _dead_list = dead_list, + _id = None) + + def __getattribute__(self, attribute): + #_prnt("Proxy::__getattr__: %s" % attribute) + if _is_local(attribute): + # call base class getattribute so we actually get the local variable + attribute = _unmunge_attr_name(attribute) + return super(Proxy, self).__getattribute__(attribute) + else: + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + _dead_list = self.__local_dict['_dead_list'] + for dead_object in _dead_list: + request = Request('del', {'id':dead_object}) + _write_request(request, my_out) + response = _read_response(my_id, attribute, my_in, my_out, _dead_list) + _dead_list.clear() + + # Pass all public attributes across to find out if it is + # callable or a simple attribute. + request = Request('getattr', {'id':my_id, 'attribute':attribute}) + _write_request(request, my_out) + return _read_response(my_id, attribute, my_in, my_out, _dead_list) + + def __setattr__(self, attribute, value): + #_prnt("Proxy::__setattr__: %s" % attribute) + if _is_local(attribute): + # It must be local to this actual object, so we have to apply + # it to the dict in a roundabout way + attribute = _unmunge_attr_name(attribute) + super(Proxy, self).__getattribute__('__dict__')[attribute]=value + else: + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + _dead_list = self.__local_dict['_dead_list'] + # Pass the set attribute across + request = Request('setattr', {'id':my_id, 'attribute':attribute, 'value':value}) + _write_request(request, my_out) + return _read_response(my_id, attribute, my_in, my_out, _dead_list) + +class ObjectProxy(Proxy): + """\ +@class ObjectProxy +@brief This class wraps a remote object in the Server + +This class will be created during normal operation, and users should +not need to deal with this class directly.""" + + def __init__(self, input, output, id, dead_list): + """\ +@param input a file-like object which supports read(). +@param output a file-like object which supports write() and flush(). +@param id an identifier for the remote object. humans do not provide this. +""" + Proxy.__init__(self, input, output, dead_list) + self.__local_dict['_id'] = id + #_prnt("ObjectProxy::__init__ %s" % self._id) + + def __del__(self): + my_id = self.__local_dict['_id'] + #_prnt"ObjectProxy::__del__ %s" % my_id + self.__local_dict['_dead_list'].add(my_id) + + def __getitem__(self, key): + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + _dead_list = self.__local_dict['_dead_list'] + request = Request('getitem', {'id':my_id, 'key':key}) + _write_request(request, my_out) + return _read_response(my_id, key, my_in, my_out, _dead_list) + + def __setitem__(self, key, value): + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + _dead_list = self.__local_dict['_dead_list'] + request = Request('setitem', {'id':my_id, 'key':key, 'value':value}) + _write_request(request, my_out) + return _read_response(my_id, key, my_in, my_out, _dead_list) + + def __eq__(self, rhs): + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + _dead_list = self.__local_dict['_dead_list'] + request = Request('eq', {'id':my_id, 'rhs':rhs.__local_dict['_id']}) + _write_request(request, my_out) + return _read_response(my_id, None, my_in, my_out, _dead_list) + + def __repr__(self): + # apparently repr(obj) skips the whole getattribute thing and just calls __repr__ + # directly. Therefore we just pass it through the normal call pipeline, and + # tack on a little header so that you can tell it's an object proxy. + val = self.__repr__() + return "saran:%s" % val + + def __str__(self): + # see description for __repr__, because str(obj) works the same. We don't + # tack anything on to the return value here because str values are used as data. + return self.__str__() + +def proxied_type(self): + if type(self) is not ObjectProxy: + return type(self) + + my_in = self.__local_dict['_in'] + my_out = self.__local_dict['_out'] + my_id = self.__local_dict['_id'] + request = Request('type', {'id':my_id}) + _write_request(request, my_out) + # dead list can be none because we know the result will always be + # a value and not an ObjectProxy itself + return _read_response(my_id, None, my_in, my_out, None) + +class CallableProxy(object): + """\ +@class CallableProxy +@brief This class wraps a remote function in the Server + +This class will be created by an Proxy during normal operation, +and users should not need to deal with this class directly.""" + + def __init__(self, object_id, name, input, output, dead_list): + #_prnt("CallableProxy::__init__: %s, %s" % (object_id, name)) + self._object_id = object_id + self._name = name + self._in = input + self._out = output + self._dead_list = dead_list + + def __call__(self, *args, **kwargs): + #_prnt("CallableProxy::__call__: %s, %s" % (args, kwargs)) + + # Pass the call across. We never build a callable without + # having already checked if the method starts with '_' so we + # can safely pass this one to the remote object. + #_prnt("calling %s %s" % (self._object_id, self._name) + request = Request('call', {'id':self._object_id, 'name':self._name, 'args':args, 'kwargs':kwargs}) + _write_request(request, self._out) + return _read_response(self._object_id, self._name, self._in, self._out, self._dead_list) + +class Server(object): + def __init__(self, input, output, export): + """\ +@param input a file-like object which supports read(). +@param output a file-like object which supports write() and flush(). +@param export an object, function, or map which is exported to clients +when the id is None.""" + #_log("Server::__init__") + self._in = input + self._out = output + self._export = export + self._next_id = 1 + self._objects = {} + + def handle_status(self, object, req): + return { + 'object_count':len(self._objects), + 'next_id':self._next_id, + 'pid':os.getpid()} + + def handle_getattr(self, object, req): + try: + return getattr(object, req['attribute']) + except AttributeError, e: + if hasattr(object, "__getitem__"): + return object[req['attribute']] + else: + raise e + #_log('getattr: %s' % str(response)) + + def handle_setattr(self, object, req): + try: + return setattr(object, req['attribute'], req['value']) + except AttributeError, e: + if hasattr(object, "__setitem__"): + return object.__setitem__(req['attribute'], req['value']) + else: + raise e + + def handle_getitem(self, object, req): + return object[req['key']] + + def handle_setitem(self, object, req): + object[req['key']] = req['value'] + return None # *TODO figure out what the actual return value of __setitem__ should be + + def handle_eq(self, object, req): + #_log("__eq__ %s %s" % (object, req)) + rhs = None + try: + rhs = self._objects[req['rhs']] + except KeyError, e: + return False + return (object == rhs) + + def handle_call(self, object, req): + #_log("calling %s " % (req['name'])) + try: + fn = getattr(object, req['name']) + except AttributeError, e: + if hasattr(object, "__setitem__"): + fn = object[req['name']] + else: + raise e + + return fn(*req['args'],**req['kwargs']) + + def handle_del(self, object, req): + id = req['id'] + _log("del %s from %s" % (id, self._objects)) + + # *TODO what does __del__ actually return? + del self._objects[id] + return None + + def handle_type(self, object, req): + return type(object) + + def loop(self): + """@brief Loop forever and respond to all requests.""" + _log("Server::loop") + while True: + try: + try: + str = _read_lp_hunk(self._in) + except EOFError: + sys.exit(0) # normal exit + request = cPickle.loads(str) + _log("request: %s (%s)" % (request, self._objects)) + req = request + id = None + object = None + try: + id = req['id'] + if id: + id = int(id) + object = self._objects[id] + #_log("id, object: %d %s" % (id, object)) + except Exception, e: + #_log("Exception %s" % str(e)) + pass + if object is None or id is None: + id = None + object = self._export + #_log("found object %s" % str(object)) + + # Handle the request via a method with a special name on the server + handler_name = 'handle_%s' % request.action() + + try: + handler = getattr(self, handler_name) + except AttributeError: + raise BadRequest, request.action() + + response = handler(object, request) + + # figure out what to do with the response, and respond + # apprpriately. + if request.action() in ['status', 'type']: + # have to handle these specially since we want to + # pickle up the actual value and not return a proxy + self.respond(['value', response]) + elif callable(response): + #_log("callable %s" % response) + self.respond(['callable']) + elif self.is_value(response): + self.respond(['value', response]) + else: + self._objects[self._next_id] = response + #_log("objects: %s" % str(self._objects)) + self.respond(['object', self._next_id]) + self._next_id += 1 + except SystemExit, e: + raise e + except Exception, e: + self.write_exception(e) + except: + self.write_exception(sys.exc_info()[0]) + + def is_value(self, value): + """\ +@brief Test if value should be serialized as a simple dataset. +@param value The value to test. +@return Returns true if value is a simple serializeable set of data. +""" + return type(value) in (str,int,float,long,bool,type(None)) + + def respond(self, body): + _log("responding with: %s" % body) + #_log("objects: %s" % self._objects) + s = cPickle.dumps(body) + _log(`s`) + str = _write_lp_hunk(self._out, s) + + def write_exception(self, e): + """@brief Helper method to respond with an exception.""" + #_log("exception: %s" % sys.exc_info()[0]) + # TODO: serialize traceback using generalization of code from mulib.htmlexception + self.respond(['exception', e]) + global _g_debug_mode + if _g_debug_mode: + _log("traceback: %s" % traceback.format_tb(sys.exc_info()[2])) + + +# test function used for testing that final except clause +def raise_a_weird_error(): + raise "oh noes you can raise a string" + +# test function used for testing return of unpicklable exceptions +def raise_an_unpicklable_error(): + class Unpicklable(Exception): + pass + raise Unpicklable() + +# test function used for testing return of picklable exceptions +def raise_standard_error(): + raise FloatingPointError() + +# test function to make sure print doesn't break the wrapper +def print_string(str): + print str + +# test function to make sure printing on stdout doesn't break the +# wrapper +def err_string(str): + print >>sys.stderr, str + +def main(): + import optparse + parser = optparse.OptionParser( + usage="usage: %prog [options]", + description="Simple saranwrap.Server wrapper") + parser.add_option( + '-c', '--child', default=False, action='store_true', + help='Wrap an object serialed via setattr.') + parser.add_option( + '-m', '--module', type='string', dest='module', default=None, + help='a module to load and export.') + parser.add_option( + '-l', '--logfile', type='string', dest='logfile', default=None, + help='file to log to.') + options, args = parser.parse_args() + global _g_logfile + if options.logfile: + _g_logfile = open(options.logfile, 'a') + if options.module: + export = api.named(options.module) + server = Server(sys.stdin, sys.stdout, export) + elif options.child: + server = Server(sys.stdin, sys.stdout, {}) + + # *HACK: some modules may emit on stderr, which breaks everything. + class NullSTDOut(object): + def write(a, b): + pass + sys.stderr = NullSTDOut() + sys.stdout = NullSTDOut() + + # Loop until EOF + server.loop() + if _g_logfile: + _g_logfile.close() + + +if __name__ == "__main__": + main() diff --git a/linden/indra/lib/python/indra/ipc/servicebuilder.py b/linden/indra/lib/python/indra/ipc/servicebuilder.py new file mode 100644 index 0000000..ebd2583 --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/servicebuilder.py @@ -0,0 +1,93 @@ +"""\ +@file servicebuilder.py +@author Phoenix +@brief Class which will generate service urls. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +from indra.base import config +from indra.ipc import llsdhttp +from indra.ipc import russ + +# *NOTE: agent presence relies on this variable existing and being current, it is a huge hack +services_config = {} +try: + services_config = llsdhttp.get(config.get('services-config')) +except: + pass + +_g_builder = None +def build(name, context={}, **kwargs): + """ Convenience method for using a global, singleton, service builder. Pass arguments either via a dict or via python keyword arguments, or both! + + Example use: + > context = {'channel':'Second Life Release', 'version':'1.18.2.0'} + > servicebuilder.build('version-manager-version', context) + 'http://int.util.vaak.lindenlab.com/channel/Second%20Life%20Release/1.18.2.0' + > servicebuilder.build('version-manager-version', channel='Second Life Release', version='1.18.2.0') + 'http://int.util.vaak.lindenlab.com/channel/Second%20Life%20Release/1.18.2.0' + > servicebuilder.build('version-manager-version', context, version='1.18.1.2') + 'http://int.util.vaak.lindenlab.com/channel/Second%20Life%20Release/1.18.1.2' + """ + context = context.copy() # shouldn't modify the caller's dictionary + context.update(kwargs) + global _g_builder + if _g_builder is None: + _g_builder = ServiceBuilder() + return _g_builder.buildServiceURL(name, context) + +class ServiceBuilder(object): + def __init__(self, services_definition = services_config): + """\ + @brief + @brief Create a ServiceBuilder. + @param services_definition Complete services definition, services.xml. + """ + # no need to keep a copy of the services section of the + # complete services definition, but it doesn't hurt much. + self.services = services_definition['services'] + self.builders = {} + for service in self.services: + service_builder = service.get('service-builder') + if not service_builder: + continue + if isinstance(service_builder, dict): + # We will be constructing several builders + for name, builder in service_builder.items(): + full_builder_name = service['name'] + '-' + name + self.builders[full_builder_name] = builder + else: + self.builders[service['name']] = service_builder + + def buildServiceURL(self, name, context): + """\ + @brief given the environment on construction, return a service URL. + @param name The name of the service. + @param context A dict of name value lookups for the service. + @returns Returns the + """ + base_url = config.get('services-base-url') + svc_path = russ.format(self.builders[name], context) + return base_url + svc_path diff --git a/linden/indra/lib/python/indra/ipc/tokenstream.py b/linden/indra/lib/python/indra/ipc/tokenstream.py index 83b087a..37896d3 100644 --- a/linden/indra/lib/python/indra/ipc/tokenstream.py +++ b/linden/indra/lib/python/indra/ipc/tokenstream.py @@ -2,28 +2,28 @@ @file tokenstream.py @brief Message template parsing utility class +$LicenseInfo:firstyear=2007&license=mit$ + Copyright (c) 2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ import re diff --git a/linden/indra/lib/python/indra/ipc/webdav.py b/linden/indra/lib/python/indra/ipc/webdav.py new file mode 100644 index 0000000..66e55ca --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/webdav.py @@ -0,0 +1,597 @@ +""" +@file webdav.py +@brief Classes to make manipulation of a webdav store easier. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import sys, os, httplib, urlparse +import socket, time +import xml.dom.minidom +import syslog +# import signal + +__revision__ = '0' + +dav_debug = False + + +# def urlsafe_b64decode (enc): +# return base64.decodestring (enc.replace ('_', '/').replace ('-', '+')) + +# def urlsafe_b64encode (str): +# return base64.encodestring (str).replace ('+', '-').replace ('/', '_') + + +class DAVError (Exception): + """ Base class for exceptions in this module. """ + def __init__ (self, status=0, message='', body='', details=''): + self.status = status + self.message = message + self.body = body + self.details = details + Exception.__init__ (self, '%d:%s:%s%s' % (self.status, self.message, + self.body, self.details)) + + def print_to_stderr (self): + """ print_to_stderr docstring """ + print >> sys.stderr, str (self.status) + ' ' + self.message + print >> sys.stderr, str (self.details) + + +class Timeout (Exception): + """ Timeout docstring """ + def __init__ (self, arg=''): + Exception.__init__ (self, arg) + + +def alarm_handler (signum, frame): + """ alarm_handler docstring """ + raise Timeout ('caught alarm') + + +class WebDAV: + """ WebDAV docstring """ + def __init__ (self, url, proxy=None, retries_before_fail=6): + self.init_url = url + self.init_proxy = proxy + self.retries_before_fail = retries_before_fail + url_parsed = urlparse.urlsplit (url) + + self.top_path = url_parsed[ 2 ] + # make sure top_path has a trailing / + if self.top_path == None or self.top_path == '': + self.top_path = '/' + elif len (self.top_path) > 1 and self.top_path[-1:] != '/': + self.top_path += '/' + + if dav_debug: + syslog.syslog ('new WebDAV %s : %s' % (str (url), str (proxy))) + + if proxy: + proxy_parsed = urlparse.urlsplit (proxy) + self.host_header = url_parsed[ 1 ] + host_and_port = proxy_parsed[ 1 ].split (':') + self.host = host_and_port[ 0 ] + if len (host_and_port) > 1: + self.port = int(host_and_port[ 1 ]) + else: + self.port = 80 + else: # no proxy + host_and_port = url_parsed[ 1 ].split (':') + self.host_header = None + self.host = host_and_port[ 0 ] + if len (host_and_port) > 1: + self.port = int(host_and_port[ 1 ]) + else: + self.port = 80 + + self.connection = False + self.connect () + + + def log (self, msg, depth=0): + """ log docstring """ + if dav_debug and depth == 0: + host = str (self.init_url) + if host == 'http://int.tuco.lindenlab.com:80/asset/': + host = 'tuco' + if host == 'http://harriet.lindenlab.com/asset-keep/': + host = 'harriet/asset-keep' + if host == 'http://harriet.lindenlab.com/asset-flag/': + host = 'harriet/asset-flag' + if host == 'http://harriet.lindenlab.com/asset/': + host = 'harriet/asset' + if host == 'http://ozzy.lindenlab.com/asset/': + host = 'ozzy/asset' + if host == 'http://station11.lindenlab.com:12041/:': + host = 'station11:12041' + proxy = str (self.init_proxy) + if proxy == 'None': + proxy = '' + if proxy == 'http://int.tuco.lindenlab.com:3128/': + proxy = 'tuco' + syslog.syslog ('WebDAV (%s:%s) %s' % (host, proxy, str (msg))) + + + def connect (self): + """ connect docstring """ + self.log ('connect') + self.connection = httplib.HTTPConnection (self.host, self.port) + + def __err (self, response, details): + """ __err docstring """ + raise DAVError (response.status, response.reason, response.read (), + str (self.init_url) + ':' + \ + str (self.init_proxy) + ':' + str (details)) + + def request (self, method, path, body=None, headers=None, + read_all=True, body_hook = None, recurse=0, allow_cache=True): + """ request docstring """ + # self.log ('request %s %s' % (method, path)) + if headers == None: + headers = {} + if not allow_cache: + headers['Pragma'] = 'no-cache' + headers['cache-control'] = 'no-cache' + try: + if method.lower () != 'purge': + if path.startswith ('/'): + path = path[1:] + if self.host_header: # use proxy + headers[ 'host' ] = self.host_header + fullpath = 'http://%s%s%s' % (self.host_header, + self.top_path, path) + else: # no proxy + fullpath = self.top_path + path + else: + fullpath = path + + self.connection.request (method, fullpath, body, headers) + if body_hook: + body_hook () + + # signal.signal (signal.SIGALRM, alarm_handler) + # try: + # signal.alarm (120) + # signal.alarm (0) + # except Timeout, e: + # if recurse < 6: + # return self.retry_request (method, path, body, headers, + # read_all, body_hook, recurse) + # else: + # raise DAVError (0, 'timeout', self.host, + # (method, path, body, headers, recurse)) + + response = self.connection.getresponse () + + if read_all: + while len (response.read (1024)) > 0: + pass + if (response.status == 500 or \ + response.status == 503 or \ + response.status == 403) and \ + recurse < self.retries_before_fail: + return self.retry_request (method, path, body, headers, + read_all, body_hook, recurse) + return response + except (httplib.ResponseNotReady, + httplib.BadStatusLine, + socket.error): + # if the server hangs up on us (keepalive off, broken pipe), + # we need to reconnect and try again. + if recurse < self.retries_before_fail: + return self.retry_request (method, path, body, headers, + read_all, body_hook, recurse) + raise DAVError (0, 'reconnect failed', self.host, + (method, path, body, headers, recurse)) + + + def retry_request (self, method, path, body, headers, + read_all, body_hook, recurse): + """ retry_request docstring """ + time.sleep (10.0 * recurse) + self.connect () + return self.request (method, path, body, headers, + read_all, body_hook, recurse+1) + + + + def propfind (self, path, body=None, depth=1): + """ propfind docstring """ + # self.log ('propfind %s' % path) + headers = {'Content-Type':'text/xml; charset="utf-8"', + 'Depth':str(depth)} + response = self.request ('PROPFIND', path, body, headers, False) + if response.status == 207: + return response # Multi-Status + self.__err (response, ('PROPFIND', path, body, headers, 0)) + + + def purge (self, path): + """ issue a squid purge command """ + headers = {'Accept':'*/*'} + response = self.request ('PURGE', path, None, headers) + if response.status == 200 or response.status == 404: + # 200 if it was purge, 404 if it wasn't there. + return response + self.__err (response, ('PURGE', path, None, headers)) + + + def get_file_size (self, path): + """ + Use propfind to ask a webdav server what the size of + a file is. If used on a directory (collection) return 0 + """ + self.log ('get_file_size %s' % path) + # "getcontentlength" property + # 8.1.1 Example - Retrieving Named Properties + # http://docs.python.org/lib/module-xml.dom.html + nsurl = 'http://apache.org/dav/props/' + doc = xml.dom.minidom.Document () + propfind_element = doc.createElementNS (nsurl, "D:propfind") + propfind_element.setAttributeNS (nsurl, 'xmlns:D', 'DAV:') + doc.appendChild (propfind_element) + prop_element = doc.createElementNS (nsurl, "D:prop") + propfind_element.appendChild (prop_element) + con_len_element = doc.createElementNS (nsurl, "D:getcontentlength") + prop_element.appendChild (con_len_element) + + response = self.propfind (path, doc.toxml ()) + doc.unlink () + + resp_doc = xml.dom.minidom.parseString (response.read ()) + cln = resp_doc.getElementsByTagNameNS ('DAV:','getcontentlength')[ 0 ] + try: + content_length = int (cln.childNodes[ 0 ].nodeValue) + except IndexError: + return 0 + resp_doc.unlink () + return content_length + + + def file_exists (self, path): + """ + do an http head on the given file. return True if it succeeds + """ + self.log ('file_exists %s' % path) + expect_gzip = path.endswith ('.gz') + response = self.request ('HEAD', path) + got_gzip = response.getheader ('Content-Encoding', '').strip () + if got_gzip.lower () == 'x-gzip' and expect_gzip == False: + # the asset server fakes us out if we ask for the non-gzipped + # version of an asset, but the server has the gzipped version. + return False + return response.status == 200 + + + def mkdir (self, path): + """ mkdir docstring """ + self.log ('mkdir %s' % path) + headers = {} + response = self.request ('MKCOL', path, None, headers) + if response.status == 201: + return # success + if response.status == 405: + return # directory already existed? + self.__err (response, ('MKCOL', path, None, headers, 0)) + + + def delete (self, path): + """ delete docstring """ + self.log ('delete %s' % path) + headers = {'Depth':'infinity'} # collections require infinity + response = self.request ('DELETE', path, None, headers) + if response.status == 204: + return # no content + if response.status == 404: + return # hmm + self.__err (response, ('DELETE', path, None, headers, 0)) + + + def list_directory (self, path, dir_filter=None, allow_cache=True, + minimum_cache_time=False): + """ + Request an http directory listing and parse the filenames out of lines + like: '
  • X'. If a filter function is provided, + only return filenames that the filter returns True for. + + This is sort of grody, but it seems faster than other ways of getting + this information from an isilon. + """ + self.log ('list_directory %s' % path) + + def try_match (lline, before, after): + """ try_match docstring """ + try: + blen = len (before) + asset_start_index = lline.index (before) + asset_end_index = lline.index (after, asset_start_index + blen) + asset = line[ asset_start_index + blen : asset_end_index ] + + if not dir_filter or dir_filter (asset): + return [ asset ] + return [] + except ValueError: + return [] + + if len (path) > 0 and path[-1:] != '/': + path += '/' + + response = self.request ('GET', path, None, {}, False, + allow_cache=allow_cache) + + if allow_cache and minimum_cache_time: # XXX + print response.getheader ('Date') + # s = "2005-12-06T12:13:14" + # from datetime import datetime + # from time import strptime + # datetime(*strptime(s, "%Y-%m-%dT%H:%M:%S")[0:6]) + # datetime.datetime(2005, 12, 6, 12, 13, 14) + + if response.status != 200: + self.__err (response, ('GET', path, None, {}, 0)) + assets = [] + for line in response.read ().split ('\n'): + lline = line.lower () + if lline.find ("parent directory") == -1: + # isilon file + assets += try_match (lline, '
  • ') + # apache dir + assets += try_match (lline, 'alt="[dir]"> ') + # apache file + assets += try_match (lline, 'alt="[ ]"> ') + return assets + + + def __tmp_filename (self, path_and_file): + """ __tmp_filename docstring """ + head, tail = os.path.split (path_and_file) + if head != '': + return head + '/.' + tail + '.' + str (os.getpid ()) + else: + return head + '.' + tail + '.' + str (os.getpid ()) + + + def __put__ (self, filesize, body_hook, remotefile): + """ __put__ docstring """ + headers = {'Content-Length' : str (filesize)} + remotefile_tmp = self.__tmp_filename (remotefile) + response = self.request ('PUT', remotefile_tmp, None, + headers, True, body_hook) + if not response.status in (201, 204): # created, no content + self.__err (response, ('PUT', remotefile, None, headers, 0)) + if filesize != self.get_file_size (remotefile_tmp): + try: + self.delete (remotefile_tmp) + except: + pass + raise DAVError (0, 'tmp upload error', remotefile_tmp) + # move the file to its final location + try: + self.rename (remotefile_tmp, remotefile) + except DAVError, exc: + if exc.status == 403: # try to clean up the tmp file + try: + self.delete (remotefile_tmp) + except: + pass + raise + if filesize != self.get_file_size (remotefile): + raise DAVError (0, 'file upload error', str (remotefile_tmp)) + + + def put_string (self, strng, remotefile): + """ put_string docstring """ + self.log ('put_string %d -> %s' % (len (strng), remotefile)) + filesize = len (strng) + def body_hook (): + """ body_hook docstring """ + self.connection.send (strng) + self.__put__ (filesize, body_hook, remotefile) + + + def put_file (self, localfile, remotefile): + """ + Send a local file to a remote webdav store. First, upload to + a temporary filename. Next make sure the file is the size we + expected. Next, move the file to its final location. Next, + check the file size at the final location. + """ + self.log ('put_file %s -> %s' % (localfile, remotefile)) + filesize = os.path.getsize (localfile) + def body_hook (): + """ body_hook docstring """ + handle = open (localfile) + while True: + data = handle.read (1300) + if len (data) == 0: + break + self.connection.send (data) + handle.close () + self.__put__ (filesize, body_hook, remotefile) + + + def create_empty_file (self, remotefile): + """ create an empty file """ + self.log ('touch_file %s' % (remotefile)) + headers = {'Content-Length' : '0'} + response = self.request ('PUT', remotefile, None, headers) + if not response.status in (201, 204): # created, no content + self.__err (response, ('PUT', remotefile, None, headers, 0)) + if self.get_file_size (remotefile) != 0: + raise DAVError (0, 'file upload error', str (remotefile)) + + + def __get_file_setup (self, remotefile, check_size=True): + """ __get_file_setup docstring """ + if check_size: + remotesize = self.get_file_size (remotefile) + response = self.request ('GET', remotefile, None, {}, False) + if response.status != 200: + self.__err (response, ('GET', remotefile, None, {}, 0)) + try: + content_length = int (response.getheader ("Content-Length")) + except TypeError: + content_length = None + if check_size: + if content_length != remotesize: + raise DAVError (0, 'file DL size error', remotefile) + return (response, content_length) + + + def __get_file_read (self, writehandle, response, content_length): + """ __get_file_read docstring """ + if content_length != None: + so_far_length = 0 + while so_far_length < content_length: + data = response.read (content_length - so_far_length) + if len (data) == 0: + raise DAVError (0, 'short file download') + so_far_length += len (data) + writehandle.write (data) + while len (response.read ()) > 0: + pass + else: + while True: + data = response.read () + if (len (data) < 1): + break + writehandle.write (data) + + + def get_file (self, remotefile, localfile, check_size=True): + """ + Get a remote file from a webdav server. Download to a local + tmp file, then move into place. Sanity check file sizes as + we go. + """ + self.log ('get_file %s -> %s' % (remotefile, localfile)) + (response, content_length) = \ + self.__get_file_setup (remotefile, check_size) + localfile_tmp = self.__tmp_filename (localfile) + handle = open (localfile_tmp, 'w') + self.__get_file_read (handle, response, content_length) + handle.close () + if check_size: + if content_length != os.path.getsize (localfile_tmp): + raise DAVError (0, 'file DL size error', + remotefile+','+localfile) + os.rename (localfile_tmp, localfile) + + + def get_file_as_string (self, remotefile, check_size=True): + """ + download a file from a webdav server and return it as a string. + """ + self.log ('get_file_as_string %s' % remotefile) + (response, content_length) = \ + self.__get_file_setup (remotefile, check_size) + # (tmp_handle, tmp_filename) = tempfile.mkstemp () + tmp_handle = os.tmpfile () + self.__get_file_read (tmp_handle, response, content_length) + tmp_handle.seek (0) + ret = tmp_handle.read () + tmp_handle.close () + # os.unlink (tmp_filename) + return ret + + + def get_post_as_string (self, remotefile, body): + """ + Do an http POST, send body, get response and return it. + """ + self.log ('get_post_as_string %s' % remotefile) + # headers = {'Content-Type':'application/x-www-form-urlencoded'} + headers = {'Content-Type':'text/xml; charset="utf-8"'} + # b64body = urlsafe_b64encode (asset_url) + response = self.request ('POST', remotefile, body, headers, False) + if response.status != 200: + self.__err (response, ('POST', remotefile, body, headers, 0)) + try: + content_length = int (response.getheader ('Content-Length')) + except TypeError: + content_length = None + tmp_handle = os.tmpfile () + self.__get_file_read (tmp_handle, response, content_length) + tmp_handle.seek (0) + ret = tmp_handle.read () + tmp_handle.close () + return ret + + + def __destination_command (self, verb, remotesrc, dstdav, remotedst): + """ + self and dstdav should point to the same http server. + """ + if len (remotedst) > 0 and remotedst[ 0 ] == '/': + remotedst = remotedst[1:] + headers = {'Destination': 'http://%s:%d%s%s' % (dstdav.host, + dstdav.port, + dstdav.top_path, + remotedst)} + response = self.request (verb, remotesrc, None, headers) + if response.status == 201: + return # created + if response.status == 204: + return # no content + self.__err (response, (verb, remotesrc, None, headers, 0)) + + + def rename (self, remotesrc, remotedst): + """ rename a file on a webdav server """ + self.log ('rename %s -> %s' % (remotesrc, remotedst)) + self.__destination_command ('MOVE', remotesrc, self, remotedst) + def xrename (self, remotesrc, dstdav, remotedst): + """ rename a file on a webdav server """ + self.log ('xrename %s -> %s' % (remotesrc, remotedst)) + self.__destination_command ('MOVE', remotesrc, dstdav, remotedst) + + + def copy (self, remotesrc, remotedst): + """ copy a file on a webdav server """ + self.log ('copy %s -> %s' % (remotesrc, remotedst)) + self.__destination_command ('COPY', remotesrc, self, remotedst) + def xcopy (self, remotesrc, dstdav, remotedst): + """ copy a file on a webdav server """ + self.log ('xcopy %s -> %s' % (remotesrc, remotedst)) + self.__destination_command ('COPY', remotesrc, dstdav, remotedst) + + +def put_string (data, url): + """ + upload string s to a url + """ + url_parsed = urlparse.urlsplit (url) + dav = WebDAV ('%s://%s/' % (url_parsed[ 0 ], url_parsed[ 1 ])) + dav.put_string (data, url_parsed[ 2 ]) + + +def get_string (url, check_size=True): + """ + return the contents of a url as a string + """ + url_parsed = urlparse.urlsplit (url) + dav = WebDAV ('%s://%s/' % (url_parsed[ 0 ], url_parsed[ 1 ])) + return dav.get_file_as_string (url_parsed[ 2 ], check_size) diff --git a/linden/indra/lib/python/indra/ipc/xml_rpc.py b/linden/indra/lib/python/indra/ipc/xml_rpc.py new file mode 100644 index 0000000..dc8f0aa --- /dev/null +++ b/linden/indra/lib/python/indra/ipc/xml_rpc.py @@ -0,0 +1,273 @@ +"""\ +@file xml_rpc.py +@brief An implementation of a parser/generator for the XML-RPC xml format. + +$LicenseInfo:firstyear=2006&license=mit$ + +Copyright (c) 2006-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + + +from greenlet import greenlet + +from mulib import mu + +from xml.sax import handler +from xml.sax import parseString + + +# States +class Expected(object): + def __init__(self, tag): + self.tag = tag + + def __getattr__(self, name): + return type(self)(name) + + def __repr__(self): + return '%s(%r)' % ( + type(self).__name__, self.tag) + + +class START(Expected): + pass + + +class END(Expected): + pass + + +class STR(object): + tag = '' + + +START = START('') +END = END('') + + +class Malformed(Exception): + pass + + +class XMLParser(handler.ContentHandler): + def __init__(self, state_machine, next_states): + handler.ContentHandler.__init__(self) + self.state_machine = state_machine + if not isinstance(next_states, tuple): + next_states = (next_states, ) + self.next_states = next_states + self._character_buffer = '' + + def assertState(self, state, name, *rest): + if not isinstance(self.next_states, tuple): + self.next_states = (self.next_states, ) + for next in self.next_states: + if type(state) == type(next): + if next.tag and next.tag != name: + raise Malformed( + "Expected %s, got %s %s %s" % ( + next, state, name, rest)) + break + else: + raise Malformed( + "Expected %s, got %s %s %s" % ( + self.next_states, state, name, rest)) + + def startElement(self, name, attrs): + self.assertState(START, name.lower(), attrs) + self.next_states = self.state_machine.switch(START, (name.lower(), dict(attrs))) + + def endElement(self, name): + if self._character_buffer.strip(): + characters = self._character_buffer.strip() + self._character_buffer = '' + self.assertState(STR, characters) + self.next_states = self.state_machine.switch(characters) + self.assertState(END, name.lower()) + self.next_states = self.state_machine.switch(END, name.lower()) + + def error(self, exc): + self.bozo = 1 + self.exc = exc + + def fatalError(self, exc): + self.error(exc) + raise exc + + def characters(self, characters): + self._character_buffer += characters + + +def parse(what): + child = greenlet(xml_rpc) + me = greenlet.getcurrent() + startup_states = child.switch(me) + parser = XMLParser(child, startup_states) + try: + parseString(what, parser) + except Malformed: + print what + raise + return child.switch() + + +def xml_rpc(yielder): + yielder.switch(START.methodcall) + yielder.switch(START.methodname) + methodName = yielder.switch(STR) + yielder.switch(END.methodname) + + yielder.switch(START.params) + + root = None + params = [] + while True: + state, _ = yielder.switch(START.param, END.params) + if state == END: + break + + yielder.switch(START.value) + + params.append( + handle(yielder)) + + yielder.switch(END.value) + yielder.switch(END.param) + + yielder.switch(END.methodcall) + ## Resume parse + yielder.switch() + ## Return result to parse + return methodName.strip(), params + + +def handle(yielder): + _, (tag, attrs) = yielder.switch(START) + if tag in ['int', 'i4']: + result = int(yielder.switch(STR)) + elif tag == 'boolean': + result = bool(int(yielder.switch(STR))) + elif tag == 'string': + result = yielder.switch(STR) + elif tag == 'double': + result = float(yielder.switch(STR)) + elif tag == 'datetime.iso8601': + result = yielder.switch(STR) + elif tag == 'base64': + result = base64.b64decode(yielder.switch(STR)) + elif tag == 'struct': + result = {} + while True: + state, _ = yielder.switch(START.member, END.struct) + if state == END: + break + + yielder.switch(START.name) + key = yielder.switch(STR) + yielder.switch(END.name) + + yielder.switch(START.value) + result[key] = handle(yielder) + yielder.switch(END.value) + + yielder.switch(END.member) + ## We already handled above, don't want to handle it below + return result + elif tag == 'array': + result = [] + yielder.switch(START.data) + while True: + state, _ = yielder.switch(START.value, END.data) + if state == END: + break + + result.append(handle(yielder)) + + yielder.switch(END.value) + + yielder.switch(getattr(END, tag)) + + return result + + +VALUE = mu.tag_factory('value') +BOOLEAN = mu.tag_factory('boolean') +INT = mu.tag_factory('int') +STRUCT = mu.tag_factory('struct') +MEMBER = mu.tag_factory('member') +NAME = mu.tag_factory('name') +ARRAY = mu.tag_factory('array') +DATA = mu.tag_factory('data') +STRING = mu.tag_factory('string') +DOUBLE = mu.tag_factory('double') +METHODRESPONSE = mu.tag_factory('methodResponse') +PARAMS = mu.tag_factory('params') +PARAM = mu.tag_factory('param') + +mu.inline_elements['string'] = True +mu.inline_elements['boolean'] = True +mu.inline_elements['name'] = True + + +def _generate(something): + if isinstance(something, dict): + result = STRUCT() + for key, value in something.items(): + result[ + MEMBER[ + NAME[key], _generate(value)]] + return VALUE[result] + elif isinstance(something, list): + result = DATA() + for item in something: + result[_generate(item)] + return VALUE[ARRAY[[result]]] + elif isinstance(something, basestring): + return VALUE[STRING[something]] + elif isinstance(something, bool): + if something: + return VALUE[BOOLEAN['1']] + return VALUE[BOOLEAN['0']] + elif isinstance(something, int): + return VALUE[INT[something]] + elif isinstance(something, float): + return VALUE[DOUBLE[something]] + +def generate(*args): + params = PARAMS() + for arg in args: + params[PARAM[_generate(arg)]] + return METHODRESPONSE[params] + + +if __name__ == '__main__': + print parse(""" examples.getStateName 41 +""") + + + + + + + + + diff --git a/linden/indra/lib/python/indra/util/__init__.py b/linden/indra/lib/python/indra/util/__init__.py index 3f79d0a..3eda184 100644 --- a/linden/indra/lib/python/indra/util/__init__.py +++ b/linden/indra/lib/python/indra/util/__init__.py @@ -2,26 +2,26 @@ @file __init__.py @brief Initialization file for the indra util module. +$LicenseInfo:firstyear=2006&license=mit$ + Copyright (c) 2006-2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ diff --git a/linden/indra/lib/python/indra/util/helpformatter.py b/linden/indra/lib/python/indra/util/helpformatter.py new file mode 100644 index 0000000..c4ff27f --- /dev/null +++ b/linden/indra/lib/python/indra/util/helpformatter.py @@ -0,0 +1,52 @@ +"""\ +@file helpformatter.py +@author Phoenix +@brief Class for formatting optparse descriptions. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import optparse +import textwrap + +class Formatter(optparse.IndentedHelpFormatter): + def __init__( + self, + p_indentIncrement = 2, + p_maxHelpPosition = 24, + p_width = 79, + p_shortFirst = 1) : + optparse.HelpFormatter.__init__( + self, + p_indentIncrement, + p_maxHelpPosition, + p_width, + p_shortFirst) + def format_description(self, p_description): + t_descWidth = self.width - self.current_indent + t_indent = " " * (self.current_indent + 2) + return "\n".join( + [textwrap.fill(descr, t_descWidth, initial_indent = t_indent, + subsequent_indent = t_indent) + for descr in p_description.split("\n")] ) diff --git a/linden/indra/lib/python/indra/util/llmanifest.py b/linden/indra/lib/python/indra/util/llmanifest.py index c496e95..029b697 100644 --- a/linden/indra/lib/python/indra/util/llmanifest.py +++ b/linden/indra/lib/python/indra/util/llmanifest.py @@ -3,28 +3,28 @@ @author Ryan Williams @brief Library for specifying operations on a set of files. +$LicenseInfo:firstyear=2007&license=mit$ + Copyright (c) 2007, Linden Research, Inc. -# Second Life Viewer Source Code -# The source code in this file ("Source Code") is provided by Linden Lab -# to you under the terms of the GNU General Public License, version 2.0 -# ("GPL"), unless you have obtained a separate licensing agreement -# ("Other License"), formally executed by you and Linden Lab. Terms of -# the GPL can be found in doc/GPL-license.txt in this distribution, or -# online at http://secondlife.com/developers/opensource/gplv2 -# -# There are special exceptions to the terms and conditions of the GPL as -# it is applied to this Source Code. View the full text of the exception -# in the file doc/FLOSS-exception.txt in this software distribution, or -# online at http://secondlife.com/developers/opensource/flossexception -# -# By copying, modifying or distributing this software, you acknowledge -# that you have read and understood your obligations described above, -# and agree to abide by those obligations. -# -# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO -# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, -# COMPLETENESS OR PERFORMANCE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ """ import commands diff --git a/linden/indra/lib/python/indra/util/llversion.py b/linden/indra/lib/python/indra/util/llversion.py new file mode 100644 index 0000000..5e699d5 --- /dev/null +++ b/linden/indra/lib/python/indra/util/llversion.py @@ -0,0 +1,95 @@ +"""@file llversion.py +@brief Utility for parsing llcommon/llversion${server}.h + for the version string and channel string + Utility that parses svn info for branch and revision + +$LicenseInfo:firstyear=2006&license=mit$ + +Copyright (c) 2006-2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import re, sys, os, commands + +# Methods for gathering version information from +# llversionviewer.h and llversionserver.h + +def get_src_root(): + indra_lib_python_indra_path = os.path.dirname(__file__) + return os.path.abspath(os.path.realpath(indra_lib_python_indra_path + "/../../../../../")) + +def get_version_file_contents(version_type): + filepath = get_src_root() + '/indra/llcommon/llversion%s.h' % version_type + file = open(filepath,"r") + file_str = file.read() + file.close() + return file_str + +def get_version(version_type): + file_str = get_version_file_contents(version_type) + m = re.search('const S32 LL_VERSION_MAJOR = (\d+);', file_str) + VER_MAJOR = m.group(1) + m = re.search('const S32 LL_VERSION_MINOR = (\d+);', file_str) + VER_MINOR = m.group(1) + m = re.search('const S32 LL_VERSION_PATCH = (\d+);', file_str) + VER_PATCH = m.group(1) + m = re.search('const S32 LL_VERSION_BUILD = (\d+);', file_str) + VER_BUILD = m.group(1) + version = "%(VER_MAJOR)s.%(VER_MINOR)s.%(VER_PATCH)s.%(VER_BUILD)s" % locals() + return version + +def get_channel(version_type): + file_str = get_version_file_contents(version_type) + m = re.search('const char \* const LL_CHANNEL = "(.+)";', file_str) + return m.group(1) + +def get_viewer_version(): + return get_version('viewer') + +def get_server_version(): + return get_version('server') + +def get_viewer_channel(): + return get_channel('viewer') + +def get_server_channel(): + return get_channel('server') + +# Methods for gathering subversion information +def get_svn_status_matching(regular_expression): + # Get the subversion info from the working source tree + status, output = commands.getstatusoutput('svn info %s' % get_src_root()) + m = regular_expression.search(output) + if not m: + print "Failed to parse svn info output, resultfollows:" + print output + raise Exception, "No matching svn status in "+src_root + return m.group(1) + +def get_svn_branch(): + branch_re = re.compile('URL: (\S+)') + return get_svn_status_matching(branch_re) + +def get_svn_revision(): + last_rev_re = re.compile('Last Changed Rev: (\d+)') + return get_svn_status_matching(last_rev_re) + + diff --git a/linden/indra/lib/python/indra/util/named_query.py b/linden/indra/lib/python/indra/util/named_query.py new file mode 100644 index 0000000..019eb63 --- /dev/null +++ b/linden/indra/lib/python/indra/util/named_query.py @@ -0,0 +1,215 @@ +"""\ +@file named_query.py +@author Ryan Williams, Phoenix +@date 2007-07-31 +@brief An API for running named queries. + +$LicenseInfo:firstyear=2007&license=mit$ + +Copyright (c) 2007, Linden Research, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +$/LicenseInfo$ +""" + +import MySQLdb +import os +import os.path +import time + +from indra.base import llsd +from indra.base import config +from indra.ipc import russ + +_g_named_manager = None + +# this function is entirely intended for testing purposes, +# because it's tricky to control the config from inside a test +def _init_g_named_manager(sql_dir = None): + if sql_dir is None: + sql_dir = config.get('named-query-base-dir') + global _g_named_manager + _g_named_manager = NamedQueryManager( + os.path.abspath(os.path.realpath(sql_dir))) + +def get(name): + "@brief get the named query object to be used to perform queries" + if _g_named_manager is None: + _init_g_named_manager() + return _g_named_manager.get(name) + +def sql(name, params): + # use module-global NamedQuery object to perform default substitution + return get(name).sql(params) + +def run(connection, name, params, expect_rows = None): + """\ +@brief given a connection, run a named query with the params + +Note that this function will fetch ALL rows. +@param connection The connection to use +@param name The name of the query to run +@param params The parameters passed into the query +@param expect_rows The number of rows expected. Set to 1 if return_as_map is true. Raises ExpectationFailed if the number of returned rows doesn't exactly match. Kind of a hack. +@return Returns the result set as a list of dicts. +""" + return get(name).run(connection, params, expect_rows) + +class ExpectationFailed(Exception): + def __init__(self, message): + self.message = message + +class NamedQuery(object): + def __init__(self, name, filename): + self._stat_interval = 5000 # 5 seconds + self._name = name + self._location = filename + self.load_contents() + + def name(self): + return self._name + + def get_modtime(self): + return os.path.getmtime(self._location) + + def load_contents(self): + self._contents = llsd.parse(open(self._location).read()) + self._ttl = int(self._contents.get('ttl', 0)) + self._return_as_map = bool(self._contents.get('return_as_map', False)) + self._legacy_dbname = self._contents.get('legacy_dbname', None) + self._legacy_query = self._contents.get('legacy_query', None) + self._options = self._contents.get('options', {}) + self._base_query = self._contents['base_query'] + + self._last_mod_time = self.get_modtime() + self._last_check_time = time.time() + + def ttl(self): + return self._ttl + + def legacy_dbname(self): + return self._legacy_dbname + + def legacy_query(self): + return self._legacy_query + + def return_as_map(self): + return self._return_as_map + + def run(self, connection, params, expect_rows = None, use_dictcursor = True): + """\ +@brief given a connection, run a named query with the params + +Note that this function will fetch ALL rows. We do this because it +opens and closes the cursor to generate the values, and this isn't a generator so the +cursor has no life beyond the method call. +@param cursor The connection to use (this generates its own cursor for the query) +@param name The name of the query to run +@param params The parameters passed into the query +@param expect_rows The number of rows expected. Set to 1 if return_as_map is true. Raises ExpectationFailed if the number of returned rows doesn't exactly match. Kind of a hack. +@param use_dictcursor Set to false to use a normal cursor and manually convert the rows to dicts. +@return Returns the result set as a list of dicts, or, if the named query has return_as_map set to true, returns a single dict. + """ + if use_dictcursor: + cursor = connection.cursor(MySQLdb.cursors.DictCursor) + else: + cursor = connection.cursor() + + statement = self.sql(params) + #print "SQL:", statement + rows = cursor.execute(statement) + + # *NOTE: the expect_rows argument is a very cheesy way to get some + # validation on the result set. If you want to add more expectation + # logic, do something more object-oriented and flexible. Or use an ORM. + if(self._return_as_map): + expect_rows = 1 + if expect_rows is not None and rows != expect_rows: + cursor.close() + raise ExpectationFailed("Statement expected %s rows, got %s. Sql: %s" % ( + expect_rows, rows, statement)) + + # convert to dicts manually if we're not using a dictcursor + if use_dictcursor: + result_set = cursor.fetchall() + else: + if cursor.description is None: + # an insert or something + x = cursor.fetchall() + cursor.close() + return x + + names = [x[0] for x in cursor.description] + + result_set = [] + for row in cursor.fetchall(): + converted_row = {} + for idx, col_name in enumerate(names): + converted_row[col_name] = row[idx] + result_set.append(converted_row) + + cursor.close() + if self._return_as_map: + return result_set[0] + return result_set + + def sql(self, params): + self.refresh() + + # build the query from the options available and the params + base_query = [] + base_query.append(self._base_query) + for opt, extra_where in self._options.items(): + if opt in params and (params[opt] == 0 or params[opt]): + if type(extra_where) in (dict, list, tuple): + base_query.append(extra_where[params[opt]]) + else: + base_query.append(extra_where) + + full_query = '\n'.join(base_query) + + # do substitution + sql = russ.format(full_query, params) + return sql + + def refresh(self): + # only stat the file every so often + now = time.time() + if(now - self._last_check_time > self._stat_interval): + self._last_check_time = now + modtime = self.get_modtime() + if(modtime > self._last_mod_time): + self.load_contents() + +class NamedQueryManager(object): + def __init__(self, named_queries_dir): + self._dir = os.path.abspath(os.path.realpath(named_queries_dir)) + self._cached_queries = {} + + def sql(self, name, params): + nq = self.get(name) + return nq.sql(params) + + def get(self, name): + # new up/refresh a NamedQuery based on the name + nq = self._cached_queries.get(name) + if nq is None: + nq = NamedQuery(name, os.path.join(self._dir, name)) + self._cached_queries[name] = nq + return nq -- cgit v1.1