Passed
Push — master ( 6131fb...a89286 )
by Steffen
01:34
created

MasterServers.game_servers()   A

Complexity

Conditions 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 7.2349

Importance

Changes 0
Metric Value
cc 3
eloc 9
nop 1
dl 0
loc 14
ccs 2
cts 9
cp 0.2222
crap 7.2349
rs 9.95
c 0
b 0
f 0
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 1
import socket
4 1
from functools import lru_cache
5 1
from time import sleep, time
6
7 1
from tw_serverinfo import Network
8
9
10 1
class MasterServers(object):
11
    # ToDo: extract to json/yml file
12 1
    master_servers_cfg = [
13
        {
14
            'hostname': 'master1.teeworlds.com',
15
            'port': 8300
16
        },
17
        {
18
            'hostname': 'master2.teeworlds.com',
19
            'port': 8300
20
        },
21
        {
22
            'hostname': 'master3.teeworlds.com',
23
            'port': 8300
24
        },
25
        {
26
            'hostname': 'master4.teeworlds.com',
27
            'port': 8300
28
        }
29
    ]
30 1
    _master_servers = {}
31 1
    _game_servers = {}
32
33 1
    @property
34 1
    def master_servers(self) -> dict:
35
        """Generate generator of master servers with resolve IP address
36
37
        :return:
38
        """
39
        self._master_servers = {}
40
        for master_server in self.master_servers_cfg:
41
            ip = socket.gethostbyname(master_server['hostname'])
42
            master_server_key = '{ip:s}:{port:d}'.format(ip=ip, port=master_server['port'])
43
            self._master_servers[master_server_key] = {
44
                'hostname': master_server['hostname'],
45
                'ip': ip,
46
                'port': master_server['port'],
47
                'type': 'master',
48
                'servers': {}
49
            }
50
        return self._master_servers
51
52 1
    @property
53 1
    def game_servers(self) -> dict:
54
        """Returns the game servers and cache the result
55
56
        :return:
57
        """
58
        cache_info = self.update_master_server_info.cache_info()
59
        if cache_info.currsize > 0:
60
            # clear the cache if the last index was more than 10 minutes ago
61
            if time() >= self.update_master_server_info()['timestamp'] + 60 * 10:
62
                self.update_master_server_info.cache_clear()
63
                self._master_servers = {}
64
                self._game_servers = {}
65
        return self.update_master_server_info()
66
67 1
    @lru_cache(maxsize=None)
68 1
    def update_master_server_info(self) -> dict:
69
        """Check the master servers for the game server count and retrieve the server list
70
71
        :return:
72
        """
73
        # create an udp protocol socket
74
        sock = socket.socket(family=Network.PROTOCOL_FAMILY, type=socket.SOCK_DGRAM)
75
        # set the socket to non blocking to allow parallel requests
76
        sock.setblocking(False)
77
78
        for key, master_server in self.master_servers.items():
79
            master_server = Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETCOUNT'],
80
                                                server=master_server)
81
            master_server = Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETLIST'],
82
                                                server=master_server)
83
            # update master server entry which probably got modified
84
            self._master_servers[key] = master_server
85
86
        duration_without_response = Network.CONNECTION_SLEEP_DURATION
87
        sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
88
89
        while True:
90
            if not Network.receive_packet(sock, self.master_servers, self.process_packet):
91
                if duration_without_response > Network.CONNECTION_TIMEOUT:
92
                    # we didn't receive any packets in time and cancel the connection here
93
                    sock.close()
94
                    break
95
                else:
96
                    # increase the measured duration without a response and sleep for the set duration
97
                    duration_without_response += Network.CONNECTION_SLEEP_DURATION
98
                    sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
99
            else:
100
                # if we got a response reset the duration in case we receive multiple packets
101
                duration_without_response = 0
102
103
        return {
104
            'servers': self._game_servers,
105
            'timestamp': time()
106
        }
107
108 1
    def process_packet(self, data: bytes, server: dict) -> None:
109
        """Process packet function for
110
         - SERVERBROWSE_COUNT
111
         - SERVERBROWSE_LIST
112
        packets
113
114
        :type data: bytes
115
        :type server: dict
116
        :return:
117
        """
118
        master_server_key = '{ip:s}:{port:d}'.format(ip=server['ip'], port=server['port'])
119
        self._master_servers[master_server_key]['response'] = True
120
121
        if data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_COUNT']:
122
            self._master_servers[master_server_key]['num_servers'] = (data[14] << 8) | data[15]
123
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_LIST']:
124
            for i in range(14, len(data) - 14, 18):
125
                if data[i:i + 12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
126
                    ip = socket.inet_ntop(socket.AF_INET, data[i + 12:i + 16])
127
                else:
128
                    ip = '[' + socket.inet_ntop(socket.AF_INET6, data[i:i + 16]) + ']'
129
130
                port = int.from_bytes(data[i + 16:i + 18], byteorder='big')
131
132
                game_server_key = '{ip:s}:{port:d}'.format(ip=ip, port=port)
133
                game_server = {
134
                    'ip': ip,
135
                    'port': port,
136
                    'type': 'game'
137
                }
138
                self._master_servers[master_server_key]['servers'][game_server_key] = game_server
139
                self._game_servers[game_server_key] = game_server
140