Ejemplo n.º 1
0
    def broadcast(cls, channels, trigger=None, originating_client=None):
        """
        Trigger all subscriptions on the given channel(s).  This method is
        called in the "broadcaster" thread, which means that all subscription
        updates happen in the same thread.

        Callers can pass an "originating_client" id, which will prevent data
        from being pushed to those clients.  This is useful in cases like this:
        -> a Javascipt application makes a call like "ecard.delete"
        -> not wanting to wait for a subscription update, the Javascript app
           preemptively updates its local data store to remove the item
        -> the response to the delete call comes back as a success
        -> because the local data store was already updated, there's no need
           for this client to get a subscription update

        Callers can pass a "trigger" field, which will be included in the
        subscription update message as the reason for the update.  This doesn't
        affect anything, but might be useful for logging.
        """
        triggered = set()
        for channel in sideboard.lib.listify(channels):
            for websocket, clients in list(cls.subscriptions[channel].items()):
                if websocket.is_closed:
                    websocket.unsubscribe_all()
                else:
                    for client, callbacks in clients.copy().items():
                        if client != originating_client:
                            for callback in callbacks:
                                triggered.add((websocket, client, callback))

        for websocket, client, callback in triggered:
            try:
                websocket.trigger(client=client, callback=callback, trigger=trigger)
            except:
                log.warn('ignoring unexpected trigger error', exc_info=True)
Ejemplo n.º 2
0
def _run_shutdown():
    for priority, functions in sorted(_shutdown_registry.items()):
        for func in functions:
            try:
                func()
            except Exception:
                log.warn('Ignored exception during shutdown', exc_info=True)
Ejemplo n.º 3
0
def _run_shutdown():
    for priority, functions in sorted(_shutdown_registry.items()):
        for func in functions:
            try:
                func()
            except Exception:
                log.warn('Ignored exception during shutdown', exc_info=True)
Ejemplo n.º 4
0
    def subscribe(self, callback, method, *args, **kwargs):
        """
        Send a websocket request which you expect to subscribe you to a channel
        with a callback which will be called every time there is new data, and
        return the client id which uniquely identifies this subscription.

        Callback may be either a function or a dictionary in the form
        {
            'callback': <function>,
            'errback': <function>,   # optional
            'paramback: <function>,  # optional
            'client': <string>       # optional
        }
        Both callback and errback take a single argument; for callback, this is
        the return value of the method, for errback it is the error message
        returning.  If no errback is specified, we will log errors at the ERROR
        level and do nothing further.

        The paramback function exists for subscriptions where we might want to
        pass different parameters every time we reconnect.  This might be used
        for e.g. time-based parameters.  This function takes no arguments and
        returns the parameters which should be passed every time we connect
        and fire (or re-fire) all of our subscriptions.

        The client id is automatically generated if omitted, and you should not
        set this yourself unless you really know what you're doing.

        The positional and keyword arguments passed to this function will be
        used as the arguments to the remote method, unless paramback is passed,
        in which case that will be used to generate the params, and args/kwargs
        will be ignored.
        """
        client = self._next_id('client')
        if isinstance(callback, Mapping):
            assert 'callback' in callback, 'callback is required'
            client = callback.setdefault('client', client)
            self._callbacks[client] = callback
        else:
            self._callbacks[client] = {'client': client, 'callback': callback}

        paramback = self._callbacks[client].get('paramback')
        params = self.preprocess(
            method,
            paramback() if paramback else (args or kwargs))
        self._callbacks[client].setdefault(
            'errback',
            lambda result: log.error('{}(*{}, **{}) returned an error: {!r}',
                                     method, args, kwargs, result))
        self._callbacks[client].update({'method': method, 'params': params})

        try:
            self._send(method=method, params=params, client=client)
        except:
            log.warn(
                'initial subscription to {} at {!r} failed, will retry on reconnect',
                method, self.url)

        return client
Ejemplo n.º 5
0
    def subscribe(self, callback, method, *args, **kwargs):
        """
        Send a websocket request which you expect to subscribe you to a channel
        with a callback which will be called every time there is new data, and
        return the client id which uniquely identifies this subscription.

        Callback may be either a function or a dictionary in the form
        {
            'callback': <function>,
            'errback': <function>,   # optional
            'paramback: <function>,  # optional
            'client': <string>       # optional
        }
        Both callback and errback take a single argument; for callback, this is
        the return value of the method, for errback it is the error message
        returning.  If no errback is specified, we will log errors at the ERROR
        level and do nothing further.

        The paramback function exists for subscriptions where we might want to
        pass different parameters every time we reconnect.  This might be used
        for e.g. time-based parameters.  This function takes no arguments and
        returns the parameters which should be passed every time we connect
        and fire (or re-fire) all of our subscriptions.

        The client id is automatically generated if omitted, and you should not
        set this yourself unless you really know what you're doing.

        The positional and keyword arguments passed to this function will be
        used as the arguments to the remote method, unless paramback is passed,
        in which case that will be used to generate the params, and args/kwargs
        will be ignored.
        """
        client = self._next_id('client')
        if isinstance(callback, Mapping):
            assert 'callback' in callback, 'callback is required'
            client = callback.setdefault('client', client)
            self._callbacks[client] = callback
        else:
            self._callbacks[client] = {
                'client': client,
                'callback': callback
            }

        paramback = self._callbacks[client].get('paramback')
        params = self.preprocess(method, paramback() if paramback else (args or kwargs))
        self._callbacks[client].setdefault('errback', lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result))
        self._callbacks[client].update({
            'method': method,
            'params': params
        })

        try:
            self._send(method=method, params=params, client=client)
        except:
            log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url)

        return client
Ejemplo n.º 6
0
 def _send(self, **kwargs):
     log.debug('sending {}', kwargs)
     with self._lock:
         assert self.connected, 'tried to send data on closed websocket {!r}'.format(self.url)
         try:
             return self.ws.send(kwargs)
         except:
             log.warn('failed to send {!r} on {!r}, closing websocket and will attempt to reconnect', kwargs, self.url)
             self.ws.close()
             raise
Ejemplo n.º 7
0
 def _send(self, **kwargs):
     log.debug('sending {}', kwargs)
     with self._lock:
         assert self.connected, 'tried to send data on closed websocket {!r}'.format(self.url)
         try:
             return self.ws.send(kwargs)
         except:
             log.warn('failed to send {!r} on {!r}, closing websocket and will attempt to reconnect', kwargs, self.url)
             self.ws.close()
             raise
Ejemplo n.º 8
0
 def error(code, message):
     body = {
         'jsonrpc': '2.0',
         'id': id,
         'error': {
             'code': code,
             'message': message
         }
     }
     log.warn('returning error message: {!r}', body)
     return body
Ejemplo n.º 9
0
    def internal_action(self, action, client, callback):
        """
        Sideboard currently supports both method calls and "internal actions"
        which affect the state of the websocket connection itself.  This
        implements the command-dispatch pattern to perform the given action and
        raises an exception if that action doesn't exist.

        The only action currently implemented is "unsubscribe".
        """
        if action == 'unsubscribe':
            self.unsubscribe(client)
        elif action is not None:
            log.warn('unknown action {!r}', action)
Ejemplo n.º 10
0
    def internal_action(self, action, client, callback):
        """
        Sideboard currently supports both method calls and "internal actions"
        which affect the state of the websocket connection itself.  This
        implements the command-dispatch pattern to perform the given action and
        raises an exception if that action doesn't exist.

        The only action currently implemented is "unsubscribe".
        """
        if action == 'unsubscribe':
            self.unsubscribe(client)
        elif action is not None:
            log.warn('unknown action {!r}', action)
Ejemplo n.º 11
0
 def _reconnect(self):
     with self._lock:
         assert not self.connected, 'connection is still active'
         try:
             self.ws = self.WebSocketDispatcher(self._dispatcher, self.url, ssl_opts=self.ssl_opts)
             self.ws.connect()
         except Exception as e:
             log.warn('failed to connect to {}: {}', self.url, str(e))
             self._last_reconnect_attempt = datetime.now()
             self._reconnect_attempts += 1
         else:
             self._reconnect_attempts = 0
             self._refire_subscriptions()
Ejemplo n.º 12
0
 def _reconnect(self):
     with self._lock:
         assert not self.connected, 'connection is still active'
         try:
             self.ws = self.WebSocketDispatcher(self._dispatcher, self.url, ssl_opts=self.ssl_opts)
             self.ws.connect()
         except Exception as e:
             log.warn('failed to connect to {}: {}', self.url, str(e))
             self._last_reconnect_attempt = datetime.now()
             self._reconnect_attempts += 1
         else:
             self._reconnect_attempts = 0
             self._refire_subscriptions()
Ejemplo n.º 13
0
 def refresh(self):
     """
     Sometimes we want to manually re-fire all of our subscription methods to
     get the latest data.  This is useful in cases where the remote server
     isn't necessarily programmed to always push the latest data as soon as
     it's available, usually for performance reasons.  This method allows the
     client to get the latest data more often than the server is programmed
     to provide it.
     """
     for ws in self.websockets.values():
         try:
             self._callback(self.ws.call(self.method, *self.args, **self.kwargs), ws)
         except:
             log.warn('failed to fetch latest data from {} on {}', self.method, ws.url)
Ejemplo n.º 14
0
    def broadcast(cls, channels, trigger=None, originating_client=None):
        triggered = set()
        for channel in sideboard.lib.listify(channels):
            for websocket, clients in cls.subscriptions[channel].items():
                for client, callbacks in clients.copy().items():
                    if client != originating_client:
                        for callback in callbacks:
                            triggered.add((websocket, client, callback))

        for websocket, client, callback in triggered:
            try:
                websocket.trigger(client=client, callback=callback, trigger=trigger)
            except:
                log.warn('ignoring unexpected trigger error', exc_info=True)
Ejemplo n.º 15
0
    def broadcast(cls, channels, trigger=None, originating_client=None):
        triggered = set()
        for channel in sideboard.lib.listify(channels):
            for websocket, clients in cls.subscriptions[channel].items():
                for client, callbacks in clients.copy().items():
                    if client != originating_client:
                        for callback in callbacks:
                            triggered.add((websocket, client, callback))

        for websocket, client, callback in triggered:
            try:
                websocket.trigger(client=client,
                                  callback=callback,
                                  trigger=trigger)
            except:
                log.warn('ignoring unexpected trigger error', exc_info=True)
Ejemplo n.º 16
0
 def refresh(self):
     """
     Sometimes we want to manually re-fire all of our subscription methods to
     get the latest data.  This is useful in cases where the remote server
     isn't necessarily programmed to always push the latest data as soon as
     it's available, usually for performance reasons.  This method allows the
     client to get the latest data more often than the server is programmed
     to provide it.
     """
     for ws in self.websockets.values():
         try:
             self._callback(
                 self.ws.call(self.method, *self.args, **self.kwargs), ws)
         except:
             log.warn('failed to fetch latest data from {} on {}',
                      self.method, ws.url)
Ejemplo n.º 17
0
 def _reconnect(self):
     with self._lock:
         assert not self.connected, 'connection is still active'
         try:
             self.ws = self.WebSocketDispatcher(self._dispatcher, self.url)
             self.ws.connect()
         except Exception as e:
             log.warn('failed to connect to {}: {}', self.url, str(e))
             self._last_reconnect_attempt = datetime.now()
             self._reconnect_attempts += 1
         else:
             self._reconnect_attempts = 0
             try:
                 for cb in self._callbacks.values():
                     if 'client' in cb:
                         self._send(method=cb['method'], params=cb['params'], client=cb['client'])
             except:
                 pass  # self.send() already closes and logs on error
Ejemplo n.º 18
0
 def connect(self, max_wait=0):
     """
     Start the background threads which connect this websocket and handle RPC
     dispatching.  This method is safe to call even if the websocket is already
     connected.  You may optionally pass a max_wait parameter if you want to
     wait for up to that amount of time for the connection to go through; if
     that amount of time elapses without successfully connecting, a warning
     message is logged.
     """
     self._checker.start()
     self._dispatcher.start()
     for i in range(10 * max_wait):
         if not self.connected:
             stopped.wait(0.1)
         else:
             break
     else:
         if max_wait:
             log.warn('websocket {!r} not connected after {} seconds', self.url, max_wait)
Ejemplo n.º 19
0
 def connect(self, max_wait=0):
     """
     Start the background threads which connect this websocket and handle RPC
     dispatching.  This method is safe to call even if the websocket is already
     connected.  You may optionally pass a max_wait parameter if you want to
     wait for up to that amount of time for the connection to go through; if
     that amount of time elapses without successfully connecting, a warning
     message is logged.
     """
     self._checker.start()
     self._dispatcher.start()
     for i in range(10 * max_wait):
         if not self.connected:
             stopped.wait(0.1)
         else:
             break
     else:
         if max_wait:
             log.warn('websocket {!r} not connected after {} seconds', self.url, max_wait)
Ejemplo n.º 20
0
 def subscribe(self, callback, method, *args, **kwargs):
     """
     Send a websocket request which you expect to subscribe you to a channel
     with a callback which will be called every time there is new data, and
     return the client id which uniquely identifies this subscription.
     
     Callback may be either a function or a dictionary in the form
     {
         'callback': <function>,
         'errback': <function>
     }
     Both callback and errback take a single argument; for callback, this is
     the return value of the method, for errback it is the error message
     returning.  If no errback is specified, we will log errors at the ERROR
     level and do nothing further.
     
     The positional and keyword arguments passed to this function will be
     used as the arguments to the remote method.
     """
     client = self._next_id('client')
     if isinstance(callback, Mapping):
         assert 'callback' in callback and 'errback' in callback, 'callback and errback are required'
         client = callback.setdefault('client', client)
         self._callbacks[client] = callback
     else:
         self._callbacks[client] = {
             'client': client,
             'callback': callback,
             'errback': lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result)
         }
     self._callbacks[client].update({
         'method': method,
         'params': args or kwargs
     })
     try:
         self._send(method=method, params=args or kwargs, client=client)
     except:
         log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url)
     return client
Ejemplo n.º 21
0
 def subscribe(self, callback, method, *args, **kwargs):
     """
     Send a websocket request which you expect to subscribe you to a channel
     with a callback which will be called every time there is new data, and
     return the client id which uniquely identifies this subscription.
     
     Callback may be either a function or a dictionary in the form
     {
         'callback': <function>,
         'errback': <function>
     }
     Both callback and errback take a single argument; for callback, this is
     the return value of the method, for errback it is the error message
     returning.  If no errback is specified, we will log errors at the ERROR
     level and do nothing further.
     
     The positional and keyword arguments passed to this function will be
     used as the arguments to the remote method.
     """
     client = self._next_id('client')
     if isinstance(callback, dict):
         assert 'callback' in callback and 'errback' in callback, 'callback and errback are required'
         client = callback.setdefault('client', client)
         self._callbacks[client] = callback
     else:
         self._callbacks[client] = {
             'client': client,
             'callback': callback,
             'errback': lambda result: log.error('{}(*{}, **{}) returned an error: {!r}', method, args, kwargs, result)
         }
     self._callbacks[client].update({
         'method': method,
         'params': args or kwargs
     })
     try:
         self._send(method=method, params=args or kwargs, client=client)
     except:
         log.warn('initial subscription to {} at {!r} failed, will retry on reconnect', method, self.url)
     return client
Ejemplo n.º 22
0
    def broadcast(cls, channels, trigger=None, originating_client=None):
        """
        Trigger all subscriptions on the given channel(s).  This method is
        called in the "broadcaster" thread, which means that all subscription
        updates happen in the same thread.

        Callers can pass an "originating_client" id, which will prevent data
        from being pushed to those clients.  This is useful in cases like this:
        -> a Javascipt application makes a call like "ecard.delete"
        -> not wanting to wait for a subscription update, the Javascript app
           preemptively updates its local data store to remove the item
        -> the response to the delete call comes back as a success
        -> because the local data store was already updated, there's no need
           for this client to get a subscription update

        Callers can pass a "trigger" field, which will be included in the
        subscription update message as the reason for the update.  This doesn't
        affect anything, but might be useful for logging.
        """
        triggered = set()
        for channel in sideboard.lib.listify(channels):
            for websocket, clients in list(cls.subscriptions[channel].items()):
                if websocket.is_closed:
                    websocket.unsubscribe_all()
                else:
                    for client, callbacks in clients.copy().items():
                        if client != originating_client:
                            for callback in callbacks:
                                triggered.add((websocket, client, callback))

        for websocket, client, callback in triggered:
            try:
                websocket.trigger(client=client,
                                  callback=callback,
                                  trigger=trigger)
            except:
                log.warn('ignoring unexpected trigger error', exc_info=True)
Ejemplo n.º 23
0
 def internal_action(self, action, client, callback):
     if action == 'unsubscribe':
         self.unsubscribe(client)
     elif action is not None:
         log.warn('unknown action {!r}', action)
Ejemplo n.º 24
0
 def error(code, message):
     body = {"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}}
     log.warn("returning error message: {!r}", body)
     return body
Ejemplo n.º 25
0
 def error(code, message):
     body = {'jsonrpc': '2.0', 'id': id,
             'error': {'code': code, 'message': message}}
     log.warn('returning error message: {!r}', body)
     return body
Ejemplo n.º 26
0
 def internal_action(self, action, client, callback):
     if action == 'unsubscribe':
         self.unsubscribe(client)
     elif action is not None:
         log.warn('unknown action {!r}', action)