Hide keyboard shortcuts

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 

6 

7import os 

8import re 

9from collections import deque 

10 

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 

18 

19log = Logger("network") 

20bandwidthlog = Logger("bandwidth") 

21 

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 

29 

30 

31""" 

32Mixin for adding server / network state monitoring functions: 

33- ping and echo 

34- info request and response 

35""" 

36class NetworkState(StubClientMixin): 

37 

38 def __init__(self): 

39 StubClientMixin.__init__(self) 

40 self.server_start_time = -1 

41 #legacy: 

42 self.compression_level = 0 

43 

44 #setting: 

45 self.pings = False 

46 

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 

53 

54 #info requests 

55 self.server_last_info = None 

56 self.info_request_pending = False 

57 

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 

68 

69 

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) 

75 

76 

77 def cleanup(self): 

78 self.cancel_ping_timer() 

79 self.cancel_ping_echo_timers() 

80 self.cancel_ping_echo_timeout_timer() 

81 

82 

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 } 

91 

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 

161 

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 

172 

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) 

178 

179 def cancel_ping_timer(self): 

180 pt = self.ping_timer 

181 if pt: 

182 self.ping_timer = None 

183 self.source_remove(pt) 

184 

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) 

190 

191 

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]) 

205 

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) 

211 

212 

213 ###################################################################### 

214 # network and status: 

215 def server_ok(self) -> bool: 

216 return self._server_ok 

217 

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 

239 

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) 

245 

246 def server_connection_state_change(self): 

247 log("server_connection_state_change() ok=%s", self._server_ok) 

248 

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) 

255 

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 

270 

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) 

281 

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) 

301 

302 

303 ###################################################################### 

304 # network level packet compression: 

305 def set_deflate_level(self, level): 

306 self.compression_level = level 

307 self.send_deflate_level() 

308 

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) 

313 

314 

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) 

319 

320 

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)