diff --git a/sipsimple/configuration/backend/file.py b/sipsimple/configuration/backend/file.py index 15e78a93..4d1da3d7 100644 --- a/sipsimple/configuration/backend/file.py +++ b/sipsimple/configuration/backend/file.py @@ -1,231 +1,231 @@ """Configuration backend for storing settings in a simple plain text format""" __all__ = ["FileParserError", "FileBuilderError", "FileBackend"] import errno import os import re import platform import random from collections import deque from application.system import makedirs, openfile, unlink from zope.interface import implementer from sipsimple.configuration.backend import IConfigurationBackend, ConfigurationBackendError class FileParserError(ConfigurationBackendError): """Error raised when the configuration file cannot be parsed.""" class FileBuilderError(ConfigurationBackendError): """Error raised when the configuration data cannot be saved.""" class GroupState(object): """ Internal class used for keeping track of the containing groups while parsing. """ def __init__(self, indentation): self.indentation = indentation self.data = {} class Line(object): """Internal representation of lines in a configuration file""" def __init__(self, indentation, name, separator, value): self.indentation = indentation self.name = name self.separator = separator self.value = value def __repr__(self): return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.indentation, self.name, self.separator, self.value) @implementer(IConfigurationBackend) class FileBackend(object): """ Implementation of a configuration backend that stores data in a simple plain text format. """ escape_characters_re = re.compile(r"""[,"'=: #\\\t\x0b\x0c\n\r]""") def __init__(self, filename, encoding='utf-8'): """ Initialize the configuration backend with the specified file. The file is not read at this time, but rather each time the load method is called. """ self.filename = filename self.encoding = encoding def load(self): """ Read the file configured with this backend and parse it, returning a dictionary conforming to the IConfigurationBackend specification. """ try: file = open(self.filename) except IOError as e: if e.errno == errno.ENOENT: return {} else: raise ConfigurationBackendError("failed to read configuration file: %s" % str(e)) state_stack = deque() state_stack.appendleft(GroupState(-1)) for lineno, line in enumerate(file, 1): line = self._parse_line(line, lineno) if not line.name: continue # find the container for this declaration while state_stack[0].indentation >= line.indentation: state_stack.popleft() if line.separator == ':': new_group_state = GroupState(line.indentation) state_stack[0].data[line.name] = new_group_state.data state_stack.appendleft(new_group_state) elif line.separator == '=': state_stack[0].data[line.name] = line.value return state_stack[-1].data def save(self, data): """ Given a dictionary conforming to the IConfigurationBackend specification, write the data to the file configured with this backend in a format suitable to be read back using load(). """ lines = self._build_group(data, 0) config_directory = os.path.dirname(self.filename) tmp_filename = '%s.%d.%08X' % (self.filename, os.getpid(), random.getrandbits(32)) try: if config_directory: makedirs(config_directory) file = openfile(tmp_filename, 'wb', permissions=0o600) file.write((os.linesep.join(lines)+os.linesep).encode(self.encoding)) file.close() if platform.system() == 'Windows': # os.rename does not work on Windows if the destination file already exists. # It seems there is no atomic way to do this on Windows. unlink(self.filename) os.rename(tmp_filename, self.filename) except (IOError, OSError) as e: raise ConfigurationBackendError("failed to write configuration file: %s" % str(e)) def _parse_line(self, line, lineno): def advance_to_next_token(line): counter = 0 while line and line[0].isspace(): line.popleft() counter += 1 if line and line[0] == '#': line.clear() return counter def token_iterator(line, delimiter=''): quote_char = None while line: if quote_char is None and line[0] in delimiter: break char = line.popleft() if char in "'\"": if quote_char is None: quote_char = char continue elif quote_char == char: quote_char = None continue else: yield char elif char == '\\': if not line: raise FileParserError("unexpected `\\' at end of line %d" % lineno) char = line.popleft() if char == 'n': yield '\n' elif char == 'r': yield '\r' else: yield char elif quote_char is None and char == '#': line.clear() break elif quote_char is None and char.isspace(): break else: yield char if quote_char is not None: raise FileParserError("missing ending quote at line %d" % lineno) - line = deque(line.rstrip().decode(self.encoding)) + line = deque(line.rstrip()) indentation = advance_to_next_token(line) if not line: return Line(indentation, None, None, None) name = ''.join(token_iterator(line, delimiter=':=')) advance_to_next_token(line) if not line or line[0] not in ':=': raise FileParserError("expected one of `:' or `=' at line %d" % lineno) if not name: raise FileParserError("missing setting/section name at line %d" % lineno) separator = line.popleft() advance_to_next_token(line) if not line: return Line(indentation, name, separator, None) elif separator == ':': raise FileParserError("unexpected characters after `:' at line %d" % lineno) value = None value_list = None while line: value = ''.join(token_iterator(line, delimiter=',')) advance_to_next_token(line) if line: if line[0] == ',': line.popleft() advance_to_next_token(line) if value_list is None: value_list = [] else: raise FileParserError("unexpected characters after value at line %d" % lineno) if value_list is not None: value_list.append(value) value = value_list if value_list is not None else value return Line(indentation, name, separator, value) def _build_group(self, group, indentation): setting_lines = [] group_lines = [] indent_spaces = ' '*4*indentation for name, data in sorted(group.items()): if data is None: setting_lines.append('%s%s =' % (indent_spaces, self._escape(name))) elif type(data) is dict: group_lines.append('%s%s:' % (indent_spaces, self._escape(name))) group_lines.extend(self._build_group(data, indentation+1)) group_lines.append('') elif type(data) is list: list_value = ', '.join(self._escape(item) for item in data) if len(data) == 1: list_value += ',' setting_lines.append('%s%s = %s' % (indent_spaces, self._escape(name), list_value)) elif type(data) is str: setting_lines.append('%s%s = %s' % (indent_spaces, self._escape(name), self._escape(data))) else: raise FileBuilderError("expected unicode, dict or list object, got %s" % type(data).__name__) return setting_lines + group_lines def _escape(self, value): if value == '': return '""' elif self.escape_characters_re.search(value): return '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r') else: return value diff --git a/sipsimple/streams/__init__.py b/sipsimple/streams/__init__.py index 0f89c49e..ba247781 100644 --- a/sipsimple/streams/__init__.py +++ b/sipsimple/streams/__init__.py @@ -1,119 +1,120 @@ """ This module automatically registers media streams to a stream registry allowing for a plug and play mechanism of various types of media negotiated in a SIP session that can be added to this library by using a generic API. For actual implementations see rtp/* and msrp/* that have media stream implementations based on their respective RTP and MSRP protocols. """ __all__ = ['StreamError', 'InvalidStreamError', 'UnknownStreamError', 'IMediaStream', 'MediaStreamRegistry', 'MediaStreamType'] from operator import attrgetter from zope.interface import Interface, Attribute class StreamError(Exception): pass class InvalidStreamError(StreamError): pass class UnknownStreamError(StreamError): pass # The MediaStream interface # class IMediaStream(Interface): type = Attribute("A string identifying the stream type (ex: audio, video, ...)") priority = Attribute("An integer value indicating the stream priority relative to the other streams types (higher numbers have higher priority).") session = Attribute("Session object to which this stream is attached") hold_supported = Attribute("True if the stream supports hold") on_hold_by_local = Attribute("True if the stream is on hold by the local party") on_hold_by_remote = Attribute("True if the stream is on hold by the remote") on_hold = Attribute("True if either on_hold_by_local or on_hold_by_remote is true") # this should be a classmethod, but zopeinterface complains if we decorate it with @classmethod -Dan def new_from_sdp(cls, session, remote_sdp, stream_index): pass def get_local_media(self, for_offer): pass def initialize(self, session, direction): pass def start(self, local_sdp, remote_sdp, stream_index): pass def deactivate(self): pass def end(self): pass def validate_update(self, remote_sdp, stream_index): pass def update(self, local_sdp, remote_sdp, stream_index): pass def hold(self): pass def unhold(self): pass def reset(self, stream_index): pass # The MediaStream registry # class StreamDescriptor(object): def __init__(self, type): self.type = type def __get__(self, obj, owner): return self if obj is None else obj.get(self.type) def __set__(self, obj, value): raise AttributeError('cannot set attribute') def __delete__(self, obj): raise AttributeError('cannot delete attribute') class MediaStreamRegistry(object): def __init__(self): self.__types__ = [] def __iter__(self): return iter(self.__types__) def add(self, cls): if cls.type is not None and cls.priority is not None and cls not in self.__types__: self.__types__.append(cls) self.__types__.sort(key=attrgetter('priority'), reverse=True) - setattr(self.__class__, cls.type.title().translate(None, ' -_') + 'Stream', StreamDescriptor(cls.type)) + translation_table = dict.fromkeys(map(ord, ' -_'), None) + setattr(self.__class__, cls.type.title().translate(translation_table) + 'Stream', StreamDescriptor(cls.type)) def get(self, type): try: return next(cls for cls in self.__types__ if cls.type == type) except StopIteration: raise UnknownStreamError("unknown stream type: %s" % type) MediaStreamRegistry = MediaStreamRegistry() class MediaStreamType(type): """Metaclass for MediaStream classes that automatically adds them to the media stream registry""" type = None priority = None def __init__(cls, name, bases, dictionary): super(MediaStreamType, cls).__init__(name, bases, dictionary) MediaStreamRegistry.add(cls) # Import the submodules in order for them to register the streams they define in MediaStreamRegistry from sipsimple.streams import msrp, rtp diff --git a/sipsimple/util/__init__.py b/sipsimple/util/__init__.py index af49a9a3..486b8b79 100644 --- a/sipsimple/util/__init__.py +++ b/sipsimple/util/__init__.py @@ -1,155 +1,155 @@ """Implements utilities commonly used in various parts of the library""" __all__ = ["All", "Any", "ExponentialTimer", "ISOTimestamp", "MultilingualText", "user_info", "sha1", "execute_once"] import os import platform import sys import dateutil.parser from application.notification import NotificationCenter from application.python.types import Singleton, MarkerType from datetime import datetime from dateutil.tz import tzlocal, tzutc from twisted.internet import reactor from sipsimple.util._sha1 import sha1 # Utility classes # class All(object, metaclass=MarkerType): pass class Any(object, metaclass=MarkerType): pass class ISOTimestamp(datetime): def __new__(cls, *args, **kw): if len(args) == 1: value = args[0] if isinstance(value, cls): return value elif isinstance(value, str): value = dateutil.parser.parse(value) return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo) elif isinstance(value, datetime): return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo or tzlocal()) else: return datetime.__new__(cls, *args, **kw) else: if len(args) < 8 and 'tzinfo' not in kw: kw['tzinfo'] = tzlocal() return datetime.__new__(cls, *args, **kw) def __str__(self): return self.isoformat() @classmethod def now(cls): return cls(datetime.now(tzlocal())) @classmethod def utcnow(cls): return cls(datetime.now(tzutc())) class MultilingualText(str): def __new__(cls, *args, **translations): if len(args) > 1: raise TypeError("%s.__new__ takes at most 1 positional argument (%d given)" % (cls.__name__, len(args))) default = args[0] if args else translations.get('en', '') obj = str.__new__(cls, default) obj.translations = translations return obj def get_translation(self, language): return self.translations.get(language, self) class ExponentialTimer(object): def __init__(self): self._timer = None self._limit_timer = None self._interval = 0 self._iterations = None def _step(self): if self._iterations is not None: self._iterations -= 1 if self._iterations == 0: self.stop() else: self._interval *= 2 self._timer = reactor.callLater(self._interval, self._step) NotificationCenter().post_notification('ExponentialTimerDidTimeout', sender=self) @property def active(self): return self._timer is not None def start(self, base_interval, immediate=False, iterations=None, time_limit=None): assert base_interval > 0 assert iterations is None or iterations > 0 assert time_limit is None or time_limit > 0 if self._timer is not None: self.stop() self._interval = base_interval / 2.0 if immediate else base_interval self._iterations = iterations if time_limit is not None: self._limit_timer = reactor.callLater(time_limit, self.stop) self._timer = reactor.callLater(0 if immediate else base_interval, self._step) def stop(self): if self._timer is not None and self._timer.active(): self._timer.cancel() if self._limit_timer is not None and self._limit_timer.active(): self._limit_timer.cancel() self._timer = None self._limit_timer = None # Utility objects # class UserInfo(object, metaclass=Singleton): def __repr__(self): return ''.format(self) @property def username(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_name - return name.decode(sys.getfilesystemencoding()) + return name.encode(sys.getfilesystemencoding()) @property def fullname(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_gecos.split(',', 1)[0] or pwd.getpwuid(os.getuid()).pw_name - return name.decode(sys.getfilesystemencoding()) + return name.encode(sys.getfilesystemencoding()) user_info = UserInfo() del UserInfo def execute_once(func): def wrapper(*args, **kwargs): if not wrapper.has_run: wrapper.has_run = True return func(*args, **kwargs) wrapper.has_run = False return wrapper