Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/source/clipboard_connection.py : 52%
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) 2010-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8from collections import deque
10from xpra.server.source.stub_source_mixin import StubSourceMixin
11from xpra.platform.features import CLIPBOARDS
12from xpra.util import envint, typedict
13from xpra.os_util import monotonic_time
14from xpra.log import Logger
16log = Logger("clipboard")
18MAX_CLIPBOARD_LIMIT = envint("XPRA_CLIPBOARD_LIMIT", 30)
19MAX_CLIPBOARD_LIMIT_DURATION = envint("XPRA_CLIPBOARD_LIMIT_DURATION", 3)
22class ClipboardConnection(StubSourceMixin):
24 @classmethod
25 def is_needed(cls, caps : typedict) -> bool:
26 return caps.boolget("clipboard")
29 def init_state(self):
30 self.clipboard_enabled = False
31 self.clipboard_notifications = False
32 self.clipboard_notifications_current = 0
33 self.clipboard_notifications_pending = 0
34 self.clipboard_progress_timer = None
35 self.clipboard_stats = deque(maxlen=MAX_CLIPBOARD_LIMIT*MAX_CLIPBOARD_LIMIT_DURATION)
36 self.clipboard_greedy = False
37 self.clipboard_want_targets = False
38 self.clipboard_client_selections = CLIPBOARDS
39 self.clipboard_preferred_targets = ()
40 self.clipboard_contents_slice_fix = False
42 def cleanup(self):
43 self.cancel_clipboard_progress_timer()
45 def parse_client_caps(self, c : typedict):
46 self.clipboard_enabled = c.boolget("clipboard", False)
47 self.clipboard_notifications = c.boolget("clipboard.notifications")
48 log("client clipboard: enabled=%s, notifications=%s",
49 self.clipboard_enabled, self.clipboard_notifications)
50 self.clipboard_greedy = c.boolget("clipboard.greedy")
51 self.clipboard_want_targets = c.boolget("clipboard.want_targets")
52 self.clipboard_client_selections = c.strtupleget("clipboard.selections", CLIPBOARDS)
53 self.clipboard_contents_slice_fix = c.boolget("clipboard.contents-slice-fix")
54 self.clipboard_preferred_targets = c.strtupleget("clipboard.preferred-targets", ())
55 log("client clipboard: greedy=%s, want_targets=%s, client_selections=%s, contents_slice_fix=%s",
56 self.clipboard_greedy, self.clipboard_want_targets,
57 self.clipboard_client_selections, self.clipboard_contents_slice_fix)
58 if self.clipboard_enabled and not self.clipboard_contents_slice_fix:
59 log.info("client clipboard does not include contents slice fix")
61 def get_info(self) -> dict:
62 return {
63 "clipboard" : {
64 "enabled" : self.clipboard_enabled,
65 "notifications" : self.clipboard_notifications,
66 "greedy" : self.clipboard_greedy,
67 "want-targets" : self.clipboard_want_targets,
68 "preferred-targets" : self.clipboard_preferred_targets,
69 "selections" : self.clipboard_client_selections,
70 "contents-slice-fix" : self.clipboard_contents_slice_fix,
71 },
72 }
75 def send_clipboard_enabled(self, reason=""):
76 if not self.hello_sent:
77 return
78 self.send_async("set-clipboard-enabled", self.clipboard_enabled, reason)
80 def cancel_clipboard_progress_timer(self):
81 cpt = self.clipboard_progress_timer
82 if cpt:
83 self.clipboard_progress_timer = None
84 self.source_remove(cpt)
86 def send_clipboard_progress(self, count : int):
87 if not self.clipboard_notifications or not self.hello_sent or self.clipboard_progress_timer:
88 return
89 #always set "pending" to the latest value:
90 self.clipboard_notifications_pending = count
91 #but send the latest value via a timer to tame toggle storms:
92 def may_send_progress_update():
93 self.clipboard_progress_timer = None
94 if self.clipboard_notifications_current!=self.clipboard_notifications_pending:
95 self.clipboard_notifications_current = self.clipboard_notifications_pending
96 log("sending clipboard-pending-requests=%s to %s", self.clipboard_notifications_current, self)
97 self.send_more("clipboard-pending-requests", self.clipboard_notifications_current)
98 delay = (count==0)*100
99 self.clipboard_progress_timer = self.timeout_add(delay, may_send_progress_update)
101 def send_clipboard(self, packet):
102 if not self.clipboard_enabled or not self.hello_sent:
103 return
104 if getattr(self, "suspended", False):
105 return
106 now = monotonic_time()
107 self.clipboard_stats.append(now)
108 if len(self.clipboard_stats)>=MAX_CLIPBOARD_LIMIT:
109 event = self.clipboard_stats[-MAX_CLIPBOARD_LIMIT]
110 elapsed = now-event
111 log("send_clipboard(..) elapsed=%.2f, clipboard_stats=%s", elapsed, self.clipboard_stats)
112 if elapsed<1:
113 msg = "more than %s clipboard requests per second!" % MAX_CLIPBOARD_LIMIT
114 log.warn("Warning: %s", msg)
115 #disable if this rate is sustained for more than S seconds:
116 events = [x for x in self.clipboard_stats if x>(now-MAX_CLIPBOARD_LIMIT_DURATION)]
117 log("%i events in the last %i seconds: %s", len(events), MAX_CLIPBOARD_LIMIT_DURATION, events)
118 if len(events)>=MAX_CLIPBOARD_LIMIT*MAX_CLIPBOARD_LIMIT_DURATION:
119 log.warn(" limit sustained for more than %i seconds,", MAX_CLIPBOARD_LIMIT_DURATION)
120 return
121 #call compress_clibboard via the encode work queue:
122 self.queue_encode((True, self.compress_clipboard, packet))
124 def compress_clipboard(self, packet):
125 from xpra.net.compression import Compressible, compressed_wrapper
126 #Note: this runs in the 'encode' thread!
127 packet = list(packet)
128 for i, item in enumerate(packet):
129 if isinstance(item, Compressible):
130 if self.brotli:
131 packet[i] = compressed_wrapper(item.datatype, item.data,
132 level=9, brotli=True, can_inline=False)
133 else:
134 packet[i] = self.compressed_wrapper(item.datatype, item.data)
135 self.queue_packet(packet)