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
49 def send_queue_messages(plom_socket, queue_in, thread_alive):
50 """Send messages via socket from queue_in while thread_alive[0]."""
51 while thread_alive[0]:
53 msg = queue_in.get(timeout=1)
56 plom_socket.send(msg, True)
60 plom_socket = plom_socket.PlomSocket(self.request)
61 print('CONNECTION FROM:', str(self.client_address))
62 connection_id = uuid.uuid4()
63 queue_in = queue.Queue()
64 self.server.queue_out.put(('ADD_QUEUE', connection_id, queue_in))
66 t = threading.Thread(target=send_queue_messages,
67 args=(plom_socket, queue_in, thread_alive))
69 for message in plom_socket.recv():
71 plom_socket.send('BAD MESSAGE', True)
72 elif 'QUIT' == message:
73 plom_socket.send('BYE', True)
76 self.server.queue_out.put(('COMMAND', connection_id, message))
77 self.server.queue_out.put(('KILL_QUEUE', connection_id))
78 thread_alive[0] = False
79 print('CONNECTION CLOSED FROM:', str(self.client_address))
80 plom_socket.socket.close()
85 def __init__(self, game_file_name, game):
86 self.game_file_name = game_file_name
88 self.parser = parser.Parser(game)
91 """Handle commands coming through queue q, send results back.
93 Commands from q are expected to be tuples, with the first element
94 either 'ADD_QUEUE', 'COMMAND', or 'KILL_QUEUE', the second element
95 a UUID, and an optional third element of arbitrary type. The UUID
96 identifies a receiver for replies.
98 An 'ADD_QUEUE' command should contain as third element a queue
99 through which to send messages back to the sender of the
100 command. A 'KILL_QUEUE' command removes the queue for that
101 receiver from the list of queues through which to send replies.
103 A 'COMMAND' command is specified in greater detail by a string
104 that is the tuple's third element. The game_command_handler takes
105 care of processing this and sending out replies.
112 content = None if len(x) == 2 else x[2]
113 if command_type == 'ADD_QUEUE':
114 self.queues_out[connection_id] = content
115 elif command_type == 'KILL_QUEUE':
116 del self.queues_out[connection_id]
117 elif command_type == 'COMMAND':
118 self.handle_input(content, connection_id)
120 def run_loop_with_server(self):
121 """Run connection of server talking to clients and game IO loop.
123 We have the TCP server (an instance of Server) and we have the
124 game IO loop, a thread running self.loop. Both communicate with
125 each other via a queue.Queue. While the TCP server may spawn
126 parallel threads to many clients, the IO loop works sequentially
127 through game commands received from the TCP server's threads (=
128 client connections to the TCP server). A processed command may
129 trigger messages to the commanding client or to all clients,
130 delivered from the IO loop to the TCP server via the queue.
134 c = threading.Thread(target=self.loop, daemon=True, args=(q,))
136 server = Server(q, 5000)
138 server.serve_forever()
139 except KeyboardInterrupt:
142 print('Killing server')
143 server.server_close()
145 def handle_input(self, input_, connection_id=None, store=True):
146 """Process input_ to command grammar, call command handler if found."""
147 from inspect import signature
150 def answer(connection_id, msg):
152 self.send(msg, connection_id)
157 command, args = self.parser.parse(input_)
159 answer(connection_id, 'UNHANDLED_INPUT')
161 if 'connection_id' in list(signature(command).parameters):
162 command(*args, connection_id=connection_id)
165 if store and not hasattr(command, 'dont_save'):
166 with open(self.game_file_name, 'a') as f:
167 f.write(input_ + '\n')
168 except parser.ArgError as e:
169 answer(connection_id, 'ARGUMENT_ERROR ' + quote(str(e)))
170 except GameError as e:
171 answer(connection_id, 'GAME_ERROR ' + quote(str(e)))
173 def send(self, msg, connection_id=None):
174 """Send message msg to server's client(s) via self.queues_out.
176 If a specific client is identified by connection_id, only
177 sends msg to that one. Else, sends it to all clients
178 identified in self.queues_out.
182 self.queues_out[connection_id].put(msg)
184 for connection_id in self.queues_out:
185 self.queues_out[connection_id].put(msg)
189 """Quote & escape string so client interprets it as single token."""
197 return ''.join(quoted)
200 def stringify_yx(tuple_):
201 """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
202 return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])