From e031af6e5e8324fe4cda66d9597904040b17ca80 Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Sun, 4 Sep 2022 14:18:02 +1000 Subject: Vendor the "simple-network" library --- network/server.py | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 network/server.py (limited to 'network/server.py') diff --git a/network/server.py b/network/server.py new file mode 100644 index 0000000..9c63ea5 --- /dev/null +++ b/network/server.py @@ -0,0 +1,148 @@ +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 = '' + + help(self.avahi_group) + 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() -- cgit v1.2.3