summaryrefslogtreecommitdiff
path: root/network/server.py
blob: 9c63ea587263731a95403ccf664bd9cc1dd42b71 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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()