from random import randint import dbus from dbus.mainloop.glib import DBusGMainLoop from gi.repository import GLib import logbook import zmq from network import avahi from network.zmqglib import ZMQSource log = logbook.Logger(__name__) dbus_loop = DBusGMainLoop() system_bus = dbus.SystemBus(mainloop=dbus_loop) avahi_server = dbus.Interface( system_bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER, ) def raise_not_implemented(message): raise NotImplementedError() class Server(object): """ This is a network server capable of handling request/reply style communication (using ZeroMQ). When running, it maintains a zeroconf entry (using avahi) so that it can be discovered automatically over LANs. :param name: The human-readable name of this server (e.g., "Bob's Dungeon Game"). :param kind: The machine-readable kind of server this is. (e.g., "dungeon") :param port: The TCP port to run the server on. If unspecified, it picks a random port (in the future, this will be better, and pick an *unused* port). :param handler: The function to run when on recieving a client request. :param text: A list of strings to put in the avahi record. The handler can also be specified using the server.handler decorator: >>> server = Server("Bob's Dungeon Game", "dungeon") >>> @server.handler ... def on_request(msg): ... if msg == 'attack': ... return 'You attack a monster!' ... return 'Nothing happens.' ... >>> server.run() """ def __init__( self, name, kind, port=None, handler=raise_not_implemented, text=(), ): if port is None: # TODO: fix this! port = randint(49152, 65535) self.name = name self.kind = '_%s._tcp' % kind self.interface = '' self.port = port self.text = text self.handle = handler self.mainloop = GLib.MainLoop() self.avahi_group = dbus.Interface( system_bus.get_object( avahi.DBUS_NAME, avahi_server.EntryGroupNew() ), avahi.DBUS_INTERFACE_ENTRY_GROUP, ) # Communication def handler(self, func): """ Decorator function which provides a more readable way of defining the handler for a server. Be aware that this will override any previously defined handler. """ self.handle = func def on_message(self, socket, data, *user_data): response = self.handle(data) socket.send(response) def run(self, ctx=None): """ Start up the server, and serve requests until ``.stop()`` is called. The server runs using a GLib mainloop so that avahi integration works, which may be helpful for integrating other things (you could put a Gtk application in the same thread). """ if ctx is None: ctx = zmq.Context.instance() socket = ctx.socket(zmq.REP) socket.bind('tcp://*:%d' % self.port) log.debug('Running on port %d' % self.port) self.publish() mainctx = self.mainloop.get_context() source = ZMQSource(socket) source.attach(mainctx) source.set_callback(self.on_message) self.mainloop.run() self.unpublish() def stop(self): """ Stop serving requests. This function is thread-safe, so you can call it from other threads, or you can call it from within the GLib mainloop (using GLib.timeout_add or GLib.idle_add or some other signal) """ self.mainloop.quit() # Avahi def publish(self): if self.interface != '': raise NotImplementedError('Serving on a specific interface.') host = '' domain = '' self.avahi_group.AddService( avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.kind, domain, host, dbus.UInt16(self.port), avahi.string_array_to_txt_array(self.text), ) self.avahi_group.Commit() def unpublish(self): self.avahi_group.Reset()