Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/mixins/display_manager.py : 61%
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# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7from xpra.os_util import strtobytes
8from xpra.util import engs, iround, log_screen_sizes
9from xpra.os_util import bytestostr
10from xpra.scripts.config import FALSE_OPTIONS
11from xpra.server.mixins.stub_server_mixin import StubServerMixin
12from xpra.log import Logger
14log = Logger("screen")
15gllog = Logger("opengl")
17"""
18Mixin for servers that handle displays.
19"""
20class DisplayManager(StubServerMixin):
22 def __init__(self):
23 self.randr = False
24 self.bell = False
25 self.cursors = False
26 self.default_dpi = 96
27 self.dpi = 0
28 self.xdpi = 0
29 self.ydpi = 0
30 self.antialias = {}
31 self.cursor_size = 0
32 self.double_click_time = -1
33 self.double_click_distance = -1, -1
34 self.opengl = False
35 self.opengl_props = {}
37 def init(self, opts):
38 self.opengl = opts.opengl
39 self.bell = opts.bell
40 self.cursors = opts.cursors
41 self.default_dpi = int(opts.dpi)
44 def parse_hello(self, ss, caps, send_ui):
45 if send_ui:
46 self.parse_screen_info(ss)
49 def last_client_exited(self):
50 self.reset_icc_profile()
53 def threaded_setup(self):
54 self.opengl_props = self.query_opengl()
57 def query_opengl(self):
58 props = {}
59 if self.opengl.lower()=="noprobe" or self.opengl.lower() in FALSE_OPTIONS:
60 gllog("query_opengl() skipped because opengl=%s", self.opengl)
61 return props
62 try:
63 from subprocess import Popen, PIPE
64 from xpra.platform.paths import get_xpra_command
65 cmd = self.get_full_child_command(get_xpra_command()+["opengl", "--opengl=yes"])
66 env = self.get_child_env()
67 #we want the output so we can parse it:
68 env["XPRA_REDIRECT_OUTPUT"] = "0"
69 proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
70 out,err = proc.communicate()
71 gllog("out(%s)=%s", cmd, out)
72 gllog("err(%s)=%s", cmd, err)
73 if proc.returncode==0:
74 #parse output:
75 for line in out.splitlines():
76 parts = line.split(b"=")
77 if len(parts)!=2:
78 continue
79 k = bytestostr(parts[0].strip())
80 v = bytestostr(parts[1].strip())
81 props[k] = v
82 gllog("opengl props=%s", props)
83 if props:
84 gllog.info("OpenGL is supported on display '%s'", self.display_name)
85 renderer = props.get("renderer")
86 if renderer:
87 gllog.info(" using '%s' renderer", renderer)
88 else:
89 gllog.info("No OpenGL information available")
90 else:
91 props["error-details"] = str(err).strip("\n\r")
92 error = "unknown error"
93 for x in str(err).splitlines():
94 if x.startswith("RuntimeError: "):
95 error = x[len("RuntimeError: "):]
96 break
97 if x.startswith("ImportError: "):
98 error = x[len("ImportError: "):]
99 break
100 props["error"] = error
101 log.warn("Warning: OpenGL support check failed:")
102 log.warn(" %s", error)
103 except Exception as e:
104 gllog("query_opengl()", exc_info=True)
105 gllog.error("Error: OpenGL support check failed")
106 gllog.error(" '%s'", e)
107 props["error"] = str(e)
108 gllog("OpenGL: %s", props)
109 return props
112 def get_caps(self, source) -> dict:
113 root_w, root_h = self.get_root_window_size()
114 caps = {
115 "bell" : self.bell,
116 "cursors" : self.cursors,
117 "desktop_size" : self._get_desktop_size_capability(source, root_w, root_h),
118 }
119 if self.opengl_props:
120 caps["opengl"] = self.opengl_props
121 return caps
123 def get_info(self, _proto) -> dict:
124 i = {
125 "randr" : self.randr,
126 "bell" : self.bell,
127 "cursors" : {
128 "" : self.cursors,
129 "size" : self.cursor_size,
130 },
131 "double-click" : {
132 "time" : self.double_click_time,
133 "distance" : self.double_click_distance,
134 },
135 "dpi" : {
136 "default" : self.default_dpi,
137 "value" : self.dpi,
138 "x" : self.xdpi,
139 "y" : self.ydpi,
140 },
141 "antialias" : self.antialias,
142 }
143 if self.opengl_props:
144 i["opengl"] = self.opengl_props
145 return {
146 "display": i,
147 }
150 def _process_set_cursors(self, proto, packet):
151 assert self.cursors, "cannot toggle send_cursors: the feature is disabled"
152 ss = self.get_server_source(proto)
153 if ss:
154 ss.send_cursors = bool(packet[1])
156 def _process_set_bell(self, proto, packet):
157 assert self.bell, "cannot toggle send_bell: the feature is disabled"
158 ss = self.get_server_source(proto)
159 if ss:
160 ss.send_bell = bool(packet[1])
163 ######################################################################
164 # display / screen / root window:
165 def set_screen_geometry_attributes(self, w, h):
166 #by default, use the screen as desktop area:
167 self.set_desktop_geometry_attributes(w, h)
169 def set_desktop_geometry_attributes(self, w, h):
170 self.calculate_desktops()
171 self.calculate_workarea(w, h)
172 self.set_desktop_geometry(w, h)
175 def parse_screen_info(self, ss):
176 return self.do_parse_screen_info(ss, ss.desktop_size)
178 def do_parse_screen_info(self, ss, desktop_size):
179 log("do_parse_screen_info%s", (ss, desktop_size))
180 dw, dh = None, None
181 if desktop_size:
182 try:
183 dw, dh = desktop_size
184 if not ss.screen_sizes:
185 log.info(" client root window size is %sx%s", dw, dh)
186 else:
187 log.info(" client root window size is %sx%s with %s display%s:",
188 dw, dh, len(ss.screen_sizes), engs(ss.screen_sizes))
189 log_screen_sizes(dw, dh, ss.screen_sizes)
190 except Exception:
191 dw, dh = None, None
192 sw, sh = self.configure_best_screen_size()
193 log("configure_best_screen_size()=%s", (sw, sh))
194 #we will tell the client about the size chosen in the hello we send back,
195 #so record this size as the current server desktop size to avoid change notifications:
196 ss.desktop_size_server = sw, sh
197 #prefer desktop size, fallback to screen size:
198 w = dw or sw
199 h = dh or sh
200 #clamp to max supported:
201 maxw, maxh = self.get_max_screen_size()
202 w = min(w, maxw)
203 h = min(h, maxh)
204 self.set_desktop_geometry_attributes(w, h)
205 self.set_icc_profile()
206 return w, h
209 def set_icc_profile(self):
210 log("set_icc_profile() not implemented")
212 def reset_icc_profile(self):
213 log("reset_icc_profile() not implemented")
216 def _monitors_changed(self, screen):
217 self.do_screen_changed(screen)
219 def _screen_size_changed(self, screen):
220 self.do_screen_changed(screen)
222 def do_screen_changed(self, screen):
223 log("do_screen_changed(%s)", screen)
224 #randr has resized the screen, tell the client (if it supports it)
225 w, h = screen.get_width(), screen.get_height()
226 log("new screen dimensions: %ix%i", w, h)
227 self.set_screen_geometry_attributes(w, h)
228 self.idle_add(self.send_updated_screen_size)
230 def get_root_window_size(self):
231 raise NotImplementedError()
233 def send_updated_screen_size(self):
234 max_w, max_h = self.get_max_screen_size()
235 root_w, root_h = self.get_root_window_size()
236 root_w = min(root_w, max_w)
237 root_h = min(root_h, max_h)
238 count = 0
239 for ss in self._server_sources.values():
240 if ss.updated_desktop_size(root_w, root_h, max_w, max_h):
241 count +=1
242 if count>0:
243 log.info("sent updated screen size to %s client%s: %sx%s (max %sx%s)",
244 count, engs(count), root_w, root_h, max_w, max_h)
246 def get_max_screen_size(self):
247 max_w, max_h = self.get_root_window_size()
248 return max_w, max_h
250 def _get_desktop_size_capability(self, server_source, root_w, root_h):
251 client_size = server_source.desktop_size
252 log("client resolution is %s, current server resolution is %sx%s", client_size, root_w, root_h)
253 if not client_size:
254 #client did not specify size, just return what we have
255 return root_w, root_h
256 client_w, client_h = client_size
257 w = min(client_w, root_w)
258 h = min(client_h, root_h)
259 return w, h
261 def configure_best_screen_size(self):
262 root_w, root_h = self.get_root_window_size()
263 return root_w, root_h
265 def _process_desktop_size(self, proto, packet):
266 log("new desktop size from %s: %s", proto, packet)
267 width, height = packet[1:3]
268 ss = self.get_server_source(proto)
269 if ss is None:
270 return
271 ss.desktop_size = (width, height)
272 if len(packet)>=10:
273 #added in 0.16 for scaled client displays:
274 xdpi, ydpi = packet[8:10]
275 if xdpi!=self.xdpi or ydpi!=self.ydpi:
276 self.xdpi, self.ydpi = xdpi, ydpi
277 log("new dpi: %ix%i", self.xdpi, self.ydpi)
278 self.dpi = iround((self.xdpi + self.ydpi)/2.0)
279 self.dpi_changed()
280 if len(packet)>=8:
281 #added in 0.16 for scaled client displays:
282 ss.desktop_size_unscaled = packet[6:8]
283 if len(packet)>=6:
284 desktops, desktop_names = packet[4:6]
285 ss.set_desktops(desktops, desktop_names)
286 self.calculate_desktops()
287 if len(packet)>=4:
288 ss.set_screen_sizes(packet[3])
289 bigger = ss.screen_resize_bigger
290 log("client requesting new size: %sx%s (bigger=%s)", width, height, bigger)
291 self.set_screen_size(width, height, bigger)
292 if len(packet)>=4:
293 log.info("received updated display dimensions")
294 log.info("client display size is %sx%s with %s screen%s:",
295 width, height, len(ss.screen_sizes), engs(ss.screen_sizes))
296 log_screen_sizes(width, height, ss.screen_sizes)
297 self.calculate_workarea(width, height)
298 #ensures that DPI and antialias information gets reset:
299 self.update_all_server_settings()
301 def dpi_changed(self):
302 pass
304 def calculate_desktops(self):
305 count = 1
306 for ss in self._server_sources.values():
307 if ss.desktops:
308 count = max(count, ss.desktops)
309 count = max(1, min(20, count))
310 names = []
311 for i in range(count):
312 if i==0:
313 name = "Main"
314 else:
315 name = "Desktop %i" % (i+1)
316 for ss in self._server_sources.values():
317 if ss.desktops and i<len(ss.desktop_names) and ss.desktop_names[i]:
318 dn = ss.desktop_names[i]
319 if isinstance(dn, str):
320 #newer clients send unicode
321 name = dn
322 else:
323 #older clients send byte strings:
324 try :
325 v = strtobytes(dn).decode("utf8")
326 except (UnicodeEncodeError, UnicodeDecodeError):
327 log.error("Error parsing '%s'", dn, exc_info=True)
328 else:
329 if v!="0" or i!=0:
330 name = v
331 names.append(name)
332 self.set_desktops(names)
334 def set_desktops(self, names):
335 pass
337 def calculate_workarea(self, w, h):
338 raise NotImplementedError()
340 def set_workarea(self, workarea):
341 pass
344 ######################################################################
345 # screenshots:
346 def _process_screenshot(self, proto, _packet):
347 packet = self.make_screenshot_packet()
348 ss = self.get_server_source(proto)
349 if packet and ss:
350 ss.send(*packet)
352 def make_screenshot_packet(self):
353 try:
354 return self.do_make_screenshot_packet()
355 except Exception:
356 log.error("make_screenshot_packet()", exc_info=True)
357 return None
359 def do_make_screenshot_packet(self):
360 raise NotImplementedError("no screenshot capability in %s" % type(self))
362 def send_screenshot(self, proto):
363 #this is a screenshot request, handle it and disconnect
364 try:
365 packet = self.make_screenshot_packet()
366 if not packet:
367 self.send_disconnect(proto, "screenshot failed")
368 return
369 proto.send_now(packet)
370 self.timeout_add(5*1000, self.send_disconnect, proto, "screenshot sent")
371 except Exception as e:
372 log.error("failed to capture screenshot", exc_info=True)
373 self.send_disconnect(proto, "screenshot failed: %s" % e)
376 def init_packet_handlers(self):
377 self.add_packet_handlers({
378 "set-cursors" : self._process_set_cursors,
379 "set-bell" : self._process_set_bell,
380 "desktop_size" : self._process_desktop_size,
381 "screenshot" : self._process_screenshot,
382 })