Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/shadow/shadow_server_base.py : 68%
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) 2012-2019 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.
7import os
9from xpra.server.window.batch_config import DamageBatchConfig
10from xpra.server.shadow.root_window_model import RootWindowModel
11from xpra.notifications.common import parse_image_path
12from xpra.platform.gui import get_native_notifier_classes, get_wm_name
13from xpra.platform.paths import get_icon_dir
14from xpra.server import server_features
15from xpra.util import envint, envbool, DONE, XPRA_STARTUP_NOTIFICATION_ID, XPRA_NEW_USER_NOTIFICATION_ID
16from xpra.log import Logger
18log = Logger("shadow")
19notifylog = Logger("notify")
20mouselog = Logger("mouse")
21cursorlog = Logger("cursor")
23REFRESH_DELAY = envint("XPRA_SHADOW_REFRESH_DELAY", 50)
24NATIVE_NOTIFIER = envbool("XPRA_NATIVE_NOTIFIER", True)
25POLL_POINTER = envint("XPRA_POLL_POINTER", 20)
26CURSORS = envbool("XPRA_CURSORS", True)
27SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False)
28NOTIFY_STARTUP = envbool("XPRA_SHADOW_NOTIFY_STARTUP", True)
31SHADOWSERVER_BASE_CLASS = object
32if server_features.rfb:
33 from xpra.server.rfb.rfb_server import RFBServer
34 SHADOWSERVER_BASE_CLASS = RFBServer
37class ShadowServerBase(SHADOWSERVER_BASE_CLASS):
39 def __init__(self, root_window, capture=None):
40 SHADOWSERVER_BASE_CLASS.__init__(self)
41 self.capture = capture
42 self.root = root_window
43 self.mapped = []
44 self.pulseaudio = False
45 self.sharing = True
46 self.refresh_delay = REFRESH_DELAY
47 self.refresh_timer = None
48 self.notifications = False
49 self.notifier = None
50 self.pointer_last_position = None
51 self.pointer_poll_timer = None
52 self.last_cursor_data = None
53 DamageBatchConfig.ALWAYS = True #always batch
54 DamageBatchConfig.MIN_DELAY = 50 #never lower than 50ms
56 def init(self, opts):
57 if SHADOWSERVER_BASE_CLASS!=object:
58 #RFBServer:
59 SHADOWSERVER_BASE_CLASS.init(self, opts)
60 self.notifications = bool(opts.notifications)
61 if self.notifications:
62 self.make_notifier()
63 log("init(..) session_name=%s", opts.session_name)
64 if opts.session_name:
65 self.session_name = opts.session_name
66 else:
67 self.guess_session_name()
69 def run(self):
70 if NOTIFY_STARTUP:
71 from gi.repository import GLib
72 GLib.timeout_add(1000, self.notify_startup_complete)
73 return super().run()
75 def cleanup(self):
76 for wid in self.mapped:
77 self.stop_refresh(wid)
78 self.cleanup_notifier()
79 self.cleanup_capture()
81 def cleanup_capture(self):
82 capture = self.capture
83 if capture:
84 self.capture = None
85 capture.clean()
88 def guess_session_name(self, procs=None):
89 log("guess_session_name(%s)", procs)
90 self.session_name = get_wm_name() # pylint: disable=assignment-from-none
91 log("get_wm_name()=%s", self.session_name)
93 def get_server_mode(self):
94 return "GTK3 shadow"
96 def print_screen_info(self):
97 if not server_features.display:
98 return
99 w, h = self.root.get_geometry()[2:4]
100 display = os.environ.get("DISPLAY")
101 self.do_print_screen_info(display, w, h)
103 def do_print_screen_info(self, display, w, h):
104 if display:
105 log.info(" on display '%s' of size %ix%i", display, w, h)
106 else:
107 log.info(" on display of size %ix%i", w, h)
108 try:
109 l = len(self._id_to_window)
110 except AttributeError as e:
111 log("no screen info: %s", e)
112 return
113 if l>1:
114 log.info(" with %i monitors:", l)
115 for window in self._id_to_window.values():
116 title = window.get_property("title")
117 x, y, w, h = window.geometry
118 log.info(" %-16s %4ix%-4i at %4i,%-4i", title, w, h, x, y)
120 def make_hello(self, _source):
121 return {"shadow" : True}
123 def get_info(self, _proto=None):
124 return {
125 "sharing" : self.sharing,
126 "refresh-delay" : self.refresh_delay,
127 "pointer-last-position" : self.pointer_last_position,
128 }
131 def get_window_position(self, _window):
132 #we export the whole desktop as a window:
133 return 0, 0
135 def watch_keymap_changes(self):
136 pass
138 def timeout_add(self, *args):
139 #usually done via gobject
140 raise NotImplementedError("subclasses should define this method!")
142 def source_remove(self, *args):
143 #usually done via gobject
144 raise NotImplementedError("subclasses should define this method!")
147 ############################################################################
148 # notifications
149 def cleanup_notifier(self):
150 n = self.notifier
151 if n:
152 self.notifier = None
153 n.cleanup()
155 def notify_setup_error(self, exception):
156 notifylog("notify_setup_error(%s)", exception)
157 notifylog.info("notification forwarding is not available")
158 if str(exception).endswith("is already claimed on the session bus"):
159 log.info(" the interface is already claimed")
161 def make_notifier(self):
162 nc = self.get_notifier_classes()
163 notifylog("make_notifier() notifier classes: %s", nc)
164 for x in nc:
165 try:
166 self.notifier = x()
167 notifylog("notifier=%s", self.notifier)
168 break
169 except Exception:
170 notifylog("failed to instantiate %s", x, exc_info=True)
172 def get_notifier_classes(self):
173 #subclasses will generally add their toolkit specific variants
174 #by overriding this method
175 #use the native ones first:
176 if not NATIVE_NOTIFIER:
177 return []
178 return get_native_notifier_classes()
180 def notify_new_user(self, ss):
181 #overriden here so we can show the notification
182 #directly on the screen we shadow
183 notifylog("notify_new_user(%s) notifier=%s", ss, self.notifier)
184 if self.notifier:
185 tray = self.get_notification_tray() #pylint: disable=assignment-from-none
186 nid = XPRA_NEW_USER_NOTIFICATION_ID
187 title = "User '%s' connected to the session" % (ss.name or ss.username or ss.uuid)
188 body = "\n".join(ss.get_connect_info())
189 actions = []
190 hints = {}
191 icon = None
192 icon_filename = os.path.join(get_icon_dir(), "user.png")
193 if os.path.exists(icon_filename):
194 icon = parse_image_path(icon_filename)
195 self.notifier.show_notify("", tray, nid, "Xpra", 0, "", title, body, actions, hints, 10*1000, icon)
197 def get_notification_tray(self):
198 return None
200 def notify_startup_complete(self):
201 self.do_notify_startup("Xpra shadow server is ready", replaces_nid=XPRA_STARTUP_NOTIFICATION_ID)
203 def do_notify_startup(self, title, body="", replaces_nid=0):
204 #overriden here so we can show the notification
205 #directly on the screen we shadow
206 notifylog("do_notify_startup%s", (title, body, replaces_nid))
207 if self.notifier:
208 tray = self.get_notification_tray() #pylint: disable=assignment-from-none
209 nid = XPRA_STARTUP_NOTIFICATION_ID
210 actions = []
211 hints = {}
212 icon = None
213 icon_filename = os.path.join(get_icon_dir(), "server-connected.png")
214 if os.path.exists(icon_filename):
215 icon = parse_image_path(icon_filename)
216 self.notifier.show_notify("", tray, nid, "Xpra", replaces_nid, "",
217 title, body, actions, hints, 10*1000, icon)
220 ############################################################################
221 # refresh
223 def start_refresh(self, wid):
224 log("start_refresh(%i) mapped=%s, timer=%s", wid, self.mapped, self.refresh_timer)
225 if wid not in self.mapped:
226 self.mapped.append(wid)
227 if not self.refresh_timer:
228 self.refresh_timer = self.timeout_add(self.refresh_delay, self.refresh)
229 self.start_poll_pointer()
231 def set_refresh_delay(self, v):
232 assert 0<v<10000
233 self.refresh_delay = v
234 if self.mapped:
235 self.cancel_refresh_timer()
236 for wid in self.mapped:
237 self.start_refresh(wid)
240 def stop_refresh(self, wid):
241 log("stop_refresh(%i) mapped=%s", wid, self.mapped)
242 try:
243 self.mapped.remove(wid)
244 except KeyError:
245 pass
246 if not self.mapped:
247 self.cancel_refresh_timer()
248 self.cancel_poll_pointer()
250 def cancel_refresh_timer(self):
251 t = self.refresh_timer
252 log("cancel_refresh_timer() timer=%s", t)
253 if t:
254 self.refresh_timer = None
255 self.source_remove(t)
257 def refresh(self):
258 raise NotImplementedError()
261 ############################################################################
262 # pointer polling
264 def get_pointer_position(self):
265 raise NotImplementedError()
267 def start_poll_pointer(self):
268 log("start_poll_pointer() pointer_poll_timer=%s, input_devices=%s, POLL_POINTER=%s",
269 self.pointer_poll_timer, server_features.input_devices, POLL_POINTER)
270 if self.pointer_poll_timer:
271 self.cancel_poll_pointer()
272 if server_features.input_devices and POLL_POINTER>0:
273 self.pointer_poll_timer = self.timeout_add(POLL_POINTER, self.poll_pointer)
275 def cancel_poll_pointer(self):
276 ppt = self.pointer_poll_timer
277 log("cancel_poll_pointer() pointer_poll_timer=%s", ppt)
278 if ppt:
279 self.pointer_poll_timer = None
280 self.source_remove(ppt)
282 def poll_pointer(self):
283 self.poll_pointer_position()
284 if CURSORS:
285 self.poll_cursor()
286 return True
289 def poll_pointer_position(self):
290 x, y = self.get_pointer_position()
291 #find the window model containing the pointer:
292 if self.pointer_last_position!=(x, y):
293 self.pointer_last_position = (x, y)
294 rwm = None
295 wid = None
296 rx, ry = 0, 0
297 for wid, window in self._id_to_window.items():
298 wx, wy, ww, wh = window.geometry
299 if wx<=x<(wx+ww) and wy<=y<(wy+wh):
300 rwm = window
301 rx = x-wx
302 ry = y-wy
303 break
304 if rwm:
305 mouselog("poll_pointer_position() wid=%i, position=%s, relative=%s", wid, (x, y), (rx, ry))
306 for ss in self._server_sources.values():
307 um = getattr(ss, "update_mouse", None)
308 if um:
309 um(wid, x, y, rx, ry)
310 else:
311 mouselog("poll_pointer_position() model not found for position=%s", (x, y))
312 else:
313 mouselog("poll_pointer_position() unchanged position=%s", (x, y))
316 def poll_cursor(self):
317 prev = self.last_cursor_data
318 curr = self.do_get_cursor_data() #pylint: disable=assignment-from-none
319 self.last_cursor_data = curr
320 def cmpv(lcd):
321 if not lcd:
322 return None
323 v = lcd[0]
324 if v and len(v)>2:
325 return v[2:]
326 return None
327 if cmpv(prev)!=cmpv(curr):
328 fields = ("x", "y", "width", "height", "xhot", "yhot", "serial", "pixels", "name")
329 if len(prev or [])==len(curr or []) and len(prev or [])==len(fields):
330 diff = []
331 for i, prev_value in enumerate(prev):
332 if prev_value!=curr[i]:
333 diff.append(fields[i])
334 cursorlog("poll_cursor() attributes changed: %s", diff)
335 if SAVE_CURSORS and curr:
336 ci = curr[0]
337 if ci:
338 w = ci[2]
339 h = ci[3]
340 serial = ci[6]
341 pixels = ci[7]
342 cursorlog("saving cursor %#x with size %ix%i, %i bytes", serial, w, h, len(pixels))
343 from PIL import Image
344 img = Image.frombuffer("RGBA", (w, h), pixels, "raw", "BGRA", 0, 1)
345 img.save("cursor-%#x.png" % serial, format="PNG")
346 for ss in self._server_sources.values():
347 ss.send_cursor()
349 def do_get_cursor_data(self):
350 #this method is overriden in subclasses with platform specific code
351 return None
353 def get_cursor_data(self):
354 #return cached value we get from polling:
355 return self.last_cursor_data
358 ############################################################################
360 def sanity_checks(self, _proto, c):
361 server_uuid = c.strget("server_uuid")
362 if server_uuid:
363 if server_uuid==self.uuid:
364 log.warn("Warning: shadowing your own display can be quite confusing")
365 clipboard = self._clipboard_helper and c.boolget("clipboard", True)
366 if clipboard:
367 log.warn("clipboard sharing cannot be enabled! (consider using the --no-clipboard option)")
368 c["clipboard"] = False
369 else:
370 log.warn("This client is running within the Xpra server %s", server_uuid)
371 return True
373 def parse_screen_info(self, ss):
374 try:
375 log.info(" client root window size is %sx%s", *ss.desktop_size)
376 except Exception:
377 log.info(" unknown client desktop size")
378 return self.get_root_window_size()
380 def _process_desktop_size(self, proto, packet):
381 #just record the screen size info in the source
382 ss = self.get_server_source(proto)
383 if ss and len(packet)>=4:
384 ss.set_screen_sizes(packet[3])
387 def set_keyboard_repeat(self, key_repeat):
388 """ don't override the existing desktop """
389 pass #pylint: disable=unnecessary-pass
391 def set_keymap(self, server_source, force=False):
392 log("set_keymap%s", (server_source, force))
393 log.info("shadow server: setting default keymap translation")
394 self.keyboard_config = server_source.set_default_keymap()
396 def load_existing_windows(self):
397 self.min_mmap_size = 1024*1024*4*2
398 for i,model in enumerate(self.makeRootWindowModels()):
399 log("load_existing_windows() root window model %i: %s", i, model)
400 self._add_new_window(model)
401 #at least big enough for 2 frames of BGRX pixel data:
402 w, h = model.get_dimensions()
403 self.min_mmap_size = max(self.min_mmap_size, w*h*4*2)
405 def makeRootWindowModels(self):
406 return (RootWindowModel(self.root),)
408 def send_initial_windows(self, ss, sharing=False):
409 log("send_initial_windows(%s, %s) will send: %s", ss, sharing, self._id_to_window)
410 for wid in sorted(self._id_to_window.keys()):
411 window = self._id_to_window[wid]
412 w, h = window.get_dimensions()
413 ss.new_window("new-window", wid, window, 0, 0, w, h, self.client_properties.get(wid, {}).get(ss.uuid))
416 def _add_new_window(self, window):
417 self._add_new_window_common(window)
418 self._send_new_window_packet(window)
420 def _send_new_window_packet(self, window):
421 geometry = window.get_geometry()
422 self._do_send_new_window_packet("new-window", window, geometry)
424 def _process_window_common(self, wid):
425 window = self._id_to_window.get(wid)
426 assert window is not None, "wid %s does not exist" % wid
427 return window
429 def _process_map_window(self, proto, packet):
430 wid, x, y, width, height = packet[1:6]
431 window = self._process_window_common(wid)
432 self._window_mapped_at(proto, wid, window, (x, y, width, height))
433 self.refresh_window_area(window, 0, 0, width, height)
434 if len(packet)>=7:
435 self._set_client_properties(proto, wid, window, packet[6])
436 self.start_refresh(wid)
438 def _process_unmap_window(self, proto, packet):
439 wid = packet[1]
440 window = self._process_window_common(wid)
441 self._window_mapped_at(proto, wid, window)
442 #TODO: deal with more than one window / more than one client
443 #and stop refresh if all the windows are unmapped everywhere
444 if len(self._server_sources)<=1 and len(self._id_to_window)<=1:
445 self.stop_refresh(wid)
447 def _process_configure_window(self, proto, packet):
448 wid, x, y, w, h = packet[1:6]
449 window = self._process_window_common(wid)
450 self._window_mapped_at(proto, wid, window, (x, y, w, h))
451 self.refresh_window_area(window, 0, 0, w, h)
452 if len(packet)>=7:
453 self._set_client_properties(proto, wid, window, packet[6])
455 def _process_close_window(self, proto, packet):
456 wid = packet[1]
457 self._process_window_common(wid)
458 self.disconnect_client(proto, DONE, "closed the only window")
461 def do_make_screenshot_packet(self):
462 raise NotImplementedError()
465 def make_dbus_server(self):
466 from xpra.server.shadow.shadow_dbus_server import Shadow_DBUS_Server
467 return Shadow_DBUS_Server(self, os.environ.get("DISPLAY", "").lstrip(":"))