예제 #1
0
    def __init__(self,
                 drop_late_bundles=False,
                 timeout=0.01,
                 advanced_matching=False,
                 encoding='',
                 encoding_errors='strict',
                 default_handler=None,
                 intercept_errors=True):
        """Create an OSCThreadServer.

        - `timeout` is a number of seconds used as a time limit for
          select() calls in the listening thread, optiomal, defaults to
          0.01.
        - `drop_late_bundles` instruct the server not to dispatch calls
          from bundles that arrived after their timetag value.
          (optional, defaults to False)
        - `advanced_matching` (defaults to False), setting this to True
          activates the pattern matching part of the specification, let
          this to False if you don't need it, as it triggers a lot more
          computation for each received message.
        - `encoding` if defined, will be used to encode/decode all
          strings sent/received to/from unicode/string objects, if left
          empty, the interface will only accept bytes and return bytes
          to callback functions.
        - `encoding_errors` if `encoding` is set, this value will be
          used as `errors` parameter in encode/decode calls.
        - `default_handler` if defined, will be used to handle any
          message that no configured address matched, the received
          arguments will be (address, *values).
        - `intercept_errors`, if True, means that exception raised by
          callbacks will be intercepted and logged. If False, the handler
          thread will terminate mostly silently on such exceptions.
        """
        self._must_loop = True
        self._termination_event = Event()

        self.addresses = {}
        self.sockets = []
        self.timeout = timeout
        self.default_socket = None
        self.drop_late_bundles = drop_late_bundles
        self.advanced_matching = advanced_matching
        self.encoding = encoding
        self.encoding_errors = encoding_errors
        self.default_handler = default_handler
        self.intercept_errors = intercept_errors

        self.stats_received = Stats()
        self.stats_sent = Stats()

        t = Thread(target=self._run_listener)
        t.daemon = True
        t.start()
        self._thread = t

        self._smart_address_cache = {}
        self._smart_part_cache = {}
예제 #2
0
def test_to_tuple_stats():
    tpl = Stats(
        calls=1, bytes=2, params=3, types=Counter("import antigravity")
    ).to_tuple()

    assert tpl[:3] == (1, 2, 3,)
    assert set(tpl[3]) == set('import antigravity')
예제 #3
0
def test_repr_stats():
    r = repr(Stats(calls=0, bytes=1, params=2, types=Counter('abc')))
    assert r == dedent(
    '''
    Stats:
        calls: 0
        bytes: 1
        params: 2
        types:
            a: 1
            b: 1
            c: 1
    ''').strip()
예제 #4
0
def _send(options):
    def _parse(s):
        try:
            return literal_eval(s)
        except:
            return s

    stats = Stats()
    for i in range(options.repeat):
        stats += send_message(options.address,
                              [_parse(x) for x in options.message],
                              options.host,
                              options.port,
                              safer=options.safer,
                              encoding=options.encoding,
                              encoding_errors=options.encoding_errors)
    print(stats)
예제 #5
0
    def __init__(self,
                 address,
                 port,
                 sock=None,
                 encoding='',
                 encoding_errors='strict'):
        """Create an OSCClient.

        `address` and `port` are the destination of messages sent
        by this client. See `send_message` and `send_bundle` documentation
        for more information.
        """
        self.address = address
        self.port = port
        self.sock = sock or SOCK
        self.encoding = encoding
        self.encoding_errors = encoding_errors
        self.stats = Stats()
예제 #6
0
def format_bundle(data, timetag=None, encoding='', encoding_errors='strict'):
    """Create a bundle from a list of (address, values) tuples.

    String values will be encoded using `encoding` or must be provided
    as bytes.
    `encoding_errors` will be used to manage encoding errors.
    """
    timetag = time_to_timetag(timetag)
    bundle = [pack('8s', b'#bundle\0')]
    bundle.append(TIME_TAG.pack(*timetag))

    stats = Stats()
    for address, values in data:
        msg, st = format_message(address,
                                 values,
                                 encoding='',
                                 encoding_errors=encoding_errors)
        bundle.append(pack('>i', len(msg)))
        bundle.append(msg)
        stats += st

    return b''.join(bundle), stats
예제 #7
0
class OSCThreadServer(object):
    """A thread-based OSC server.

    Listens for osc messages in a thread, and dispatches the messages
    values to callbacks from there.

    The '/_oscpy/' namespace is reserved for metadata about the OSCPy
    internals, please see package documentation for further details.
    """

    def __init__(
        self, drop_late_bundles=False, timeout=0.01, advanced_matching=False,
        encoding='', encoding_errors='strict', default_handler=None
    ):
        """Create an OSCThreadServer.

        - `timeout` is a number of seconds used as a time limit for
          select() calls in the listening thread, optiomal, defaults to
          0.01.
        - `drop_late_bundles` instruct the server not to dispatch calls
          from bundles that arrived after their timetag value.
          (optional, defaults to False)
        - `advanced_matching` (defaults to False), setting this to True
          activates the pattern matching part of the specification, let
          this to False if you don't need it, as it triggers a lot more
          computation for each received message.
        - `encoding` if defined, will be used to encode/decode all
          strings sent/received to/from unicode/string objects, if left
          empty, the interface will only accept bytes and return bytes
          to callback functions.
        - `encoding_errors` if `encoding` is set, this value will be
          used as `errors` parameter in encode/decode calls.
        - `default_handler` if defined, will be used to handle any
          message that no configured address matched, the received
          arguments will be (address, *values).
        """
        self.addresses = {}
        self.sockets = []
        self.timeout = timeout
        self.default_socket = None
        self.drop_late_bundles = drop_late_bundles
        self.advanced_matching = advanced_matching
        self.encoding = encoding
        self.encoding_errors = encoding_errors
        self.default_handler = default_handler

        self.stats_received = Stats()
        self.stats_sent = Stats()

        t = Thread(target=self._listen)
        t.daemon = True
        t.start()

        self._smart_address_cache = {}
        self._smart_part_cache = {}

    def bind(self, address, callback, sock=None, get_address=False):
        """Bind a callback to an osc address.

        A socket in the list of existing sockets of the server can be
        given. If no socket is provided, the default socket of the
        server is used, if no default socket has been defined, a
        RuntimeError is raised.

        Multiple callbacks can be bound to the same address.
        """
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        if isinstance(address, UNICODE) and self.encoding:
            address = address.encode(
                self.encoding, errors=self.encoding_errors)

        if self.advanced_matching:
            address = self.create_smart_address(address)

        callbacks = self.addresses.get((sock, address), [])
        cb = (callback, get_address)
        if cb not in callbacks:
            callbacks.append(cb)
        self.addresses[(sock, address)] = callbacks

    def create_smart_address(self, address):
        """Create an advanced matching address from a string.

        The address will be split by '/' and each part will be converted
        into a regexp, using the rules defined in the OSC specification.
        """
        cache = self._smart_address_cache

        if address in cache:
            return cache[address]

        else:
            parts = address.split(b'/')
            smart_parts = tuple(
                re.compile(self._convert_part_to_regex(part)) for part in parts
            )
            cache[address] = smart_parts
            return smart_parts

    def _convert_part_to_regex(self, part):
        cache = self._smart_part_cache

        if part in cache:
            return cache[part]

        else:
            r = [b'^']
            for i, _ in enumerate(part):
                # getting a 1 char byte string instead of an int in
                # python3
                c = part[i:i + 1]
                if c == b'?':
                    r.append(b'.')
                elif c == b'*':
                    r.append(b'.*')
                elif c == b'[':
                    r.append(b'[')
                elif c == b'!' and r and r[-1] == b'[':
                    r.append(b'^')
                elif c == b']':
                    r.append(b']')
                elif c == b'{':
                    r.append(b'(')
                elif c == b',':
                    r.append(b'|')
                elif c == b'}':
                    r.append(b')')
                else:
                    r.append(c)

            r.append(b'$')

            smart_part = re.compile(b''.join(r))

            cache[part] = smart_part
            return smart_part

    def unbind(self, address, callback, sock=None):
        """Unbind a callback from an OSC address.

        See `bind` for `sock` documentation.
        """
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        if isinstance(address, UNICODE) and self.encoding:
            address = address.encode(
                self.encoding, errors=self.encoding_errors)

        callbacks = self.addresses.get((sock, address), [])
        to_remove = []
        for cb in callbacks:
            if cb[0] == callback:
                to_remove.append(cb)

        while to_remove:
            callbacks.remove(to_remove.pop())

        self.addresses[(sock, address)] = callbacks

    def listen(
        self, address='localhost', port=0, default=False, family='inet'
    ):
        """Start listening on an (address, port).

        - if `port` is 0, the system will allocate a free port
        - if `default` is True, the instance will save this socket as the
          default one for subsequent calls to methods with an optional socket
        - `family` accepts the 'unix' and 'inet' values, a socket of the
          corresponding type will be created.
          If family is 'unix', then the address must be a filename, the
          `port` value won't be used. 'unix' sockets are not defined on
          Windows.

        The socket created to listen is returned, and can be used later
        with methods accepting the `sock` parameter.
        """
        if family == 'unix':
            family_ = socket.AF_UNIX
        elif family == 'inet':
            family_ = socket.AF_INET
        else:
            raise ValueError(
                "Unknown socket family, accepted values are 'unix' and 'inet'"
            )

        sock = socket.socket(family_, socket.SOCK_DGRAM)
        if family == 'unix':
            addr = address
        else:
            addr = (address, port)
        sock.bind(addr)
        self.sockets.append(sock)
        if default and not self.default_socket:
            self.default_socket = sock
        elif default:
            raise RuntimeError(
                'Only one default socket authorized! Please set '
                'default=False to other calls to listen()'
            )
        self.bind_meta_routes(sock)
        return sock

    def close(self, sock=None):
        """Close a socket opened by the server."""
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        if platform != 'win32' and sock.family == socket.AF_UNIX:
            os.unlink(sock.getsockname())
        else:
            sock.close()

        if sock == self.default_socket:
            self.default_socket = None

    def getaddress(self, sock=None):
        """Wrap call to getsockname.

        If `sock` is None, uses the default socket for the server.

        Returns (ip, port) for an inet socket, or filename for an unix
        socket.
        """
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        return sock.getsockname()

    def stop(self, s=None):
        """Close and remove a socket from the server's sockets.

        If `sock` is None, uses the default socket for the server.

        """
        if not s and self.default_socket:
            s = self.default_socket

        if s in self.sockets:
            read = select([s], [], [], 0)
            s.close()
            if s in read:
                s.recvfrom(65535)
            self.sockets.remove(s)
        else:
            raise RuntimeError('{} is not one of my sockets!'.format(s))

    def stop_all(self):
        """Call stop on all the existing sockets."""
        for s in self.sockets[:]:
            self.stop(s)
        sleep(10e-9)

    def _listen(self):
        """(internal) Busy loop to listen for events.

        This method is called in a thread by the `listen` method, and
        will be the one actually listening for messages on the server's
        sockets, and calling the callbacks when messages are received.
        """
        match = self._match_address
        advanced_matching = self.advanced_matching
        addresses = self.addresses
        stats = self.stats_received

        while True:
            drop_late = self.drop_late_bundles
            if not self.sockets:
                sleep(.01)
                continue
            else:
                try:
                    read, write, error = select(self.sockets, [], [], self.timeout)
                except (ValueError, socket.error):
                    continue

            for sender_socket in read:
                try:
                    data, sender = sender_socket.recvfrom(65535)
                except:
                    continue

                for address, tags, values, offset in read_packet(
                    data, drop_late=drop_late, encoding=self.encoding,
                    encoding_errors=self.encoding_errors
                ):
                    stats.calls += 1
                    stats.bytes += offset
                    stats.params += len(values)
                    stats.types.update(tags)

                    matched = False
                    if advanced_matching:
                        for sock, addr in addresses:
                            if sock == sender_socket and match(addr, address):
                                matched = True
                                for cb, get_address in addresses[(sock, addr)]:
                                    if get_address:
                                        cb(address, *values)
                                    else:
                                        cb(*values)

                    else:
                        if (sender_socket, address) in addresses:
                            matched = True

                        for cb, get_address in addresses.get(
                            (sender_socket, address), []
                        ):
                            if get_address:
                                cb(address, *values)
                            else:
                                cb(*values)

                    if not matched and self.default_handler:
                        self.default_handler(address, *values)

    @staticmethod
    def _match_address(smart_address, target_address):
        """(internal) Check if provided `smart_address` matches address.

        A `smart_address` is a list of regexps to match
        against the parts of the `target_address`.
        """
        target_parts = target_address.split(b'/')
        if len(target_parts) != len(smart_address):
            return False

        return all(
            model.match(part)
            for model, part in
            zip(smart_address, target_parts)
        )

    def send_message(
        self, osc_address, values, ip_address, port, sock=None, safer=False
    ):
        """Shortcut to the client's `send_message` method.

        Use the default_socket of the server by default.
        See `client.send_message` for more info about the parameters.
        """
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        stats = send_message(
            osc_address,
            values,
            ip_address,
            port,
            sock=sock,
            safer=safer,
            encoding=self.encoding,
            encoding_errors=self.encoding_errors
        )
        self.stats_sent += stats
        return stats

    def send_bundle(
        self, messages, ip_address, port, timetag=None, sock=None, safer=False
    ):
        """Shortcut to the client's `send_bundle` method.

        Use the `default_socket` of the server by default.
        See `client.send_bundle` for more info about the parameters.
        """
        if not sock and self.default_socket:
            sock = self.default_socket
        elif not sock:
            raise RuntimeError('no default socket yet and no socket provided')

        stats = send_bundle(
            messages,
            ip_address,
            port,
            sock=sock,
            safer=safer,
            encoding=self.encoding,
            encoding_errors=self.encoding_errors
        )
        self.stats_sent += stats
        return stats

    def get_sender(self):
        """Return the socket, ip and port of the message that is currently being managed.
        Warning::

            this method should only be called from inside the handling
            of a message (i.e, inside a callback).
        """
        frames = inspect.getouterframes(inspect.currentframe())
        for frame, filename, _, function, _, _ in frames:
            if function == '_listen' and __FILE__.startswith(filename):
                break
        else:
            raise RuntimeError('get_sender() not called from a callback')

        sock = frame.f_locals.get('sender_socket')
        address, port = frame.f_locals.get('sender')
        return sock, address, port

    def answer(
        self, address=None, values=None, bundle=None, timetag=None,
        safer=False, port=None
    ):
        """Answers a message or bundle to a client.

        This method can only be called from a callback, it will lookup
        the sender of the packet that triggered the callback, and send
        the given message or bundle to it.

        `timetag` is only used if `bundle` is True.
        See `send_message` and `send_bundle` for info about the parameters.

        Only one of `values` or `bundle` should be defined, if `values`
        is defined, `send_message` is used with it, if `bundle` is
        defined, `send_bundle` is used with its value.
        """
        if not values:
            values = []

        sock, ip_address, response_port = self.get_sender()

        if port is not None:
            response_port = port

        if bundle:
            return self.send_bundle(
                bundle, ip_address, response_port, timetag=timetag, sock=sock,
                safer=safer
            )
        else:
            return self.send_message(
                address, values, ip_address, response_port, sock=sock
            )

    def address(self, address, sock=None, get_address=False):
        """Decorate functions to bind them from their definition.

        `address` is the osc address to bind to the callback.
        if `get_address` is set to True, the first parameter the
        callback will receive will be the address that matched (useful
        with advanced matching).

        example:
            server = OSCThreadServer()
            server.listen('localhost', 8000, default=True)

            @server.address(b'/printer')
            def printer(values):
                print(values)

            send_message(b'/printer', [b'hello world'])

        note:
            This won't work on methods as it'll call them as normal
            functions, and the callback won't get a `self` argument.

            To bind a method use the `address_method` decorator.
        """
        def decorator(callback):
            self.bind(address, callback, sock, get_address=get_address)
            return callback

        return decorator

    def address_method(self, address, sock=None, get_address=False):
        """Decorate methods to bind them from their definition.

        The class defining the method must itself be decorated with the
        `ServerClass` decorator, the methods will be bound to the
        address when the class is instantiated.

        See `address` for more information about the parameters.

        example:

            osc = OSCThreadServer()
            osc.listen(default=True)

            @ServerClass
            class MyServer(object):

                @osc.address_method(b'/test')
                def success(self, *args):
                    print("success!", args)
        """
        def decorator(decorated):
            decorated._address = (self, address, sock, get_address)
            return decorated

        return decorator

    def bind_meta_routes(self, sock=None):
        """This module implements osc routes to probe the internal state of a
        live OSCPy server. These routes are placed in the /_oscpy/ namespace,
        and provide information such as the version, the existing routes, and
        usage statistics of the server over time.

        These requests will be sent back to the client's address/port that sent
        them, with the osc address suffixed with '/answer'.

        examples:
            '/_oscpy/version' -> '/_oscpy/version/answer'
            '/_oscpy/stats/received' -> '/_oscpy/stats/received/answer'

        messages to these routes require a port number as argument, to
        know to which port to send to.
        """
        self.bind(b'/_oscpy/version', self._get_version, sock=sock)
        self.bind(b'/_oscpy/routes', self._get_routes, sock=sock)
        self.bind(b'/_oscpy/stats/received', self._get_stats_received, sock=sock)
        self.bind(b'/_oscpy/stats/sent', self._get_stats_sent, sock=sock)

    def _get_version(self, port, *args):
        self.answer(
            b'/_oscpy/version/answer',
            (__version__, ),
            port=port
        )

    def _get_routes(self, port, *args):
        self.answer(
            b'/_oscpy/routes/answer',
            [a[1] for a in self.addresses],
            port=port
        )

    def _get_stats_received(self, port, *args):
        self.answer(
            b'/_oscpy/stats/received/answer',
            self.stats_received.to_tuple(),
            port=port
        )

    def _get_stats_sent(self, port, *args):
        self.answer(
            b'/_oscpy/stats/sent/answer',
            self.stats_sent.to_tuple(),
            port=port
        )
예제 #8
0
def test_create_stats():
    stats = Stats(calls=1, bytes=2, params=3, types=Counter('abc'))
    assert stats.calls == 1
    assert stats.bytes == 2
    assert stats.params == 3
    assert stats.types['a'] == 1
예제 #9
0
def test_compare_stats():
    assert Stats(
        calls=1, bytes=2, params=3, types=Counter('abc')
    ) == Stats(
        calls=1, bytes=2, params=3, types=Counter('abc')
    )
예제 #10
0
def test_add_stats():
    stats = Stats(calls=1) + Stats(calls=3, bytes=2)
    assert stats.calls == 4
    assert stats.bytes == 2
예제 #11
0
def format_message(address, values, encoding='', encoding_errors='strict'):
    """Create a message."""
    tags = [b',']
    fmt = []

    encode_cache = {}

    lv = 0
    count = Counter()

    for value in values:
        lv += 1
        cls_or_value, writer = None, None
        for cls_or_value, writer in WRITERS:
            if (cls_or_value is value or isinstance(cls_or_value, type)
                    and isinstance(value, cls_or_value)):
                break
        else:
            raise TypeError(
                u'unable to find a writer for value {}, type not in: {}.'.
                format(value, [x[0] for x in WRITERS]))

        if cls_or_value == UNICODE:
            if not encoding:
                raise TypeError(
                    u"Can't format unicode string without encoding")

            cls_or_value = bytes
            value = (encode_cache[value]
                     if value in encode_cache else encode_cache.setdefault(
                         value, value.encode(encoding,
                                             errors=encoding_errors)))

        assert cls_or_value, writer

        tag, v_fmt = writer
        if b'%i' in v_fmt:
            v_fmt = v_fmt % padded(len(value) + 1, PADSIZES[cls_or_value])

        tags.append(tag)
        fmt.append(v_fmt)
        count[tag.decode('utf8')] += 1

    fmt = b''.join(fmt)
    tags = b''.join(tags + [NULL])

    if encoding and isinstance(address, UNICODE):
        address = address.encode(encoding, errors=encoding_errors)

    if not address.endswith(NULL):
        address += NULL

    fmt = b'>%is%is%s' % (padded(len(address)), padded(len(tags)), fmt)
    message = pack(
        fmt, address, tags,
        *((encode_cache.get(v) +
           NULL if isinstance(v, UNICODE) and encoding else
           (v + NULL) if t in (
               b's',
               b'b') else format_midi(v) if isinstance(v, MidiTuple) else v)
          for t, v in izip(tags[1:], values)))
    return message, Stats(1, len(message), lv, count)