Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/source/client_connection.py : 78%
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.
9import sys
10from time import sleep
11from threading import Event
12from collections import deque
13from queue import Queue
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
23log = Logger("server")
24notifylog = Logger("notify")
25bandwidthlog = Logger("bandwidth")
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)
34counter = AtomicInteger()
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.
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'.
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"""
55class ClientConnection(StubSourceMixin):
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
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
86 self.setting_changed = setting_changed
87 # network constraints:
88 self.server_bandwidth_limit = bandwidth_limit
89 self.bandwidth_detection = bandwidth_detection
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)
96 def __repr__(self) -> str:
97 return "%s(%i : %s)" % (type(self).__name__, self.counter, self.protocol)
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()
126 def is_closed(self) -> bool:
127 return self.close_event.isSet()
129 def cleanup(self):
130 log("%s.close()", self)
131 self.close_event.set()
132 self.protocol = None
133 self.statistics.reset(0)
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)
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)
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)
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)
221 if getattr(self, "mmap_size", 0)>0:
222 log("mmap enabled, ignoring bandwidth-limit")
223 self.bandwidth_limit = 0
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
239 def startup_complete(self):
240 log("startup_complete()")
241 self.send("startup-complete")
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")
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()
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)
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()
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)
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
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()
341 def send_more(self, *parts, **kwargs):
342 kwargs["will_have_more"] = True
343 self.send(*parts, **kwargs)
345 def send_async(self, *parts, **kwargs):
346 kwargs["synchronous"] = False
347 kwargs["will_have_more"] = False
348 self.send(*parts, **kwargs)
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
375 def get_features_info(self) -> dict:
376 info = {
377 "lock" : bool(self.lock),
378 "share" : bool(self.share),
379 }
380 return info
383 def send_info_response(self, info):
384 self.send_async("info-response", notypedict(info))
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)
393 def send_server_event(self, *args):
394 if self.wants_events:
395 self.send_more("server-event", *args)
398 def set_deflate(self, level : int):
399 self.send("set_deflate", level)
402 def send_client_command(self, *args):
403 if self.hello_sent:
404 self.send_more("control", *args)
407 def rpc_reply(self, *args):
408 if self.hello_sent:
409 self.send("rpc-reply", *args)