1# -*- coding: utf-8 -*- 2# This file is part of Xpra. 3# Copyright (C) 2012-2021 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. 6 7import os 8 9from xpra.server.window import batch_config 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 17 18log = Logger("shadow") 19notifylog = Logger("notify") 20mouselog = Logger("mouse") 21cursorlog = Logger("cursor") 22 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) 29 30 31SHADOWSERVER_BASE_CLASS = object 32if server_features.rfb: 33 from xpra.server.rfb.rfb_server import RFBServer 34 SHADOWSERVER_BASE_CLASS = RFBServer 35 36 37class ShadowServerBase(SHADOWSERVER_BASE_CLASS): 38 39 def __init__(self, root_window, capture=None): 40 super().__init__() 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 batch_config.ALWAYS = True #always batch 54 batch_config.MIN_DELAY = 50 #never lower than 50ms 55 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() 68 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() 74 75 def cleanup(self): 76 for wid in self.mapped: 77 self.stop_refresh(wid) 78 self.cleanup_notifier() 79 self.cleanup_capture() 80 81 def cleanup_capture(self): 82 capture = self.capture 83 if capture: 84 self.capture = None 85 capture.clean() 86 87 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) 92 93 def get_server_mode(self): 94 return "shadow" 95 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) 102 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) 119 120 def make_hello(self, _source): 121 return {"shadow" : True} 122 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 } 129 130 131 def get_window_position(self, _window): 132 #we export the whole desktop as a window: 133 return 0, 0 134 135 def _keys_changed(self, *_args): 136 log.info("keymap has been changed") 137 138 def timeout_add(self, *args): 139 #usually done via gobject 140 raise NotImplementedError("subclasses should define this method!") 141 142 def source_remove(self, *args): 143 #usually done via gobject 144 raise NotImplementedError("subclasses should define this method!") 145 146 147 ############################################################################ 148 # notifications 149 def cleanup_notifier(self): 150 n = self.notifier 151 if n: 152 self.notifier = None 153 n.cleanup() 154 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") 160 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) 171 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() 179 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) 196 197 def get_notification_tray(self): 198 return None 199 200 def notify_startup_complete(self): 201 self.do_notify_startup("Xpra shadow server is ready", replaces_nid=XPRA_STARTUP_NOTIFICATION_ID) 202 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) 218 219 220 ############################################################################ 221 # refresh 222 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() 230 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) 238 239 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() 249 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) 256 257 def refresh(self): 258 raise NotImplementedError() 259 260 261 ############################################################################ 262 # pointer polling 263 264 def get_pointer_position(self): 265 raise NotImplementedError() 266 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) 274 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) 281 282 def poll_pointer(self): 283 self.poll_pointer_position() 284 if CURSORS: 285 self.poll_cursor() 286 return True 287 288 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)) 314 315 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.window_sources(): 347 ss.send_cursor() 348 349 def do_get_cursor_data(self): 350 #this method is overriden in subclasses with platform specific code 351 return None 352 353 def get_cursor_data(self): 354 #return cached value we get from polling: 355 return self.last_cursor_data 356 357 358 ############################################################################ 359 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 372 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() 379 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]) 385 386 387 def set_keyboard_repeat(self, key_repeat): 388 """ don't override the existing desktop """ 389 pass #pylint: disable=unnecessary-pass 390 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() 395 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) 404 405 def makeRootWindowModels(self): 406 return (RootWindowModel(self.root),) 407 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)) 414 415 416 def _add_new_window(self, window): 417 self._add_new_window_common(window) 418 self._send_new_window_packet(window) 419 420 def _send_new_window_packet(self, window): 421 geometry = window.get_geometry() 422 self._do_send_new_window_packet("new-window", window, geometry) 423 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 428 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) 437 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) 446 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]) 454 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") 459 460 461 def do_make_screenshot_packet(self): 462 raise NotImplementedError() 463 464 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(":")) 468