7 from server_.game_error import GameError
10 # Avoid "Address already in use" errors.
11 socketserver.TCPServer.allow_reuse_address = True
14 class Server(socketserver.ThreadingTCPServer):
15 """Bind together threaded IO handling server and message queue."""
17 def __init__(self, queue, port, *args, **kwargs):
18 super().__init__(('localhost', port), IO_Handler, *args, **kwargs)
19 self.queue_out = queue
20 self.daemon_threads = True # Else, server's threads have daemon=False.
23 class IO_Handler(socketserver.BaseRequestHandler):
26 """Move messages between network socket and game IO loop via queues.
28 On start (a new connection from client to server), sets up a
29 new queue, sends it via self.server.queue_out to the game IO
30 loop thread, and from then on receives messages to send back
31 from the game IO loop via that new queue.
33 At the same time, loops over socket's recv to get messages
34 from the outside into the game IO loop by way of
35 self.server.queue_out into the game IO. Ends connection once a
36 'QUIT' message is received from socket, and then also calls
37 for a kill of its own queue.
39 All messages to the game IO loop are tuples, with the first
40 element a meta command ('ADD_QUEUE' for queue creation,
41 'KILL_QUEUE' for queue deletion, and 'COMMAND' for everything
42 else), the second element a UUID that uniquely identifies the
43 thread (so that the game IO loop knows whom to send replies
44 back to), and optionally a third element for further
50 def caught_send(socket, message):
51 """Send message by socket, catch broken socket connection error."""
53 plom_socket_io.send(socket, message)
54 except plom_socket_io.BrokenSocketConnection:
57 def send_queue_messages(socket, queue_in, thread_alive):
58 """Send messages via socket from queue_in while thread_alive[0]."""
59 while thread_alive[0]:
61 msg = queue_in.get(timeout=1)
64 caught_send(socket, msg)
67 print('CONNECTION FROM:', str(self.client_address))
68 connection_id = uuid.uuid4()
69 queue_in = queue.Queue()
70 self.server.queue_out.put(('ADD_QUEUE', connection_id, queue_in))
72 t = threading.Thread(target=send_queue_messages,
73 args=(self.request, queue_in, thread_alive))
75 for message in plom_socket_io.recv(self.request):
77 caught_send(self.request, 'BAD MESSAGE')
78 elif 'QUIT' == message:
79 caught_send(self.request, 'BYE')
82 self.server.queue_out.put(('COMMAND', connection_id, message))
83 self.server.queue_out.put(('KILL_QUEUE', connection_id))
84 thread_alive[0] = False
85 print('CONNECTION CLOSED FROM:', str(self.client_address))
91 def __init__(self, game_file_name, game):
92 self.game_file_name = game_file_name
94 self.parser = parser.Parser(game)
97 """Handle commands coming through queue q, send results back.
99 Commands from q are expected to be tuples, with the first element
100 either 'ADD_QUEUE', 'COMMAND', or 'KILL_QUEUE', the second element
101 a UUID, and an optional third element of arbitrary type. The UUID
102 identifies a receiver for replies.
104 An 'ADD_QUEUE' command should contain as third element a queue
105 through which to send messages back to the sender of the
106 command. A 'KILL_QUEUE' command removes the queue for that
107 receiver from the list of queues through which to send replies.
109 A 'COMMAND' command is specified in greater detail by a string
110 that is the tuple's third element. The game_command_handler takes
111 care of processing this and sending out replies.
118 content = None if len(x) == 2 else x[2]
119 if command_type == 'ADD_QUEUE':
120 self.queues_out[connection_id] = content
121 elif command_type == 'KILL_QUEUE':
122 del self.queues_out[connection_id]
123 elif command_type == 'COMMAND':
124 self.handle_input(content, connection_id)
126 def run_loop_with_server(self):
127 """Run connection of server talking to clients and game IO loop.
129 We have the TCP server (an instance of Server) and we have the
130 game IO loop, a thread running self.loop. Both communicate with
131 each other via a queue.Queue. While the TCP server may spawn
132 parallel threads to many clients, the IO loop works sequentially
133 through game commands received from the TCP server's threads (=
134 client connections to the TCP server). A processed command may
135 trigger messages to the commanding client or to all clients,
136 delivered from the IO loop to the TCP server via the queue.
140 c = threading.Thread(target=self.loop, daemon=True, args=(q,))
142 server = Server(q, 5000)
144 server.serve_forever()
145 except KeyboardInterrupt:
148 print('Killing server')
149 server.server_close()
151 def handle_input(self, input_, connection_id=None, store=True):
152 """Process input_ to command grammar, call command handler if found."""
153 from inspect import signature
156 def answer(connection_id, msg):
158 self.send(msg, connection_id)
163 command, args = self.parser.parse(input_)
165 answer(connection_id, 'UNHANDLED_INPUT')
167 if 'connection_id' in list(signature(command).parameters):
168 command(*args, connection_id=connection_id)
171 if store and not hasattr(command, 'dont_save'):
172 with open(self.game_file_name, 'a') as f:
173 f.write(input_ + '\n')
174 except parser.ArgError as e:
175 answer(connection_id, 'ARGUMENT_ERROR ' + quote(str(e)))
176 except GameError as e:
177 answer(connection_id, 'GAME_ERROR ' + quote(str(e)))
179 def send(self, msg, connection_id=None):
180 """Send message msg to server's client(s) via self.queues_out.
182 If a specific client is identified by connection_id, only
183 sends msg to that one. Else, sends it to all clients
184 identified in self.queues_out.
188 self.queues_out[connection_id].put(msg)
190 for connection_id in self.queues_out:
191 self.queues_out[connection_id].put(msg)
195 """Quote & escape string so client interprets it as single token."""
203 return ''.join(quoted)
206 def stringify_yx(tuple_):
207 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
208 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])