summaryrefslogtreecommitdiff
path: root/network/server.py
diff options
context:
space:
mode:
authorCarlo Zancanaro <carlo@zancanaro.id.au>2022-09-04 14:18:02 +1000
committerCarlo Zancanaro <carlo@zancanaro.id.au>2022-09-24 21:19:23 +1000
commite031af6e5e8324fe4cda66d9597904040b17ca80 (patch)
tree0917439655016b088ffe10ae76f85c7e97f1f621 /network/server.py
parent833f87ebba90e6c2d3cb386b2d51c3d113d98a54 (diff)
Vendor the "simple-network" library
Diffstat (limited to 'network/server.py')
-rw-r--r--network/server.py148
1 files changed, 148 insertions, 0 deletions
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()