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) 2017-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 sys 

7import os.path 

8 

9from xpra.util import envbool 

10from xpra.os_util import OSX, POSIX, shellsub, getuid, get_util_logger, osexpand, umask_context 

11from xpra.platform.dotxpra import norm_makepath 

12from xpra.scripts.config import InitException 

13 

14 

15def source_env(source=()) -> dict: 

16 log = get_util_logger() 

17 env = {} 

18 for f in source: 

19 e = env_from_sourcing(f) 

20 log("source_env %s=%s", f, e) 

21 env.update(e) 

22 return env 

23 

24 

25# credit: https://stackoverflow.com/a/47080959/428751 

26# returns a dictionary of the environment variables resulting from sourcing a file 

27def env_from_sourcing(file_to_source_path, include_unexported_variables=False): 

28 import json 

29 import subprocess 

30 source = '%ssource %s' % ("set -a && " if include_unexported_variables else "", file_to_source_path) 

31 dump = '/usr/bin/python -c "import os, json;print(json.dumps(dict(os.environ)))"' 

32 pipe = subprocess.Popen(['/bin/bash', '-c', '%s && %s' % (source, dump)], stdout=subprocess.PIPE) 

33 return json.loads(pipe.stdout.read()) 

34 

35 

36def sh_quotemeta(s): 

37 return b"'" + s.replace(b"'", b"'\\''") + b"'" 

38 

39def xpra_runner_shell_script(xpra_file, starting_dir, socket_dir): 

40 script = [] 

41 script.append(b"#!/bin/sh\n") 

42 for var, value in os.environb.items(): 

43 # these aren't used by xpra, and some should not be exposed 

44 # as they are either irrelevant or simply do not match 

45 # the new environment used by xpra 

46 # TODO: use a whitelist 

47 if var in (b"XDG_SESSION_COOKIE", b"LS_COLORS", b"DISPLAY"): 

48 continue 

49 #XPRA_SOCKET_DIR is a special case, it is handled below 

50 if var==b"XPRA_SOCKET_DIR": 

51 continue 

52 if var.startswith(b"BASH_FUNC"): 

53 #some versions of bash will apparently generate functions 

54 #that cannot be reloaded using this script 

55 continue 

56 # :-separated envvars that people might change while their server is 

57 # going: 

58 if var in (b"PATH", b"LD_LIBRARY_PATH", b"PYTHONPATH"): 

59 #prevent those paths from accumulating the same values multiple times, 

60 #only keep the first one: 

61 pathsep = os.pathsep.encode() 

62 pval = value.split(pathsep) #ie: ["/usr/bin", "/usr/local/bin", "/usr/bin"] 

63 seen = set() 

64 value = pathsep.join(x for x in pval if not (x in seen or seen.add(x))) 

65 script.append(b"%s=%s:\"$%s\"; export %s\n" 

66 % (var, sh_quotemeta(value), var, var)) 

67 else: 

68 script.append(b"%s=%s; export %s\n" 

69 % (var, sh_quotemeta(value), var)) 

70 #XPRA_SOCKET_DIR is a special case, we want to honour it 

71 #when it is specified, but the client may override it: 

72 if socket_dir: 

73 script.append(b'if [ -z "${XPRA_SOCKET_DIR}" ]; then\n') 

74 script.append(b' XPRA_SOCKET_DIR="%s"; export XPRA_SOCKET_DIR\n' % sh_quotemeta(os.path.expanduser(socket_dir).encode())) 

75 script.append(b'fi\n') 

76 # We ignore failures in cd'ing, b/c it's entirely possible that we were 

77 # started from some temporary directory and all paths are absolute. 

78 script.append(b"cd %s\n" % sh_quotemeta(starting_dir.encode())) 

79 if OSX: 

80 #OSX contortions: 

81 #The executable is the python interpreter, 

82 #which is execed by a shell script, which we have to find.. 

83 sexec = sys.executable 

84 bini = sexec.rfind("Resources/bin/") 

85 if bini>0: 

86 sexec = os.path.join(sexec[:bini], "Resources", "MacOS", "Xpra") 

87 script.append(b"_XPRA_SCRIPT=%s\n" % (sh_quotemeta(sexec.encode()),)) 

88 script.append(b""" 

89if which "$_XPRA_SCRIPT" > /dev/null; then 

90 # Happypath: 

91 exec "$_XPRA_SCRIPT" "$@" 

92else 

93 # Hope for the best: 

94 exec Xpra "$@" 

95fi 

96""") 

97 else: 

98 script.append(b"_XPRA_PYTHON=%s\n" % (sh_quotemeta(sys.executable.encode()),)) 

99 script.append(b"_XPRA_SCRIPT=%s\n" % (sh_quotemeta(xpra_file.encode()),)) 

100 script.append(b""" 

101if which "$_XPRA_PYTHON" > /dev/null && [ -e "$_XPRA_SCRIPT" ]; then 

102 # Happypath: 

103 exec "$_XPRA_PYTHON" "$_XPRA_SCRIPT" "$@" 

104else 

105 cat >&2 <<END 

106 Could not find one or both of '$_XPRA_PYTHON' and '$_XPRA_SCRIPT' 

107 Perhaps your environment has changed since the xpra server was started? 

108 I'll just try executing 'xpra' with current PATH, and hope... 

109END 

110 exec xpra "$@" 

111fi 

112""") 

113 return b"".join(script) 

114 

115def write_runner_shell_scripts(contents, overwrite=True): 

116 assert POSIX 

117 # This used to be given a display-specific name, but now we give it a 

118 # single fixed name and if multiple servers are started then the last one 

119 # will clobber the rest. This isn't great, but the tradeoff is that it 

120 # makes it possible to use bare 'ssh:hostname' display names and 

121 # autodiscover the proper numeric display name when only one xpra server 

122 # is running on the remote host. Might need to revisit this later if 

123 # people run into problems or autodiscovery turns out to be less useful 

124 # than expected. 

125 log = get_util_logger() 

126 MODE = 0o700 

127 from xpra.platform.paths import get_script_bin_dirs 

128 for d in get_script_bin_dirs(): 

129 scriptdir = osexpand(d) 

130 if not os.path.exists(scriptdir): 

131 try: 

132 os.mkdir(scriptdir, MODE) 

133 except Exception as e: 

134 log("os.mkdir(%s, %s)", scriptdir, oct(MODE), exc_info=True) 

135 log.warn("Warning: failed to create script directory '%s':", scriptdir) 

136 log.warn(" %s", e) 

137 if scriptdir.startswith("/var/run/user") or scriptdir.startswith("/run/user"): 

138 log.warn(" ($XDG_RUNTIME_DIR has not been created?)") 

139 continue 

140 scriptpath = os.path.join(scriptdir, "run-xpra") 

141 if os.path.exists(scriptpath) and not overwrite: 

142 continue 

143 # Write out a shell-script so that we can start our proxy in a clean 

144 # environment: 

145 try: 

146 with umask_context(0o022): 

147 h = os.open(scriptpath, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, MODE) 

148 try: 

149 os.write(h, contents) 

150 finally: 

151 os.close(h) 

152 except Exception as e: 

153 log("writing to %s", scriptpath, exc_info=True) 

154 log.error("Error: failed to write script file '%s':", scriptpath) 

155 log.error(" %s\n", e) 

156 

157 

158def find_log_dir(username="", uid=0, gid=0): 

159 from xpra.platform.paths import get_default_log_dirs 

160 errs = [] 

161 for x in get_default_log_dirs(): 

162 v = osexpand(x, username, uid, gid) 

163 if not os.path.exists(v): 

164 if getuid()==0 and uid!=0: 

165 continue 

166 try: 

167 os.mkdir(v, 0o700) 

168 except Exception as e: 

169 errs.append((v, e)) 

170 continue 

171 return v 

172 for d, e in errs: 

173 sys.stderr.write("Error: cannot create log directory '%s':" % d) 

174 sys.stderr.write(" %s\n" % e) 

175 return None 

176 

177 

178def open_log_file(logpath): 

179 """ renames the existing log file if it exists, 

180 then opens it for writing. 

181 """ 

182 if os.path.exists(logpath): 

183 try: 

184 os.rename(logpath, logpath + ".old") 

185 except OSError: 

186 pass 

187 try: 

188 return os.open(logpath, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644) 

189 except OSError as e: 

190 raise InitException("cannot open log file '%s': %s" % (logpath, e)) from None 

191 

192def select_log_file(log_dir, log_file, display_name): 

193 """ returns the log file path we should be using given the parameters, 

194 this may return a temporary logpath if display_name is not available. 

195 """ 

196 if log_file: 

197 if os.path.isabs(log_file): 

198 logpath = log_file 

199 else: 

200 logpath = os.path.join(log_dir, log_file) 

201 v = shellsub(logpath, {"DISPLAY" : display_name}) 

202 if display_name or v==logpath: 

203 #we have 'display_name', or we just don't need it: 

204 return v 

205 if display_name: 

206 logpath = norm_makepath(log_dir, display_name) + ".log" 

207 else: 

208 logpath = os.path.join(log_dir, "tmp_%d.log" % os.getpid()) 

209 return logpath 

210 

211def redirect_std_to_log(logfd, *noclose_fds): 

212 from xpra.os_util import close_all_fds 

213 # save current stdout/stderr to be able to print info 

214 # before exiting the non-deamon process 

215 # and closing those file descriptors definitively 

216 old_fd_stdout = os.dup(1) 

217 old_fd_stderr = os.dup(2) 

218 close_all_fds(exceptions=[logfd, old_fd_stdout, old_fd_stderr]+list(noclose_fds)) 

219 fd0 = os.open("/dev/null", os.O_RDONLY) 

220 if fd0 != 0: 

221 os.dup2(fd0, 0) 

222 os.close(fd0) 

223 # reopen STDIO files 

224 stdout = os.fdopen(old_fd_stdout, "w", 1) 

225 stderr = os.fdopen(old_fd_stderr, "w", 1) 

226 # replace standard stdout/stderr by the log file 

227 os.dup2(logfd, 1) 

228 os.dup2(logfd, 2) 

229 os.close(logfd) 

230 # Make these line-buffered: 

231 sys.stdout = os.fdopen(1, "w", 1) 

232 sys.stderr = os.fdopen(2, "w", 1) 

233 return stdout, stderr 

234 

235 

236def daemonize(): 

237 os.chdir("/") 

238 if os.fork(): 

239 os._exit(0) #pylint: disable=protected-access 

240 os.setsid() 

241 if os.fork(): 

242 os._exit(0) #pylint: disable=protected-access 

243 

244 

245def write_pidfile(pidfile): 

246 log = get_util_logger() 

247 pidstr = str(os.getpid()) 

248 inode = 0 

249 try: 

250 with open(pidfile, "w") as f: 

251 os.fchmod(f.fileno(), 0o600) 

252 f.write("%s\n" % pidstr) 

253 try: 

254 inode = os.fstat(f.fileno()).st_ino 

255 except OSError: 

256 inode = 0 

257 log.info("wrote pid %s to '%s'", pidstr, pidfile) 

258 except Exception as e: 

259 log.error("Error: failed to write pid %i to pidfile '%s':", os.getpid(), pidfile) 

260 log.error(" %s", e) 

261 return inode 

262 

263def rm_pidfile(pidfile, inode): 

264 #verify this is the right file! 

265 log = get_util_logger() 

266 log("cleanuppidfile(%s, %s)", pidfile, inode) 

267 if inode>0: 

268 try: 

269 i = os.stat(pidfile).st_ino 

270 log("cleanuppidfile: current inode=%i", i) 

271 if i!=inode: 

272 return 0 

273 except OSError: 

274 pass 

275 try: 

276 os.unlink(pidfile) 

277 except OSError: 

278 log("rm_pidfile(%s, %s)", pidfile, inode, exc_info=True) 

279 return 0 

280 

281 

282def get_uinput_device_path(device): 

283 log = get_util_logger() 

284 try: 

285 log("get_uinput_device_path(%s)", device) 

286 fd = device._Device__uinput_fd 

287 log("fd(%s)=%s", device, fd) 

288 import fcntl #@UnresolvedImport 

289 import ctypes 

290 l = 16 

291 buf = ctypes.create_string_buffer(l) 

292 #this magic value was calculated using the C macros: 

293 l = fcntl.ioctl(fd, 2148554028, buf) 

294 if 0<l<16: 

295 virt_dev_path = buf.raw[:l].rstrip(b"\0") 

296 log("UI_GET_SYSNAME(%s)=%s", fd, virt_dev_path) 

297 uevent_path = b"/sys/devices/virtual/input/%s" % virt_dev_path 

298 event_dirs = [x for x in os.listdir(uevent_path) if x.startswith(b"event")] 

299 log("event dirs(%s)=%s", uevent_path, event_dirs) 

300 for d in event_dirs: 

301 uevent_filename = os.path.join(uevent_path, d, b"uevent") 

302 uevent_conf = open(uevent_filename, "rb").read() 

303 for line in uevent_conf.splitlines(): 

304 if line.find(b"=")>0: 

305 k,v = line.split(b"=", 1) 

306 log("%s=%s", k, v) 

307 if k==b"DEVNAME": 

308 dev_path = b"/dev/%s" % v 

309 log("found device path: %s" % dev_path) 

310 return dev_path 

311 except Exception as e: 

312 log("get_uinput_device_path(%s)", device, exc_info=True) 

313 log.error("Error: cannot query uinput device path:") 

314 log.error(" %s", e) 

315 return None 

316 

317def has_uinput(): 

318 try: 

319 import uinput 

320 assert uinput 

321 except NameError as e: 

322 log = get_util_logger() 

323 log("has_uinput()", exc_info=True) 

324 log.warn("Warning: the system python uinput module looks broken:") 

325 log.warn(" %s", e) 

326 return False 

327 except ImportError as e: 

328 log = get_util_logger() 

329 log("has_uinput()", exc_info=True) 

330 log.info("cannot access python uinput module:") 

331 log.info(" %s", e) 

332 return False 

333 try: 

334 uinput.fdopen() #@UndefinedVariable 

335 except Exception as e: 

336 log = get_util_logger() 

337 log("has_uinput()", exc_info=True) 

338 log.info("cannot use uinput for virtual devices:") 

339 log.info(" %s", e) 

340 return False 

341 return True 

342 

343def create_uinput_device(uuid, uid, events, name): 

344 log = get_util_logger() 

345 import uinput 

346 BUS_USB = 0x03 

347 #BUS_VIRTUAL = 0x06 

348 VENDOR = 0xffff 

349 PRODUCT = 0x1000 

350 #our xpra_udev_product_version script will use the version attribute to set 

351 #the udev OWNER value 

352 VERSION = uid 

353 try: 

354 device = uinput.Device(events, name=name, bustype=BUS_USB, vendor=VENDOR, product=PRODUCT, version=VERSION) 

355 except OSError as e: 

356 log("uinput.Device creation failed", exc_info=True) 

357 if os.getuid()==0: 

358 #running as root, this should work! 

359 log.error("Error: cannot open uinput,") 

360 log.error(" make sure that the kernel module is loaded") 

361 log.error(" and that the /dev/uinput device exists:") 

362 log.error(" %s", e) 

363 else: 

364 log.info("cannot access uinput: %s", e) 

365 return None 

366 dev_path = get_uinput_device_path(device) 

367 if not dev_path: 

368 device.destroy() 

369 return None 

370 return name, device, dev_path 

371 

372def create_uinput_pointer_device(uuid, uid): 

373 if not envbool("XPRA_UINPUT_POINTER", True): 

374 return None 

375 from uinput import ( 

376 REL_X, REL_Y, REL_WHEEL, #@UnresolvedImport 

377 BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, #@UnresolvedImport 

378 BTN_EXTRA, BTN_FORWARD, BTN_BACK, #@UnresolvedImport 

379 ) 

380 events = ( 

381 REL_X, REL_Y, REL_WHEEL, 

382 BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_SIDE, 

383 BTN_EXTRA, BTN_FORWARD, BTN_BACK, 

384 ) 

385 #REL_HIRES_WHEEL = 0x10 

386 #uinput.REL_HWHEEL, 

387 name = "Xpra Virtual Pointer %s" % uuid 

388 return create_uinput_device(uuid, uid, events, name) 

389 

390def create_uinput_touchpad_device(uuid, uid): 

391 if not envbool("XPRA_UINPUT_TOUCHPAD", False): 

392 return None 

393 from uinput import ( 

394 BTN_TOUCH, ABS_X, ABS_Y, ABS_PRESSURE, #@UnresolvedImport 

395 ) 

396 events = ( 

397 BTN_TOUCH, 

398 ABS_X + (0, 2**24-1, 0, 0), 

399 ABS_Y + (0, 2**24-1, 0, 0), 

400 ABS_PRESSURE + (0, 255, 0, 0), 

401 #BTN_TOOL_PEN, 

402 ) 

403 name = "Xpra Virtual Touchpad %s" % uuid 

404 return create_uinput_device(uuid, uid, events, name) 

405 

406 

407def create_uinput_devices(uinput_uuid, uid): 

408 log = get_util_logger() 

409 try: 

410 import uinput 

411 assert uinput 

412 except (ImportError, NameError) as e: 

413 log.error("Error: cannot access python uinput module:") 

414 log.error(" %s", e) 

415 return {} 

416 pointer = create_uinput_pointer_device(uinput_uuid, uid) 

417 touchpad = create_uinput_touchpad_device(uinput_uuid, uid) 

418 if not pointer and not touchpad: 

419 return {} 

420 def i(device): 

421 if not device: 

422 return {} 

423 name, uinput_pointer, dev_path = device 

424 return { 

425 "name" : name, 

426 "uinput" : uinput_pointer, 

427 "device" : dev_path, 

428 } 

429 return { 

430 "pointer" : i(pointer), 

431 "touchpad" : i(touchpad), 

432 } 

433 

434def create_input_devices(uinput_uuid, uid): 

435 return create_uinput_devices(uinput_uuid, uid)