summaryrefslogtreecommitdiff
path: root/src/leap/common/events/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/common/events/client.py')
-rw-r--r--src/leap/common/events/client.py328
1 files changed, 328 insertions, 0 deletions
diff --git a/src/leap/common/events/client.py b/src/leap/common/events/client.py
new file mode 100644
index 0000000..4ae9bff
--- /dev/null
+++ b/src/leap/common/events/client.py
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+# client.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+The client end point of the events mechanism.
+
+Clients are the communicating parties of the events mechanism. They
+communicate by sending messages to a server, which in turn redistributes
+messages to other clients.
+
+When a client registers a callback for a given signal, it also tells the
+server that it wants to be notified whenever signals of that type are sent by
+some other client.
+"""
+
+
+import logging
+import threading
+
+
+from protobuf.socketrpc import RpcService
+from leap.common.events import (
+ events_pb2 as proto,
+ server,
+ daemon,
+ mac_auth,
+)
+
+
+logger = logging.getLogger(__name__)
+
+
+# the `registered_callbacks` dictionary below should have the following
+# format:
+#
+# { event_signal: [ (uid, callback), ... ], ... }
+#
+registered_callbacks = {}
+
+
+class CallbackAlreadyRegistered(Exception):
+ """
+ Raised when trying to register an already registered callback.
+ """
+ pass
+
+
+def ensure_client_daemon():
+ """
+ Ensure the client daemon is running and listening for incoming
+ messages.
+
+ :return: the daemon instance
+ :rtype: EventsClientDaemon
+ """
+ import time
+ daemon = EventsClientDaemon.ensure(0)
+ logger.debug('ensure client daemon')
+
+ # Because we use a random port we want to wait until a port is assigned to
+ # local client daemon.
+
+ while not (EventsClientDaemon.get_instance() and
+ EventsClientDaemon.get_instance().get_port()):
+ time.sleep(0.1)
+ return daemon
+
+
+def register(signal, callback, uid=None, replace=False, reqcbk=None,
+ timeout=1000):
+ """
+ Registers a callback to be called when a specific signal event is
+ received.
+
+ Will timeout after timeout ms if response has not been received. The
+ timeout arg is only used for asynch requests. If a reqcbk callback has
+ been supplied the timeout arg is not used. The response value will be
+ returned for a synch request but nothing will be returned for an asynch
+ request.
+
+ :param signal: the signal that causes the callback to be launched
+ :type signal: int (see the `events.proto` file)
+ :param callback: the callback to be called when the signal is received
+ :type callback: function(leap.common.events.events_pb2.SignalRequest)
+ :param uid: a unique id for the callback
+ :type uid: int
+ :param replace: should an existent callback with same uid be replaced?
+ :type replace: bool
+ :param reqcbk: a callback to be called when a response from server is
+ received
+ :type reqcbk: function(proto.RegisterRequest, proto.EventResponse)
+ :param timeout: the timeout for synch calls
+ :type timeout: int
+
+ Might raise a CallbackAlreadyRegistered exception if there's already a
+ callback identified by the given uid and replace is False.
+
+ :return: the response from server for synch calls or nothing for asynch
+ calls.
+ :rtype: leap.common.events.events_pb2.EventsResponse or None
+ """
+ ensure_client_daemon() # so we can receive registered signals
+ # register callback locally
+ if signal not in registered_callbacks:
+ registered_callbacks[signal] = []
+ cbklist = registered_callbacks[signal]
+ if uid and filter(lambda (x, y): x == uid, cbklist):
+ if not replace:
+ raise CallbackAlreadyRegisteredException()
+ else:
+ registered_callbacks[signal] = filter(lambda(x, y): x != uid,
+ cbklist)
+ registered_callbacks[signal].append((uid, callback))
+ # register callback on server
+ request = proto.RegisterRequest()
+ request.event = signal
+ request.port = EventsClientDaemon.get_instance().get_port()
+ request.mac_method = mac_auth.MacMethod.MAC_NONE
+ request.mac = ""
+ service = RpcService(proto.EventsServerService_Stub,
+ server.SERVER_PORT, 'localhost')
+ logger.info(
+ "Sending registration request to server on port %s: %s",
+ server.SERVER_PORT,
+ str(request)[:40])
+ return service.register(request, callback=reqcbk, timeout=timeout)
+
+def unregister(signal, uid=None, reqcbk=None, timeout=1000):
+ """
+ Unregister a callback.
+
+ If C{uid} is specified, unregisters only the callback identified by that
+ unique id. Otherwise, unregisters all callbacks
+
+ :param signal: the signal that causes the callback to be launched
+ :type signal: int (see the `events.proto` file)
+ :param uid: a unique id for the callback
+ :type uid: int
+ :param reqcbk: a callback to be called when a response from server is
+ received
+ :type reqcbk: function(proto.UnregisterRequest, proto.EventResponse)
+ :param timeout: the timeout for synch calls
+ :type timeout: int
+
+ :return: the response from server for synch calls or nothing for asynch
+ calls or None if no callback is registered for that signal or
+ uid.
+ :rtype: leap.common.events.events_pb2.EventsResponse or None
+ """
+ if signal not in registered_callbacks or not registered_callbacks[signal]:
+ logger.warning("No callback registered for signal %d." % signal)
+ return None
+ # unregister callback locally
+ cbklist = registered_callbacks[signal]
+ if uid is not None:
+ if filter(lambda (cbkuid, _): cbkuid == uid, cbklist) == []:
+ logger.warning("No callback registered for uid %d." % st)
+ return None
+ registered_callbacks[signal] = filter(lambda(x, y): x != uid, cbklist)
+ else:
+ # exclude all callbacks for given signal
+ registered_callbacks[signal] = []
+ # unregister port in server if there are no more callbacks for this signal
+ if not registered_callbacks[signal]:
+ request = proto.UnregisterRequest()
+ request.event = signal
+ request.port = EventsClientDaemon.get_instance().get_port()
+ request.mac_method = mac_auth.MacMethod.MAC_NONE
+ request.mac = ""
+ service = RpcService(proto.EventsServerService_Stub,
+ server.SERVER_PORT, 'localhost')
+ logger.info(
+ "Sending unregistration request to server on port %s: %s",
+ server.SERVER_PORT,
+ str(request)[:40])
+ return service.unregister(request, callback=reqcbk, timeout=timeout)
+
+
+def signal(signal, content="", mac_method="", mac="", reqcbk=None,
+ timeout=1000):
+ """
+ Send `signal` event to events server.
+
+ Will timeout after timeout ms if response has not been received. The
+ timeout arg is only used for asynch requests. If a reqcbk callback has
+ been supplied the timeout arg is not used. The response value will be
+ returned for a synch request but nothing will be returned for an asynch
+ request.
+
+ :param signal: the signal that causes the callback to be launched
+ :type signal: int (see the `events.proto` file)
+ :param content: the contents of the event signal
+ :type content: str
+ :param mac_method: the method used for auth mac
+ :type mac_method: str
+ :param mac: the content of the auth mac
+ :type mac: str
+ :param reqcbk: a callback to be called when a response from server is
+ received
+ :type reqcbk: function(proto.SignalRequest, proto.EventResponse)
+ :param timeout: the timeout for synch calls
+ :type timeout: int
+
+ :return: the response from server for synch calls or nothing for asynch
+ calls.
+ :rtype: leap.common.events.events_pb2.EventsResponse or None
+ """
+ request = proto.SignalRequest()
+ request.event = signal
+ request.content = content
+ request.mac_method = mac_method
+ request.mac = mac
+ service = RpcService(proto.EventsServerService_Stub, server.SERVER_PORT,
+ 'localhost')
+ logger.info("Sending signal to server: %s", str(request)[:40])
+ return service.signal(request, callback=reqcbk, timeout=timeout)
+
+
+def ping(port, reqcbk=None, timeout=1000):
+ """
+ Ping a client running in C{port}.
+
+ :param port: the port in which the client should be listening
+ :type port: int
+ :param reqcbk: a callback to be called when a response from client is
+ received
+ :type reqcbk: function(proto.PingRequest, proto.EventResponse)
+ :param timeout: the timeout for synch calls
+ :type timeout: int
+
+ :return: the response from client for synch calls or nothing for asynch
+ calls.
+ :rtype: leap.common.events.events_pb2.EventsResponse or None
+ """
+ request = proto.PingRequest()
+ service = RpcService(
+ proto.EventsClientService_Stub,
+ port,
+ 'localhost')
+ logger.info("Pinging a client in port %d..." % port)
+ return service.ping(request, callback=reqcbk, timeout=timeout)
+
+
+class EventsClientService(proto.EventsClientService):
+ """
+ Service for receiving signal events in clients.
+ """
+
+ def __init__(self):
+ proto.EventsClientService.__init__(self)
+
+ def signal(self, controller, request, done):
+ """
+ Receive a signal and run callbacks registered for that signal.
+
+ This method is called whenever a signal request is received from
+ server.
+
+ :param controller: used to mediate a single method call
+ :type controller: protobuf.socketrpc.controller.SocketRpcController
+ :param request: the request received from the client
+ :type request: leap.common.events.events_pb2.SignalRequest
+ :param done: callback to be called when done
+ :type done: protobuf.socketrpc.server.Callback
+ """
+ logger.info('Received signal from server: %s...' % str(request)[:40])
+
+ # run registered callbacks
+ # TODO: verify authentication using mac in incoming message
+ if request.event in registered_callbacks:
+ for (_, cbk) in registered_callbacks[request.event]:
+ # callbacks should be prepared to receive a
+ # events_pb2.SignalRequest.
+ cbk(request)
+
+ # send response back to server
+ response = proto.EventResponse()
+ response.status = proto.EventResponse.OK
+ done.run(response)
+
+ def ping(self, controller, request, done):
+ """
+ Reply to a ping request.
+
+ :param controller: used to mediate a single method call
+ :type controller: protobuf.socketrpc.controller.SocketRpcController
+ :param request: the request received from the client
+ :type request: leap.common.events.events_pb2.RegisterRequest
+ :param done: callback to be called when done
+ :type done: protobuf.socketrpc.server.Callback
+ """
+ logger.info("Received ping request, sending response.")
+ response = proto.EventResponse()
+ response.status = proto.EventResponse.OK
+ done.run(response)
+
+
+class EventsClientDaemon(daemon.EventsSingletonDaemon):
+ """
+ A daemon that listens for incoming events from server.
+ """
+
+ @classmethod
+ def ensure(cls, port):
+ """
+ Make sure the daemon is running on the given port.
+
+ :param port: the port in which the daemon should listen
+ :type port: int
+
+ :return: a daemon instance
+ :rtype: EventsClientDaemon
+ """
+ return cls.ensure_service(port, EventsClientService())