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
|
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()
|