Hide keyboard shortcuts

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. 

5 

6import os 

7from ctypes import c_ubyte, c_char, c_uint32 

8 

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 

14 

15log = Logger("mmap") 

16 

17MMAP_GROUP = os.environ.get("XPRA_MMAP_GROUP", "xpra") 

18 

19 

20""" 

21Utility functions for communicating via mmap 

22""" 

23 

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 

30 

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 

43 

44 

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() 

162 

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) 

171 

172DEFAULT_TOKEN_INDEX = 512 

173DEFAULT_TOKEN_BYTES = 128 

174 

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" 

185 

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 

195 

196 

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 

211 

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 

234 

235def int_from_buffer(mmap_area, pos): 

236 return c_uint32.from_buffer(mmap_area, pos) #@UndefinedVariable 

237 

238 

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) 

260 

261 

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