Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/mixins/network_state.py : 80%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5#pylint: disable-msg=E1101
7import os
8import re
9from collections import deque
11from xpra.os_util import monotonic_time, POSIX
12from xpra.util import envint, envbool, csv, typedict
13from xpra.exit_codes import EXIT_TIMEOUT
14from xpra.net.packet_encoding import ALL_ENCODERS
15from xpra.client.mixins.stub_client_mixin import StubClientMixin
16from xpra.scripts.config import parse_with_unit
17from xpra.log import Logger
19log = Logger("network")
20bandwidthlog = Logger("bandwidth")
22FAKE_BROKEN_CONNECTION = envint("XPRA_FAKE_BROKEN_CONNECTION")
23PING_TIMEOUT = envint("XPRA_PING_TIMEOUT", 60)
24SWALLOW_PINGS = envbool("XPRA_SWALLOW_PINGS", False)
25#LOG_INFO_RESPONSE = ("^window.*position", "^window.*size$")
26LOG_INFO_RESPONSE = os.environ.get("XPRA_LOG_INFO_RESPONSE", "")
27AUTO_BANDWIDTH_PCT = envint("XPRA_AUTO_BANDWIDTH_PCT", 80)
28assert 1<AUTO_BANDWIDTH_PCT<=100, "invalid value for XPRA_AUTO_BANDWIDTH_PCT: %i" % AUTO_BANDWIDTH_PCT
31"""
32Mixin for adding server / network state monitoring functions:
33- ping and echo
34- info request and response
35"""
36class NetworkState(StubClientMixin):
38 def __init__(self):
39 StubClientMixin.__init__(self)
40 self.server_start_time = -1
41 #legacy:
42 self.compression_level = 0
44 #setting:
45 self.pings = False
47 #bandwidth
48 self.bandwidth_limit = 0
49 self.bandwidth_detection = True
50 self.server_bandwidth_limit_change = False
51 self.server_bandwidth_limit = 0
52 self.server_session_name = None
54 #info requests
55 self.server_last_info = None
56 self.info_request_pending = False
58 #network state:
59 self.server_packet_encoders = ()
60 self.server_ping_latency = deque(maxlen=1000)
61 self.server_load = None
62 self.client_ping_latency = deque(maxlen=1000)
63 self._server_ok = True
64 self.last_ping_echoed_time = 0
65 self.ping_timer = None
66 self.ping_echo_timers = {}
67 self.ping_echo_timeout_timer = None
70 def init(self, opts):
71 self.pings = opts.pings
72 self.bandwidth_limit = parse_with_unit("bandwidth-limit", opts.bandwidth_limit)
73 self.bandwidth_detection = opts.bandwidth_detection
74 bandwidthlog("init bandwidth_limit=%s", self.bandwidth_limit)
77 def cleanup(self):
78 self.cancel_ping_timer()
79 self.cancel_ping_echo_timers()
80 self.cancel_ping_echo_timeout_timer()
83 def get_info(self) -> dict:
84 return {
85 "network" : {
86 "bandwidth-limit" : self.bandwidth_limit,
87 "bandwidth-detection" : self.bandwidth_detection,
88 "server-ok" : self._server_ok,
89 }
90 }
92 def get_caps(self) -> dict:
93 caps = {
94 "network-state" : True,
95 "info-namespace" : True, #v4 servers assume this is always supported
96 }
97 #get socket speed if we have it:
98 pinfo = self._protocol.get_info()
99 device_info = pinfo.get("socket", {}).get("device", {})
100 connection_data = {}
101 try:
102 coptions = self._protocol._conn.options
103 except AttributeError:
104 coptions = {}
105 log("get_caps() device_info=%s, connection options=%s", device_info, coptions)
106 def device_value(attr, conv=str, default_value=""):
107 #first try an env var:
108 v = os.environ.get("XPRA_NETWORK_%s" % attr.upper().replace("-", "_"))
109 #next try device options (ie: from connection URI)
110 if v is None:
111 v = coptions.get("socket.%s" % attr)
112 #last: the OS may know:
113 if v is None:
114 v = device_info.get(attr)
115 if v is not None:
116 try:
117 return conv(v)
118 except (ValueError, TypeError) as e:
119 log("device_value%s", (attr, conv, default_value), exc_info=True)
120 log.warn("Warning: invalid value for network attribute '%s'", attr)
121 log.warn(" %r: %s", v, e)
122 return default_value
123 def parse_speed(v):
124 return parse_with_unit("speed", v)
125 #network interface speed:
126 socket_speed = device_value("speed", parse_speed, 0)
127 log("get_caps() found socket_speed=%s", socket_speed)
128 if socket_speed:
129 connection_data["speed"] = socket_speed
130 adapter_type = device_value("adapter-type")
131 log("get_caps() found adapter-type=%s", adapter_type)
132 if adapter_type:
133 connection_data["adapter-type"] = adapter_type
134 jitter = device_value("jitter", int, -1)
135 if jitter<0:
136 at = adapter_type.lower()
137 if any(at.find(x)>=0 for x in ("ether", "local", "fiber", "1394", "infiniband")):
138 jitter = 0
139 elif at.find("wan")>=0:
140 jitter = 20
141 elif at.find("wireless")>=0 or at.find("wifi")>=0 or at.find("80211")>=0:
142 jitter = 1000
143 if jitter>=0:
144 connection_data["jitter"] = jitter
145 log("get_caps() connection-data=%s", connection_data)
146 caps["connection-data"] = connection_data
147 bandwidth_limit = self.bandwidth_limit
148 bandwidthlog("bandwidth-limit setting=%s, socket-speed=%s", self.bandwidth_limit, socket_speed)
149 if bandwidth_limit is None:
150 if socket_speed:
151 #auto: use 80% of socket speed if we have it:
152 bandwidth_limit = socket_speed*AUTO_BANDWIDTH_PCT//100 or 0
153 else:
154 bandwidth_limit = 0
155 bandwidthlog("bandwidth-limit capability=%s", bandwidth_limit)
156 if bandwidth_limit>0:
157 caps["bandwidth-limit"] = bandwidth_limit
158 caps["bandwidth-detection"] = self.bandwidth_detection
159 caps["ping-echo-sourceid"] = True
160 return caps
162 def parse_server_capabilities(self, c : typedict) -> bool:
163 #make sure the server doesn't provide a start time in the future:
164 import time
165 self.server_start_time = min(time.time(), c.intget("start_time", -1))
166 self.server_bandwidth_limit_change = c.boolget("network.bandwidth-limit-change")
167 self.server_bandwidth_limit = c.intget("network.bandwidth-limit")
168 bandwidthlog("server_bandwidth_limit_change=%s, server_bandwidth_limit=%s",
169 self.server_bandwidth_limit_change, self.server_bandwidth_limit)
170 self.server_packet_encoders = tuple(x for x in ALL_ENCODERS if c.boolget(x, False))
171 return True
173 def process_ui_capabilities(self, caps : typedict):
174 self.send_deflate_level()
175 self.send_ping()
176 if self.pings>0:
177 self.ping_timer = self.timeout_add(1000*self.pings, self.send_ping)
179 def cancel_ping_timer(self):
180 pt = self.ping_timer
181 if pt:
182 self.ping_timer = None
183 self.source_remove(pt)
185 def cancel_ping_echo_timers(self):
186 pet = tuple(self.ping_echo_timers.values())
187 self.ping_echo_timers = {}
188 for t in pet:
189 self.source_remove(t)
192 ######################################################################
193 # info:
194 def _process_info_response(self, packet):
195 self.info_request_pending = False
196 self.server_last_info = packet[1]
197 log("info-response: %s", self.server_last_info)
198 if LOG_INFO_RESPONSE:
199 items = LOG_INFO_RESPONSE.split(",")
200 logres = [re.compile(v) for v in items]
201 log.info("info-response debug for %s:", csv(["'%s'" % x for x in items]))
202 for k in sorted(self.server_last_info.keys()):
203 if LOG_INFO_RESPONSE=="all" or any(lr.match(k) for lr in logres):
204 log.info(" %s=%s", k, self.server_last_info[k])
206 def send_info_request(self, *categories):
207 if not self.info_request_pending:
208 self.info_request_pending = True
209 window_ids = () #no longer used or supported by servers
210 self.send("info-request", [self.uuid], window_ids, categories)
213 ######################################################################
214 # network and status:
215 def server_ok(self) -> bool:
216 return self._server_ok
218 def check_server_echo(self, ping_sent_time):
219 self.ping_echo_timers.pop(ping_sent_time, None)
220 if self._protocol is None:
221 #no longer connected!
222 return False
223 last = self._server_ok
224 if FAKE_BROKEN_CONNECTION>0:
225 self._server_ok = (int(monotonic_time()) % FAKE_BROKEN_CONNECTION) <= (FAKE_BROKEN_CONNECTION//2)
226 else:
227 self._server_ok = self.last_ping_echoed_time>=ping_sent_time
228 if not self._server_ok:
229 if not self.ping_echo_timeout_timer:
230 self.ping_echo_timeout_timer = self.timeout_add(PING_TIMEOUT*1000,
231 self.check_echo_timeout, ping_sent_time)
232 else:
233 self.cancel_ping_echo_timeout_timer()
234 log("check_server_echo(%s) last=%s, server_ok=%s (last_ping_echoed_time=%s)",
235 ping_sent_time, last, self._server_ok, self.last_ping_echoed_time)
236 if last!=self._server_ok:
237 self.server_connection_state_change()
238 return False
240 def cancel_ping_echo_timeout_timer(self):
241 pett = self.ping_echo_timeout_timer
242 if pett:
243 self.ping_echo_timeout_timer = None
244 self.source_remove(pett)
246 def server_connection_state_change(self):
247 log("server_connection_state_change() ok=%s", self._server_ok)
249 def check_echo_timeout(self, ping_time):
250 self.ping_echo_timeout_timer = None
251 log("check_echo_timeout(%s) last_ping_echoed_time=%s", ping_time, self.last_ping_echoed_time)
252 if self.last_ping_echoed_time<ping_time:
253 #no point trying to use disconnect_and_quit() to tell the server here..
254 self.warn_and_quit(EXIT_TIMEOUT, "server ping timeout - waited %s seconds without a response" % PING_TIMEOUT)
256 def send_ping(self):
257 now_ms = int(1000.0*monotonic_time())
258 self.send("ping", now_ms)
259 wait = 2.0
260 spl = tuple(self.server_ping_latency)
261 if spl:
262 spl = tuple(x[1] for x in spl)
263 avg = sum(spl) / len(spl)
264 wait = min(5, 1.0+avg*2.0)
265 log("send_ping() timestamp=%s, average server latency=%.1f, using max wait %.2fs",
266 now_ms, 1000.0*avg, wait)
267 t = self.timeout_add(int(1000.0*wait), self.check_server_echo, now_ms)
268 self.ping_echo_timers[now_ms] = t
269 return True
271 def _process_ping_echo(self, packet):
272 echoedtime, l1, l2, l3, cl = packet[1:6]
273 self.last_ping_echoed_time = echoedtime
274 self.check_server_echo(0)
275 server_ping_latency = monotonic_time()-echoedtime/1000.0
276 self.server_ping_latency.append((monotonic_time(), server_ping_latency))
277 self.server_load = l1, l2, l3
278 if cl>=0:
279 self.client_ping_latency.append((monotonic_time(), cl/1000.0))
280 log("ping echo server load=%s, measured client latency=%sms", self.server_load, cl)
282 def _process_ping(self, packet):
283 echotime = packet[1]
284 l1,l2,l3 = 0,0,0
285 sid = ""
286 if len(packet)>=4:
287 sid = packet[3]
288 if POSIX:
289 try:
290 (fl1, fl2, fl3) = os.getloadavg()
291 l1,l2,l3 = int(fl1*1000), int(fl2*1000), int(fl3*1000)
292 except (OSError, AttributeError):
293 pass
294 try:
295 sl = self.server_ping_latency[-1][1]
296 except IndexError:
297 sl = -1
298 if SWALLOW_PINGS>0:
299 return
300 self.send("ping_echo", echotime, l1, l2, l3, int(1000.0*sl), sid)
303 ######################################################################
304 # network level packet compression:
305 def set_deflate_level(self, level):
306 self.compression_level = level
307 self.send_deflate_level()
309 def send_deflate_level(self):
310 if self._protocol:
311 self._protocol.set_compression_level(self.compression_level)
312 self.send("set_deflate", self.compression_level)
315 def send_bandwidth_limit(self):
316 bandwidthlog("send_bandwidth_limit() bandwidth-limit=%i", self.bandwidth_limit)
317 assert self.server_bandwidth_limit_change, self.bandwidth_limit is not None
318 self.send("bandwidth-limit", self.bandwidth_limit)
321 ######################################################################
322 # packets:
323 def init_authenticated_packet_handlers(self):
324 self.add_packet_handler("ping", self._process_ping, False)
325 self.add_packet_handler("ping_echo", self._process_ping_echo, False)
326 self.add_packet_handler("info-response", self._process_info_response, False)