1# This file is part of Xpra.
2# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5
6import os
7
8from xpra.util import envbool, typedict
9from xpra.os_util import get_int_uuid
10from xpra.exit_codes import EXIT_MMAP_TOKEN_FAILURE
11from xpra.scripts.config import TRUE_OPTIONS
12from xpra.simple_stats import std_unit
13from xpra.client.mixins.stub_client_mixin import StubClientMixin
14from xpra.log import Logger
15
16log = Logger("mmap")
17
18KEEP_MMAP_FILE = envbool("XPRA_KEEP_MMAP_FILE", False)
19
20
21class MmapClient(StubClientMixin):
22    """
23    Mixin for adding mmap support to a client
24    """
25
26    def __init__(self):
27        super().__init__()
28        self.mmap_enabled = False
29        self.mmap = None
30        self.mmap_token = None
31        self.mmap_token_index = 0
32        self.mmap_token_bytes = 0
33        self.mmap_filename = None
34        self.mmap_size = 0
35        self.mmap_group = None
36        self.mmap_tempfile = None
37        self.mmap_delete = False
38        self.supports_mmap = True
39
40
41    def init(self, opts):
42        self.mmap_group = opts.mmap_group
43        if os.path.isabs(opts.mmap):
44            self.mmap_filename = opts.mmap
45            self.supports_mmap = True
46        else:
47            self.supports_mmap = opts.mmap.lower() in TRUE_OPTIONS
48
49
50    def cleanup(self):
51        self.clean_mmap()
52
53
54    def setup_connection(self, conn):
55        if self.supports_mmap:
56            self.init_mmap(self.mmap_filename, self.mmap_group, conn.filename)
57
58
59    def get_root_size(self):
60        #subclasses should provide real values
61        return 1024, 1024
62
63    def parse_server_capabilities(self, c : typedict) -> bool:
64        self.mmap_enabled = self.supports_mmap and self.mmap_enabled and c.boolget("mmap_enabled")
65        log("parse_server_capabilities(..) mmap_enabled=%s", self.mmap_enabled)
66        if self.mmap_enabled:
67            from xpra.net.mmap_pipe import read_mmap_token, DEFAULT_TOKEN_INDEX, DEFAULT_TOKEN_BYTES
68            def iget(attrname, default_value=0):
69                return c.intget("mmap_%s" % attrname) or c.intget("mmap.%s" % attrname) or default_value
70            mmap_token = iget("token")
71            mmap_token_index = iget("token_index", DEFAULT_TOKEN_INDEX)
72            mmap_token_bytes = iget("token_bytes", DEFAULT_TOKEN_BYTES)
73            token = read_mmap_token(self.mmap, mmap_token_index, mmap_token_bytes)
74            if token!=mmap_token:
75                log.error("Error: mmap token verification failed!")
76                log.error(" expected '%#x'", token)
77                log.error(" found '%#x'", mmap_token)
78                self.mmap_enabled = False
79                self.quit(EXIT_MMAP_TOKEN_FAILURE)
80                return
81            log.info("enabled fast mmap transfers using %sB shared memory area", std_unit(self.mmap_size, unit=1024))
82        #the server will have a handle on the mmap file by now, safe to delete:
83        if not KEEP_MMAP_FILE:
84            self.clean_mmap()
85        return True
86
87
88    def get_info(self):
89        if not self.mmap_enabled:
90            return {}
91        mmap_info = self.get_raw_caps()
92        mmap_info["group"] = self.mmap_group or ""
93        return {
94            "mmap" : mmap_info,
95            }
96
97    def get_caps(self) -> dict:
98        if not self.mmap_enabled:
99            return {}
100        raw_caps = self.get_raw_caps()
101        caps = {
102            "mmap" : raw_caps,
103            }
104        #pre 2.3 servers only use underscore instead of "." prefix for mmap caps:
105        for k,v in raw_caps.items():
106            caps["mmap_%s" % k] = v
107        return caps
108
109    def get_raw_caps(self):
110        return {
111            "file"          : self.mmap_filename,
112            "size"          : self.mmap_size,
113            "token"         : self.mmap_token,
114            "token_index"   : self.mmap_token_index,
115            "token_bytes"   : self.mmap_token_bytes,
116            "namespace"     : True, #this client understands "mmap.ATTRIBUTE" format
117            }
118
119    def init_mmap(self, mmap_filename, mmap_group, socket_filename):
120        log("init_mmap(%s, %s, %s)", mmap_filename, mmap_group, socket_filename)
121        from xpra.net.mmap_pipe import (  #pylint: disable=import-outside-toplevel
122            init_client_mmap, write_mmap_token,
123            DEFAULT_TOKEN_INDEX, DEFAULT_TOKEN_BYTES,
124            )
125        #calculate size:
126        root_w, root_h = self.get_root_size()
127        #at least 256MB, or 8 fullscreen RGBX frames:
128        mmap_size = max(512*1024*1024, root_w*root_h*4*8)
129        mmap_size = min(2048*1024*1024, mmap_size)
130        self.mmap_enabled, self.mmap_delete, self.mmap, self.mmap_size, self.mmap_tempfile, self.mmap_filename = \
131            init_client_mmap(mmap_group, socket_filename, mmap_size, self.mmap_filename)
132        if self.mmap_enabled:
133            self.mmap_token = get_int_uuid()
134            self.mmap_token_bytes = DEFAULT_TOKEN_BYTES
135            self.mmap_token_index = self.mmap_size - DEFAULT_TOKEN_BYTES
136            #self.mmap_token_index = DEFAULT_TOKEN_INDEX*2
137            #write the token twice:
138            # once at the old default offset for older servers,
139            # and at the offset we want to use with new servers
140            for index in (DEFAULT_TOKEN_INDEX, self.mmap_token_index):
141                write_mmap_token(self.mmap, self.mmap_token, index, self.mmap_token_bytes)
142
143    def clean_mmap(self):
144        log("XpraClient.clean_mmap() mmap_filename=%s", self.mmap_filename)
145        if self.mmap_tempfile:
146            try:
147                self.mmap_tempfile.close()
148            except Exception as e:
149                log("clean_mmap error closing file %s: %s", self.mmap_tempfile, e)
150            self.mmap_tempfile = None
151        if self.mmap_delete:
152            #this should be redundant: closing the tempfile should get it deleted
153            if self.mmap_filename and os.path.exists(self.mmap_filename):
154                from xpra.net.mmap_pipe import clean_mmap
155                clean_mmap(self.mmap_filename)
156                self.mmap_filename = None
157