# -*- coding: utf-8 -*- # This file is part of Xpra. # Copyright (C) 2012-2021 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. import os from xpra.server.window import batch_config from xpra.server.shadow.root_window_model import RootWindowModel from xpra.notifications.common import parse_image_path from xpra.platform.gui import get_native_notifier_classes, get_wm_name from xpra.platform.paths import get_icon_dir from xpra.server import server_features from xpra.util import envint, envbool, DONE, XPRA_STARTUP_NOTIFICATION_ID, XPRA_NEW_USER_NOTIFICATION_ID from xpra.log import Logger log = Logger("shadow") notifylog = Logger("notify") mouselog = Logger("mouse") cursorlog = Logger("cursor") REFRESH_DELAY = envint("XPRA_SHADOW_REFRESH_DELAY", 50) NATIVE_NOTIFIER = envbool("XPRA_NATIVE_NOTIFIER", True) POLL_POINTER = envint("XPRA_POLL_POINTER", 20) CURSORS = envbool("XPRA_CURSORS", True) SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False) NOTIFY_STARTUP = envbool("XPRA_SHADOW_NOTIFY_STARTUP", True) SHADOWSERVER_BASE_CLASS = object if server_features.rfb: from xpra.server.rfb.rfb_server import RFBServer SHADOWSERVER_BASE_CLASS = RFBServer class ShadowServerBase(SHADOWSERVER_BASE_CLASS): def __init__(self, root_window, capture=None): super().__init__() self.capture = capture self.root = root_window self.mapped = [] self.pulseaudio = False self.sharing = True self.refresh_delay = REFRESH_DELAY self.refresh_timer = None self.notifications = False self.notifier = None self.pointer_last_position = None self.pointer_poll_timer = None self.last_cursor_data = None batch_config.ALWAYS = True #always batch batch_config.MIN_DELAY = 50 #never lower than 50ms def init(self, opts): if SHADOWSERVER_BASE_CLASS!=object: #RFBServer: SHADOWSERVER_BASE_CLASS.init(self, opts) self.notifications = bool(opts.notifications) if self.notifications: self.make_notifier() log("init(..) session_name=%s", opts.session_name) if opts.session_name: self.session_name = opts.session_name else: self.guess_session_name() def run(self): if NOTIFY_STARTUP: from gi.repository import GLib GLib.timeout_add(1000, self.notify_startup_complete) return super().run() def cleanup(self): for wid in self.mapped: self.stop_refresh(wid) self.cleanup_notifier() self.cleanup_capture() def cleanup_capture(self): capture = self.capture if capture: self.capture = None capture.clean() def guess_session_name(self, procs=None): log("guess_session_name(%s)", procs) self.session_name = get_wm_name() # pylint: disable=assignment-from-none log("get_wm_name()=%s", self.session_name) def get_server_mode(self): return "shadow" def print_screen_info(self): if not server_features.display: return w, h = self.root.get_geometry()[2:4] display = os.environ.get("DISPLAY") self.do_print_screen_info(display, w, h) def do_print_screen_info(self, display, w, h): if display: log.info(" on display '%s' of size %ix%i", display, w, h) else: log.info(" on display of size %ix%i", w, h) try: l = len(self._id_to_window) except AttributeError as e: log("no screen info: %s", e) return if l>1: log.info(" with %i monitors:", l) for window in self._id_to_window.values(): title = window.get_property("title") x, y, w, h = window.geometry log.info(" %-16s %4ix%-4i at %4i,%-4i", title, w, h, x, y) def make_hello(self, _source): return {"shadow" : True} def get_info(self, _proto=None): return { "sharing" : self.sharing, "refresh-delay" : self.refresh_delay, "pointer-last-position" : self.pointer_last_position, } def get_window_position(self, _window): #we export the whole desktop as a window: return 0, 0 def _keys_changed(self, *_args): log.info("keymap has been changed") def timeout_add(self, *args): #usually done via gobject raise NotImplementedError("subclasses should define this method!") def source_remove(self, *args): #usually done via gobject raise NotImplementedError("subclasses should define this method!") ############################################################################ # notifications def cleanup_notifier(self): n = self.notifier if n: self.notifier = None n.cleanup() def notify_setup_error(self, exception): notifylog("notify_setup_error(%s)", exception) notifylog.info("notification forwarding is not available") if str(exception).endswith("is already claimed on the session bus"): log.info(" the interface is already claimed") def make_notifier(self): nc = self.get_notifier_classes() notifylog("make_notifier() notifier classes: %s", nc) for x in nc: try: self.notifier = x() notifylog("notifier=%s", self.notifier) break except Exception: notifylog("failed to instantiate %s", x, exc_info=True) def get_notifier_classes(self): #subclasses will generally add their toolkit specific variants #by overriding this method #use the native ones first: if not NATIVE_NOTIFIER: return [] return get_native_notifier_classes() def notify_new_user(self, ss): #overriden here so we can show the notification #directly on the screen we shadow notifylog("notify_new_user(%s) notifier=%s", ss, self.notifier) if self.notifier: tray = self.get_notification_tray() #pylint: disable=assignment-from-none nid = XPRA_NEW_USER_NOTIFICATION_ID title = "User '%s' connected to the session" % (ss.name or ss.username or ss.uuid) body = "\n".join(ss.get_connect_info()) actions = [] hints = {} icon = None icon_filename = os.path.join(get_icon_dir(), "user.png") if os.path.exists(icon_filename): icon = parse_image_path(icon_filename) self.notifier.show_notify("", tray, nid, "Xpra", 0, "", title, body, actions, hints, 10*1000, icon) def get_notification_tray(self): return None def notify_startup_complete(self): self.do_notify_startup("Xpra shadow server is ready", replaces_nid=XPRA_STARTUP_NOTIFICATION_ID) def do_notify_startup(self, title, body="", replaces_nid=0): #overriden here so we can show the notification #directly on the screen we shadow notifylog("do_notify_startup%s", (title, body, replaces_nid)) if self.notifier: tray = self.get_notification_tray() #pylint: disable=assignment-from-none nid = XPRA_STARTUP_NOTIFICATION_ID actions = [] hints = {} icon = None icon_filename = os.path.join(get_icon_dir(), "server-connected.png") if os.path.exists(icon_filename): icon = parse_image_path(icon_filename) self.notifier.show_notify("", tray, nid, "Xpra", replaces_nid, "", title, body, actions, hints, 10*1000, icon) ############################################################################ # refresh def start_refresh(self, wid): log("start_refresh(%i) mapped=%s, timer=%s", wid, self.mapped, self.refresh_timer) if wid not in self.mapped: self.mapped.append(wid) if not self.refresh_timer: self.refresh_timer = self.timeout_add(self.refresh_delay, self.refresh) self.start_poll_pointer() def set_refresh_delay(self, v): assert 00: self.pointer_poll_timer = self.timeout_add(POLL_POINTER, self.poll_pointer) def cancel_poll_pointer(self): ppt = self.pointer_poll_timer log("cancel_poll_pointer() pointer_poll_timer=%s", ppt) if ppt: self.pointer_poll_timer = None self.source_remove(ppt) def poll_pointer(self): self.poll_pointer_position() if CURSORS: self.poll_cursor() return True def poll_pointer_position(self): x, y = self.get_pointer_position() #find the window model containing the pointer: if self.pointer_last_position!=(x, y): self.pointer_last_position = (x, y) rwm = None wid = None rx, ry = 0, 0 for wid, window in self._id_to_window.items(): wx, wy, ww, wh = window.geometry if wx<=x<(wx+ww) and wy<=y<(wy+wh): rwm = window rx = x-wx ry = y-wy break if rwm: mouselog("poll_pointer_position() wid=%i, position=%s, relative=%s", wid, (x, y), (rx, ry)) for ss in self._server_sources.values(): um = getattr(ss, "update_mouse", None) if um: um(wid, x, y, rx, ry) else: mouselog("poll_pointer_position() model not found for position=%s", (x, y)) else: mouselog("poll_pointer_position() unchanged position=%s", (x, y)) def poll_cursor(self): prev = self.last_cursor_data curr = self.do_get_cursor_data() #pylint: disable=assignment-from-none self.last_cursor_data = curr def cmpv(lcd): if not lcd: return None v = lcd[0] if v and len(v)>2: return v[2:] return None if cmpv(prev)!=cmpv(curr): fields = ("x", "y", "width", "height", "xhot", "yhot", "serial", "pixels", "name") if len(prev or [])==len(curr or []) and len(prev or [])==len(fields): diff = [] for i, prev_value in enumerate(prev): if prev_value!=curr[i]: diff.append(fields[i]) cursorlog("poll_cursor() attributes changed: %s", diff) if SAVE_CURSORS and curr: ci = curr[0] if ci: w = ci[2] h = ci[3] serial = ci[6] pixels = ci[7] cursorlog("saving cursor %#x with size %ix%i, %i bytes", serial, w, h, len(pixels)) from PIL import Image img = Image.frombuffer("RGBA", (w, h), pixels, "raw", "BGRA", 0, 1) img.save("cursor-%#x.png" % serial, format="PNG") for ss in self.window_sources(): ss.send_cursor() def do_get_cursor_data(self): #this method is overriden in subclasses with platform specific code return None def get_cursor_data(self): #return cached value we get from polling: return self.last_cursor_data ############################################################################ def sanity_checks(self, _proto, c): server_uuid = c.strget("server_uuid") if server_uuid: if server_uuid==self.uuid: log.warn("Warning: shadowing your own display can be quite confusing") clipboard = self._clipboard_helper and c.boolget("clipboard", True) if clipboard: log.warn("clipboard sharing cannot be enabled! (consider using the --no-clipboard option)") c["clipboard"] = False else: log.warn("This client is running within the Xpra server %s", server_uuid) return True def parse_screen_info(self, ss): try: log.info(" client root window size is %sx%s", *ss.desktop_size) except Exception: log.info(" unknown client desktop size") return self.get_root_window_size() def _process_desktop_size(self, proto, packet): #just record the screen size info in the source ss = self.get_server_source(proto) if ss and len(packet)>=4: ss.set_screen_sizes(packet[3]) def set_keyboard_repeat(self, key_repeat): """ don't override the existing desktop """ pass #pylint: disable=unnecessary-pass def set_keymap(self, server_source, force=False): log("set_keymap%s", (server_source, force)) log.info("shadow server: setting default keymap translation") self.keyboard_config = server_source.set_default_keymap() def load_existing_windows(self): self.min_mmap_size = 1024*1024*4*2 for i,model in enumerate(self.makeRootWindowModels()): log("load_existing_windows() root window model %i: %s", i, model) self._add_new_window(model) #at least big enough for 2 frames of BGRX pixel data: w, h = model.get_dimensions() self.min_mmap_size = max(self.min_mmap_size, w*h*4*2) def makeRootWindowModels(self): return (RootWindowModel(self.root),) def send_initial_windows(self, ss, sharing=False): log("send_initial_windows(%s, %s) will send: %s", ss, sharing, self._id_to_window) for wid in sorted(self._id_to_window.keys()): window = self._id_to_window[wid] w, h = window.get_dimensions() ss.new_window("new-window", wid, window, 0, 0, w, h, self.client_properties.get(wid, {}).get(ss.uuid)) def _add_new_window(self, window): self._add_new_window_common(window) self._send_new_window_packet(window) def _send_new_window_packet(self, window): geometry = window.get_geometry() self._do_send_new_window_packet("new-window", window, geometry) def _process_window_common(self, wid): window = self._id_to_window.get(wid) assert window is not None, "wid %s does not exist" % wid return window def _process_map_window(self, proto, packet): wid, x, y, width, height = packet[1:6] window = self._process_window_common(wid) self._window_mapped_at(proto, wid, window, (x, y, width, height)) self.refresh_window_area(window, 0, 0, width, height) if len(packet)>=7: self._set_client_properties(proto, wid, window, packet[6]) self.start_refresh(wid) def _process_unmap_window(self, proto, packet): wid = packet[1] window = self._process_window_common(wid) self._window_mapped_at(proto, wid, window) #TODO: deal with more than one window / more than one client #and stop refresh if all the windows are unmapped everywhere if len(self._server_sources)<=1 and len(self._id_to_window)<=1: self.stop_refresh(wid) def _process_configure_window(self, proto, packet): wid, x, y, w, h = packet[1:6] window = self._process_window_common(wid) self._window_mapped_at(proto, wid, window, (x, y, w, h)) self.refresh_window_area(window, 0, 0, w, h) if len(packet)>=7: self._set_client_properties(proto, wid, window, packet[6]) def _process_close_window(self, proto, packet): wid = packet[1] self._process_window_common(wid) self.disconnect_client(proto, DONE, "closed the only window") def do_make_screenshot_packet(self): raise NotImplementedError() def make_dbus_server(self): from xpra.server.shadow.shadow_dbus_server import Shadow_DBUS_Server return Shadow_DBUS_Server(self, os.environ.get("DISPLAY", "").lstrip(":"))