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# -*- coding: utf-8 -*- 

2# This file is part of Xpra. 

3# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>) 

4# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org> 

5# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com> 

6# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

7# later version. See the file COPYING for details. 

8 

9import sys 

10from time import sleep 

11from threading import Event 

12from collections import deque 

13from queue import Queue 

14 

15from xpra.make_thread import start_thread 

16from xpra.os_util import monotonic_time 

17from xpra.util import notypedict, envbool, envint, typedict, AtomicInteger 

18from xpra.net.compression import compressed_wrapper, Compressed 

19from xpra.server.source.source_stats import GlobalPerformanceStatistics 

20from xpra.server.source.stub_source_mixin import StubSourceMixin 

21from xpra.log import Logger 

22 

23log = Logger("server") 

24notifylog = Logger("notify") 

25bandwidthlog = Logger("bandwidth") 

26 

27 

28BANDWIDTH_DETECTION = envbool("XPRA_BANDWIDTH_DETECTION", True) 

29MIN_BANDWIDTH = envint("XPRA_MIN_BANDWIDTH", 5*1024*1024) 

30AUTO_BANDWIDTH_PCT = envint("XPRA_AUTO_BANDWIDTH_PCT", 80) 

31assert 1<AUTO_BANDWIDTH_PCT<=100, "invalid value for XPRA_AUTO_BANDWIDTH_PCT: %i" % AUTO_BANDWIDTH_PCT 

32YIELD = envbool("XPRA_YIELD", False) 

33 

34counter = AtomicInteger() 

35 

36 

37""" 

38This class mediates between the server class (which only knows about actual window objects and display server events) 

39and the client specific WindowSource instances (which only know about window ids 

40and manage window pixel compression). 

41It sends messages to the client via its 'protocol' instance (the network connection), 

42directly for a number of cases (cursor, sound, notifications, etc) 

43or on behalf of the window sources for pixel data. 

44 

45Strategy: if we have 'ordinary_packets' to send, send those. 

46When we don't, then send packets from the 'packet_queue'. (compressed pixels or clipboard data) 

47See 'next_packet'. 

48 

49The UI thread calls damage(), which goes into WindowSource and eventually (batching may be involved) 

50adds the damage pixels ready for processing to the encode_work_queue, 

51items are picked off by the separate 'encode' thread (see 'encode_loop') 

52and added to the damage_packet_queue. 

53""" 

54 

55class ClientConnection(StubSourceMixin): 

56 

57 def __init__(self, protocol, disconnect_cb, session_name, 

58 setting_changed, 

59 socket_dir, unix_socket_paths, log_disconnect, bandwidth_limit, bandwidth_detection, 

60 ): 

61 global counter 

62 self.counter = counter.increase() 

63 self.protocol = protocol 

64 self.connection_time = monotonic_time() 

65 self.close_event = Event() 

66 self.disconnect = disconnect_cb 

67 self.session_name = session_name 

68 

69 #holds actual packets ready for sending (already encoded) 

70 #these packets are picked off by the "protocol" via 'next_packet()' 

71 #format: packet, wid, pixels, start_send_cb, end_send_cb 

72 #(only packet is required - the rest can be 0/None for clipboard packets) 

73 self.packet_queue = deque() 

74 # the encode work queue is used by mixins that need to encode data before sending it, 

75 # ie: encodings and clipboard 

76 #this queue will hold functions to call to compress data (pixels, clipboard) 

77 #items placed in this queue are picked off by the "encode" thread, 

78 #the functions should add the packets they generate to the 'packet_queue' 

79 self.encode_work_queue = None 

80 self.encode_thread = None 

81 self.ordinary_packets = [] 

82 self.socket_dir = socket_dir 

83 self.unix_socket_paths = unix_socket_paths 

84 self.log_disconnect = log_disconnect 

85 

86 self.setting_changed = setting_changed 

87 # network constraints: 

88 self.server_bandwidth_limit = bandwidth_limit 

89 self.bandwidth_detection = bandwidth_detection 

90 

91 def run(self): 

92 # ready for processing: 

93 self.queue_encode = self.start_queue_encode 

94 self.protocol.set_packet_source(self.next_packet) 

95 

96 def __repr__(self) -> str: 

97 return "%s(%i : %s)" % (type(self).__name__, self.counter, self.protocol) 

98 

99 def init_state(self): 

100 self.hello_sent = False 

101 self.info_namespace = False 

102 self.share = False 

103 self.lock = False 

104 self.control_commands = () 

105 self.xdg_menu_update = False 

106 self.bandwidth_limit = self.server_bandwidth_limit 

107 self.soft_bandwidth_limit = self.bandwidth_limit 

108 self.bandwidth_warnings = True 

109 self.bandwidth_warning_time = 0 

110 self.client_connection_data = {} 

111 self.adapter_type = "" 

112 self.jitter = 0 

113 #what we send back in hello packet: 

114 self.ui_client = True 

115 self.wants_aliases = True 

116 self.wants_encodings = True 

117 self.wants_versions = True 

118 self.wants_features = True 

119 self.wants_display = True 

120 self.wants_events = False 

121 self.wants_default_cursor = False 

122 #these statistics are shared by all WindowSource instances: 

123 self.statistics = GlobalPerformanceStatistics() 

124 

125 

126 def is_closed(self) -> bool: 

127 return self.close_event.isSet() 

128 

129 def cleanup(self): 

130 log("%s.close()", self) 

131 self.close_event.set() 

132 self.protocol = None 

133 self.statistics.reset(0) 

134 

135 

136 def may_notify(self, *args, **kwargs): 

137 #fugly workaround, 

138 #MRO is depth first and would hit the default implementation 

139 #instead of the mixin unless we force it: 

140 notification_mixin = sys.modules.get("xpra.server.source.notification_mixin") 

141 if notification_mixin and isinstance(self, notification_mixin.NotificationMixin): 

142 notification_mixin.NotificationMixin.may_notify(self, *args, **kwargs) 

143 

144 

145 def compressed_wrapper(self, datatype, data, min_saving=128): 

146 if self.zlib or self.lz4 or self.lzo: 

147 cw = compressed_wrapper(datatype, data, zlib=self.zlib, lz4=self.lz4, lzo=self.lzo, can_inline=False) 

148 if len(cw)+min_saving<=len(data): 

149 #the compressed version is smaller, use it: 

150 return cw 

151 #skip compressed version: fall through 

152 #we can't compress, so at least avoid warnings in the protocol layer: 

153 return Compressed(datatype, data, can_inline=True) 

154 

155 

156 def update_bandwidth_limits(self): 

157 if not self.bandwidth_detection: 

158 return 

159 mmap_size = getattr(self, "mmap_size", 0) 

160 if mmap_size>0: 

161 return 

162 #calculate soft bandwidth limit based on send congestion data: 

163 bandwidth_limit = 0 

164 if BANDWIDTH_DETECTION: 

165 bandwidth_limit = self.statistics.avg_congestion_send_speed 

166 bandwidthlog("avg_congestion_send_speed=%s", bandwidth_limit) 

167 if bandwidth_limit>20*1024*1024: 

168 #ignore congestion speed if greater 20Mbps 

169 bandwidth_limit = 0 

170 if (self.bandwidth_limit or 0)>0: 

171 #command line options could overrule what we detect? 

172 bandwidth_limit = min(self.bandwidth_limit, bandwidth_limit) 

173 if bandwidth_limit>0: 

174 bandwidth_limit = max(MIN_BANDWIDTH, bandwidth_limit) 

175 self.soft_bandwidth_limit = bandwidth_limit 

176 bandwidthlog("update_bandwidth_limits() bandwidth_limit=%s, soft bandwidth limit=%s", 

177 self.bandwidth_limit, bandwidth_limit) 

178 #figure out how to distribute the bandwidth amongst the windows, 

179 #we use the window size, 

180 #(we should use the number of bytes actually sent: framerate, compression, etc..) 

181 window_weight = {} 

182 for wid, ws in self.window_sources.items(): 

183 weight = 0 

184 if not ws.suspended: 

185 ww, wh = ws.window_dimensions 

186 #try to reserve bandwidth for at least one screen update, 

187 #and add the number of pixels damaged: 

188 weight = ww*wh + ws.statistics.get_damage_pixels() 

189 window_weight[wid] = weight 

190 bandwidthlog("update_bandwidth_limits() window weights=%s", window_weight) 

191 total_weight = max(1, sum(window_weight.values())) 

192 for wid, ws in self.window_sources.items(): 

193 if bandwidth_limit==0: 

194 ws.bandwidth_limit = 0 

195 else: 

196 weight = window_weight.get(wid, 0) 

197 ws.bandwidth_limit = max(MIN_BANDWIDTH//10, bandwidth_limit*weight//total_weight) 

198 

199 

200 def parse_client_caps(self, c : typedict): 

201 #general features: 

202 self.info_namespace = c.boolget("info-namespace") 

203 self.share = c.boolget("share") 

204 self.lock = c.boolget("lock") 

205 self.control_commands = c.strtupleget("control_commands") 

206 self.xdg_menu_update = c.boolget("xdg-menu-update") 

207 bandwidth_limit = c.intget("bandwidth-limit", 0) 

208 server_bandwidth_limit = self.server_bandwidth_limit 

209 if self.server_bandwidth_limit is None: 

210 server_bandwidth_limit = self.get_socket_bandwidth_limit() or bandwidth_limit 

211 self.bandwidth_limit = min(server_bandwidth_limit, bandwidth_limit) 

212 if self.bandwidth_detection: 

213 self.bandwidth_detection = c.boolget("bandwidth-detection", True) 

214 self.client_connection_data = c.dictget("connection-data", {}) 

215 ccd = typedict(self.client_connection_data) 

216 self.adapter_type = ccd.strget("adapter-type", "") 

217 self.jitter = ccd.intget("jitter", 0) 

218 bandwidthlog("server bandwidth-limit=%s, client bandwidth-limit=%s, value=%s, detection=%s", 

219 server_bandwidth_limit, bandwidth_limit, self.bandwidth_limit, self.bandwidth_detection) 

220 

221 if getattr(self, "mmap_size", 0)>0: 

222 log("mmap enabled, ignoring bandwidth-limit") 

223 self.bandwidth_limit = 0 

224 

225 def get_socket_bandwidth_limit(self) -> int: 

226 p = self.protocol 

227 if not p: 

228 return 0 

229 #auto-detect: 

230 pinfo = p.get_info() 

231 socket_speed = pinfo.get("socket", {}).get("device", {}).get("speed") 

232 if not socket_speed: 

233 return 0 

234 bandwidthlog("get_socket_bandwidth_limit() socket_speed=%s", socket_speed) 

235 #auto: use 80% of socket speed if we have it: 

236 return socket_speed*AUTO_BANDWIDTH_PCT//100 or 0 

237 

238 

239 def startup_complete(self): 

240 log("startup_complete()") 

241 self.send("startup-complete") 

242 

243 

244 # 

245 # The encode thread loop management: 

246 # 

247 def start_queue_encode(self, item): 

248 #start the encode work queue: 

249 #holds functions to call to compress data (pixels, clipboard) 

250 #items placed in this queue are picked off by the "encode" thread, 

251 #the functions should add the packets they generate to the 'packet_queue' 

252 self.encode_work_queue = Queue() 

253 self.queue_encode = self.encode_work_queue.put 

254 self.queue_encode(item) 

255 self.encode_thread = start_thread(self.encode_loop, "encode") 

256 

257 def encode_queue_size(self) -> int: 

258 ewq = self.encode_work_queue 

259 if ewq is None: 

260 return 0 

261 return ewq.qsize() 

262 

263 def call_in_encode_thread(self, *fn_and_args): 

264 """ 

265 This is used by WindowSource to queue damage processing to be done in the 'encode' thread. 

266 The 'encode_and_send_cb' will then add the resulting packet to the 'packet_queue' via 'queue_packet'. 

267 """ 

268 self.statistics.compression_work_qsizes.append((monotonic_time(), self.encode_queue_size())) 

269 self.queue_encode(fn_and_args) 

270 

271 def queue_packet(self, packet, wid=0, pixels=0, 

272 start_send_cb=None, end_send_cb=None, fail_cb=None, wait_for_more=False): 

273 """ 

274 Add a new 'draw' packet to the 'packet_queue'. 

275 Note: this code runs in the non-ui thread 

276 """ 

277 now = monotonic_time() 

278 self.statistics.packet_qsizes.append((now, len(self.packet_queue))) 

279 if wid>0: 

280 self.statistics.damage_packet_qpixels.append( 

281 (now, wid, sum(x[2] for x in tuple(self.packet_queue) if x[1]==wid)) 

282 ) 

283 self.packet_queue.append((packet, wid, pixels, start_send_cb, end_send_cb, fail_cb, wait_for_more)) 

284 p = self.protocol 

285 if p: 

286 p.source_has_more() 

287 

288 def encode_loop(self): 

289 """ 

290 This runs in a separate thread and calls all the function callbacks 

291 which are added to the 'encode_work_queue'. 

292 Must run until we hit the end of queue marker, 

293 to ensure all the queued items get called, 

294 those that are marked as optional will be skipped when is_closed() 

295 """ 

296 while True: 

297 fn_and_args = self.encode_work_queue.get(True) 

298 if fn_and_args is None: 

299 return #empty marker 

300 #some function calls are optional and can be skipped when closing: 

301 #(but some are not, like encoder clean functions) 

302 optional_when_closing = fn_and_args[0] 

303 if optional_when_closing and self.is_closed(): 

304 continue 

305 try: 

306 fn_and_args[1](*fn_and_args[2:]) 

307 except Exception as e: 

308 if self.is_closed(): 

309 log("ignoring encoding error in %s as source is already closed:", fn_and_args[0]) 

310 log(" %s", e) 

311 else: 

312 log.error("Error during encoding:", exc_info=True) 

313 del e 

314 if YIELD: 

315 sleep(0) 

316 

317 ###################################################################### 

318 # network: 

319 def next_packet(self): 

320 """ Called by protocol.py when it is ready to send the next packet """ 

321 packet, start_send_cb, end_send_cb, fail_cb = None, None, None, None 

322 synchronous, have_more, will_have_more = True, False, False 

323 if not self.is_closed(): 

324 if self.ordinary_packets: 

325 packet, synchronous, fail_cb, will_have_more = self.ordinary_packets.pop(0) 

326 elif self.packet_queue: 

327 packet, _, _, start_send_cb, end_send_cb, fail_cb, will_have_more = self.packet_queue.popleft() 

328 have_more = packet is not None and (self.ordinary_packets or self.packet_queue) 

329 return packet, start_send_cb, end_send_cb, fail_cb, synchronous, have_more, will_have_more 

330 

331 def send(self, *parts, **kwargs): 

332 """ This method queues non-damage packets (higher priority) """ 

333 synchronous = kwargs.get("synchronous", True) 

334 will_have_more = kwargs.get("will_have_more", not synchronous) 

335 fail_cb = kwargs.get("fail_cb", None) 

336 p = self.protocol 

337 if p: 

338 self.ordinary_packets.append((parts, synchronous, fail_cb, will_have_more)) 

339 p.source_has_more() 

340 

341 def send_more(self, *parts, **kwargs): 

342 kwargs["will_have_more"] = True 

343 self.send(*parts, **kwargs) 

344 

345 def send_async(self, *parts, **kwargs): 

346 kwargs["synchronous"] = False 

347 kwargs["will_have_more"] = False 

348 self.send(*parts, **kwargs) 

349 

350 

351 ###################################################################### 

352 # info: 

353 def get_info(self) -> dict: 

354 info = { 

355 "protocol" : "xpra", 

356 "connection_time" : int(self.connection_time), 

357 "elapsed_time" : int(monotonic_time()-self.connection_time), 

358 "counter" : self.counter, 

359 "hello-sent" : self.hello_sent, 

360 "jitter" : self.jitter, 

361 "adapter-type" : self.adapter_type, 

362 "bandwidth-limit" : { 

363 "detection" : self.bandwidth_detection, 

364 "actual" : self.soft_bandwidth_limit or 0, 

365 } 

366 } 

367 p = self.protocol 

368 if p: 

369 info.update({ 

370 "connection" : p.get_info(), 

371 }) 

372 info.update(self.get_features_info()) 

373 return info 

374 

375 def get_features_info(self) -> dict: 

376 info = { 

377 "lock" : bool(self.lock), 

378 "share" : bool(self.share), 

379 } 

380 return info 

381 

382 

383 def send_info_response(self, info): 

384 self.send_async("info-response", notypedict(info)) 

385 

386 

387 def send_setting_change(self, setting, value): 

388 #we always subclass InfoMixin which defines "client_setting_change": 

389 if self.client_setting_change: 

390 self.send_more("setting-change", setting, value) 

391 

392 

393 def send_server_event(self, *args): 

394 if self.wants_events: 

395 self.send_more("server-event", *args) 

396 

397 

398 def set_deflate(self, level : int): 

399 self.send("set_deflate", level) 

400 

401 

402 def send_client_command(self, *args): 

403 if self.hello_sent: 

404 self.send_more("control", *args) 

405 

406 

407 def rpc_reply(self, *args): 

408 if self.hello_sent: 

409 self.send("rpc-reply", *args)