Passed
Push — master ( 9c999c...6131fb )
by Steffen
01:22
created

MasterServers.master_servers()   A

Complexity

Conditions 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3.6875

Importance

Changes 0
Metric Value
cc 2
eloc 13
nop 1
dl 0
loc 18
ccs 2
cts 8
cp 0.25
crap 3.6875
rs 9.75
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
            self._master_servers[key] = Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETCOUNT'],
80
                                                            server=master_server)
81
            self._master_servers[key] = Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETLIST'],
82
                                                            server=master_server)
83
84
        duration_without_response = Network.CONNECTION_SLEEP_DURATION
85
        sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
86
87
        while True:
88
            if not Network.receive_packet(sock, self.master_servers, self.process_packet):
89
                if duration_without_response > Network.CONNECTION_TIMEOUT:
90
                    # we didn't receive any packets in time and cancel the connection here
91
                    sock.close()
92
                    break
93
                else:
94
                    # increase the measured duration without a response and sleep for the set duration
95
                    duration_without_response += Network.CONNECTION_SLEEP_DURATION
96
                    sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
97
            else:
98
                # if we got a response reset the duration in case we receive multiple packets
99
                duration_without_response = 0
100
101
        return {
102
            'servers': self._game_servers,
103
            'timestamp': time()
104
        }
105
106 1
    def process_packet(self, data: bytes, server: dict) -> None:
107
        """Process packet function for
108
         - SERVERBROWSE_COUNT
109
         - SERVERBROWSE_LIST
110
        packets
111
112
        :type data: bytes
113
        :type server: dict
114
        :return:
115
        """
116
        master_server_key = '{ip:s}:{port:d}'.format(ip=server['ip'], port=server['port'])
117
        self._master_servers[master_server_key]['response'] = True
118
119
        if data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_COUNT']:
120
            self._master_servers[master_server_key]['num_servers'] = (data[14] << 8) | data[15]
121
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_LIST']:
122
            for i in range(14, len(data) - 14, 18):
123
                if data[i:i + 12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
124
                    ip = socket.inet_ntop(socket.AF_INET, data[i + 12:i + 16])
125
                else:
126
                    ip = '[' + socket.inet_ntop(socket.AF_INET6, data[i:i + 16]) + ']'
127
128
                port = int.from_bytes(data[i + 16:i + 18], byteorder='big')
129
130
                game_server_key = '{ip:s}:{port:d}'.format(ip=ip, port=port)
131
                game_server = {
132
                    'ip': ip,
133
                    'port': port,
134
                    'type': 'game'
135
                }
136
                self._master_servers[master_server_key]['servers'][game_server_key] = game_server
137
                self._game_servers[game_server_key] = game_server
138