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