Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/mmap_pipe.py : 57%
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# This file is part of Xpra.
2# Copyright (C) 2011-2018 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.
6import os
7from ctypes import c_ubyte, c_char, c_uint32
9from xpra.util import roundup
10from xpra.os_util import memoryview_to_bytes, shellsub, get_group_id, get_groups, WIN32, POSIX
11from xpra.scripts.config import FALSE_OPTIONS, TRUE_OPTIONS
12from xpra.simple_stats import std_unit
13from xpra.log import Logger
15log = Logger("mmap")
17MMAP_GROUP = os.environ.get("XPRA_MMAP_GROUP", "xpra")
20"""
21Utility functions for communicating via mmap
22"""
24def get_socket_group(socket_filename) -> int:
25 if isinstance(socket_filename, str) and os.path.exists(socket_filename):
26 s = os.stat(socket_filename)
27 return s.st_gid
28 log.warn("Warning: missing valid socket filename to set mmap group")
29 return -1
31def xpra_group() -> int:
32 if POSIX:
33 try:
34 username = os.getgroups()
35 groups = get_groups(username)
36 if MMAP_GROUP in groups:
37 group_id = get_group_id(MMAP_GROUP)
38 if group_id>=0:
39 return group_id
40 except Exception:
41 log("xpra_group()", exc_info=True)
42 return 0
45def init_client_mmap(mmap_group=None, socket_filename=None, size=128*1024*1024, filename=None):
46 """
47 Initializes an mmap area, writes the token in it and returns:
48 (success flag, mmap_area, mmap_size, temp_file, mmap_filename)
49 The caller must keep hold of temp_file to ensure it does not get deleted!
50 This is used by the client.
51 """
52 def rerr():
53 return False, False, None, 0, None, None
54 log("init_mmap%s", (mmap_group, socket_filename, size, filename))
55 mmap_filename = filename
56 mmap_temp_file = None
57 delete = True
58 def validate_size(size : int):
59 assert size>=64*1024*1024, "mmap size is too small: %sB (minimum is 64MB)" % std_unit(size)
60 assert size<=4*1024*1024*1024, "mmap is too big: %sB (maximum is 4GB)" % std_unit(size)
61 try:
62 import mmap
63 unit = max(4096, mmap.PAGESIZE)
64 #add 8 bytes for the mmap area control header zone:
65 mmap_size = roundup(size + 8, unit)
66 if WIN32:
67 validate_size(mmap_size)
68 if not filename:
69 from xpra.os_util import get_hex_uuid
70 filename = "xpra-%s" % get_hex_uuid()
71 mmap_filename = filename
72 mmap_area = mmap.mmap(0, mmap_size, filename)
73 #not a real file:
74 delete = False
75 mmap_temp_file = None
76 else:
77 assert POSIX
78 if filename:
79 if os.path.exists(filename):
80 fd = os.open(filename, os.O_EXCL | os.O_RDWR)
81 mmap_size = os.path.getsize(mmap_filename)
82 validate_size(mmap_size)
83 #mmap_size = 4*1024*1024 #size restriction needed with ivshmem
84 delete = False
85 log.info("Using existing mmap file '%s': %sMB", mmap_filename, mmap_size//1024//1024)
86 else:
87 validate_size(mmap_size)
88 flags = os.O_CREAT | os.O_EXCL | os.O_RDWR
89 try:
90 fd = os.open(filename, flags)
91 mmap_temp_file = None #os.fdopen(fd, 'w')
92 mmap_filename = filename
93 except FileExistsError:
94 log.error("Error: the mmap file '%s' already exists", filename)
95 return rerr()
96 else:
97 validate_size(mmap_size)
98 import tempfile
99 from xpra.platform.paths import get_mmap_dir
100 mmap_dir = get_mmap_dir()
101 subs = os.environ.copy()
102 subs.update({
103 "UID" : os.getuid(),
104 "GID" : os.getgid(),
105 "PID" : os.getpid(),
106 })
107 mmap_dir = shellsub(mmap_dir, subs)
108 if mmap_dir and not os.path.exists(mmap_dir):
109 os.mkdir(mmap_dir, 0o700)
110 if not mmap_dir or not os.path.exists(mmap_dir):
111 raise Exception("mmap directory %s does not exist!" % mmap_dir)
112 #create the mmap file, the mkstemp that is called via NamedTemporaryFile ensures
113 #that the file is readable and writable only by the creating user ID
114 try:
115 temp = tempfile.NamedTemporaryFile(prefix="xpra.", suffix=".mmap", dir=mmap_dir)
116 except OSError as e:
117 log.error("Error: cannot create mmap file:")
118 log.error(" %s", e)
119 return rerr()
120 #keep a reference to it so it does not disappear!
121 mmap_temp_file = temp
122 mmap_filename = temp.name
123 fd = temp.file.fileno()
124 #set the group permissions and gid if the mmap-group option is specified
125 mmap_group = (mmap_group or "")
126 if POSIX and mmap_group and mmap_group not in FALSE_OPTIONS:
127 group_id = None
128 if mmap_group=="SOCKET":
129 group_id = get_socket_group(socket_filename)
130 elif mmap_group.lower()=="auto":
131 group_id = xpra_group()
132 if not group_id and socket_filename:
133 group_id = get_socket_group(socket_filename)
134 elif mmap_group.lower() in TRUE_OPTIONS:
135 log.info("parsing legacy mmap-group value '%s' as 'auto'", mmap_group)
136 log.info(" please update your configuration")
137 group_id = xpra_group() or get_socket_group(socket_filename)
138 else:
139 group_id = get_group_id(mmap_group)
140 if group_id>0:
141 log("setting mmap file %s to group id=%i", mmap_filename, group_id)
142 try:
143 os.fchown(fd, -1, group_id)
144 except OSError as e:
145 log("fchown(%i, %i, %i) on %s", fd, -1, group_id, mmap_filename, exc_info=True)
146 log.error("Error: failed to change group ownership of mmap file to '%s':", mmap_group)
147 log.error(" %s", e)
148 from stat import S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP
149 os.fchmod(fd, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP)
150 log("using mmap file %s, fd=%s, size=%s", mmap_filename, fd, mmap_size)
151 os.lseek(fd, mmap_size-1, os.SEEK_SET)
152 assert os.write(fd, b'\x00')
153 os.lseek(fd, 0, os.SEEK_SET)
154 mmap_area = mmap.mmap(fd, length=mmap_size)
155 return True, delete, mmap_area, mmap_size, mmap_temp_file, mmap_filename
156 except Exception as e:
157 log("failed to setup mmap: %s", e, exc_info=True)
158 log.error("Error: mmap setup failed:")
159 log.error(" %s", e)
160 clean_mmap(mmap_filename)
161 return rerr()
163def clean_mmap(mmap_filename):
164 log("clean_mmap(%s)", mmap_filename)
165 if mmap_filename and os.path.exists(mmap_filename):
166 try:
167 os.unlink(mmap_filename)
168 except OSError as e:
169 log.error("Error: failed to remove the mmap file '%s':", mmap_filename)
170 log.error(" %s", e)
172DEFAULT_TOKEN_INDEX = 512
173DEFAULT_TOKEN_BYTES = 128
175def write_mmap_token(mmap_area, token, index=DEFAULT_TOKEN_INDEX, count=DEFAULT_TOKEN_BYTES):
176 assert count>0
177 #write the token one byte at a time - no endianness
178 log("write_mmap_token(%s, %#x, %#x, %#x)", mmap_area, token, index, count)
179 v = token
180 for i in range(0, count):
181 poke = c_ubyte.from_buffer(mmap_area, index+i)
182 poke.value = v % 256
183 v = v>>8
184 assert v==0, "token value is too big"
186def read_mmap_token(mmap_area, index=DEFAULT_TOKEN_INDEX, count=DEFAULT_TOKEN_BYTES):
187 assert count>0
188 v = 0
189 for i in range(0, count):
190 v = v<<8
191 peek = c_ubyte.from_buffer(mmap_area, index+count-1-i)
192 v += peek.value
193 log("read_mmap_token(%s, %#x, %#x)=%#x", mmap_area, index, count, v)
194 return v
197def init_server_mmap(mmap_filename, mmap_size=0):
198 """
199 Reads the mmap file provided by the client
200 and verifies the token if supplied.
201 Returns the mmap object and its size: (mmap, size)
202 """
203 if not WIN32:
204 try:
205 f = open(mmap_filename, "r+b")
206 except Exception as e:
207 log.error("Error: cannot access mmap file '%s':", mmap_filename)
208 log.error(" %s", e)
209 log.error(" see mmap-group option?")
210 return None, 0
212 mmap_area = None
213 try:
214 import mmap
215 if not WIN32:
216 actual_mmap_size = os.path.getsize(mmap_filename)
217 if mmap_size and actual_mmap_size!=mmap_size:
218 log.warn("Warning: expected mmap file '%s' of size %i but got %i",
219 mmap_filename, mmap_size, actual_mmap_size)
220 mmap_area = mmap.mmap(f.fileno(), mmap_size)
221 else:
222 if mmap_size==0:
223 log.error("Error: client did not supply the mmap area size")
224 log.error(" try updating your client version?")
225 mmap_area = mmap.mmap(0, mmap_size, mmap_filename)
226 actual_mmap_size = mmap_size
227 f.close()
228 return mmap_area, actual_mmap_size
229 except Exception as e:
230 log.error("cannot use mmap file '%s': %s", mmap_filename, e, exc_info=True)
231 if mmap_area:
232 mmap_area.close()
233 return None, 0
235def int_from_buffer(mmap_area, pos):
236 return c_uint32.from_buffer(mmap_area, pos) #@UndefinedVariable
239#descr_data is a list of (offset, length)
240#areas from the mmap region
241def mmap_read(mmap_area, *descr_data):
242 """
243 Reads data from the mmap_area as written by 'mmap_write'.
244 The descr_data is the list of mmap chunks used.
245 """
246 data_start = int_from_buffer(mmap_area, 0)
247 if len(descr_data)==1:
248 #construct an array directly from the mmap zone:
249 offset, length = descr_data[0]
250 arraytype = c_char * length
251 data_start.value = offset+length
252 return arraytype.from_buffer(mmap_area, offset)
253 #re-construct the buffer from discontiguous chunks:
254 data = []
255 for offset, length in descr_data:
256 mmap_area.seek(offset)
257 data.append(mmap_area.read(length))
258 data_start.value = offset+length
259 return b"".join(data)
262def mmap_write(mmap_area, mmap_size, data):
263 """
264 Sends 'data' to the client via the mmap shared memory region,
265 returns the chunks of the mmap area used (or None if it failed)
266 and the mmap area's free memory.
267 """
268 #This is best explained using diagrams:
269 #mmap_area=[&S&E-------------data-------------]
270 #The first pair of 4 bytes are occupied by:
271 #S=data_start index is only updated by the client and tells us where it has read up to
272 #E=data_end index is only updated here and marks where we have written up to (matches current seek)
273 # '-' denotes unused/available space
274 # '+' is for data we have written
275 # '*' is for data we have just written in this call
276 # E and S show the location pointed to by data_start/data_end
277 mmap_data_start = int_from_buffer(mmap_area, 0)
278 mmap_data_end = int_from_buffer(mmap_area, 4)
279 start = max(8, mmap_data_start.value)
280 end = max(8, mmap_data_end.value)
281 l = len(data)
282 log("mmap: start=%i, end=%i, size of data to write=%i", start, end, l)
283 if end<start:
284 #we have wrapped around but the client hasn't yet:
285 #[++++++++E--------------------S+++++]
286 #so there is one chunk available (from E to S) which we will use:
287 #[++++++++************E--------S+++++]
288 available = start-end
289 chunk = available
290 else:
291 #we have not wrapped around yet, or the client has wrapped around too:
292 #[------------S++++++++++++E---------]
293 #so there are two chunks available (from E to the end, from the start to S):
294 #[****--------S++++++++++++E*********]
295 chunk = mmap_size-end
296 available = chunk+(start-8)
297 #update global mmap stats:
298 mmap_free_size = available-l
299 if l>(mmap_size-8):
300 log.warn("Warning: mmap area is too small!")
301 log.warn(" we need to store %s bytes but the mmap area is limited to %i", l, (mmap_size-8))
302 return None, mmap_free_size
303 if mmap_free_size<=0:
304 log.warn("Warning: mmap area is full!")
305 log.warn(" we need to store %s bytes but only have %s free space left", l, available)
306 return None, mmap_free_size
307 if l<chunk:
308 # data fits in the first chunk:
309 #ie: initially:
310 #[----------------------------------]
311 #[*********E------------------------]
312 #or if data already existed:
313 #[+++++++++E------------------------]
314 #[+++++++++**********E--------------]
315 mmap_area.seek(end)
316 mmap_area.write(memoryview_to_bytes(data))
317 chunks = [(end, l)]
318 mmap_data_end.value = end+l
319 else:
320 # data does not fit in first chunk alone:
321 if available>=(mmap_size/2) and available>=(l*3) and l<(start-8):
322 # still plenty of free space, don't wrap around: just start again:
323 #[------------------S+++++++++E------]
324 #[*******E----------S+++++++++-------]
325 mmap_area.seek(8)
326 mmap_area.write(memoryview_to_bytes(data))
327 chunks = [(8, l)]
328 mmap_data_end.value = 8+l
329 else:
330 # split in 2 chunks: wrap around the end of the mmap buffer:
331 #[------------------S+++++++++E------]
332 #[******E-----------S+++++++++*******]
333 mmap_area.seek(end)
334 mmap_area.write(memoryview_to_bytes(data[:chunk]))
335 mmap_area.seek(8)
336 mmap_area.write(memoryview_to_bytes(data[chunk:]))
337 l2 = l-chunk
338 chunks = [(end, chunk), (8, l2)]
339 mmap_data_end.value = 8+l2
340 log("sending damage with mmap: %s", data)
341 return chunks, mmap_free_size