Passed
Push — master ( a89286...fe5519 )
by Steffen
01:36
created

MasterServers.master_servers()   A

Complexity

Conditions 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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