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

7import sys 

8import subprocess 

9from queue import Queue 

10 

11from xpra.gtk_common.gobject_compat import register_os_signals 

12from xpra.util import repr_ellipsized, envint, envbool 

13from xpra.net.bytestreams import TwoFileConnection 

14from xpra.net.common import ConnectionClosedException, PACKET_TYPES 

15from xpra.net.protocol import Protocol 

16from xpra.os_util import setbinarymode, SIGNAMES, bytestostr, hexstr, WIN32 

17from xpra.child_reaper import getChildReaper 

18from xpra.log import Logger 

19 

20log = Logger("util") 

21 

22 

23#this wrapper allows us to interact with a subprocess as if it was 

24#a normal class with gobject signals 

25#so that we can interact with it using a standard xpra protocol layer 

26#there is a wrapper for the caller 

27#and one for the class 

28#they talk to each other through stdin / stdout, 

29#using the protocol for encoding the data 

30 

31 

32DEBUG_WRAPPER = envbool("XPRA_WRAPPER_DEBUG", False) 

33#to make it possible to inspect files (more human readable): 

34HEXLIFY_PACKETS = envbool("XPRA_HEXLIFY_PACKETS", False) 

35#avoids showing a new console window on win32: 

36WIN32_SHOWWINDOW = envbool("XPRA_WIN32_SHOWWINDOW", False) 

37#assume that the subprocess is running the same version of xpra, 

38#so we can set the packet aliases without exchanging and parsing caps: 

39#(this can break, ie: if both python2 and python3 builds are installed 

40# and the python2 builds are from an older version) 

41LOCAL_ALIASES = envbool("XPRA_LOCAL_ALIASES", False) 

42 

43LOCAL_SEND_ALIASES = dict((v, i) for i,v in enumerate(PACKET_TYPES)) 

44LOCAL_RECEIVE_ALIASES = dict(enumerate(PACKET_TYPES)) 

45 

46FLUSH = envbool("XPRA_SUBPROCESS_FLUSH", False) 

47 

48 

49FAULT_RATE = envint("XPRA_WRAPPER_FAULT_INJECTION_RATE") 

50def noop(_p): 

51 pass 

52INJECT_FAULT = noop 

53if FAULT_RATE>0: 

54 _counter = 0 

55 def DO_INJECT_FAULT(p): 

56 global _counter 

57 _counter += 1 

58 if (_counter % FAULT_RATE)==0: 

59 log.warn("injecting fault in %s", p) 

60 p.raw_write("junk", "Wrapper JUNK! added by fault injection code") 

61 INJECT_FAULT = DO_INJECT_FAULT 

62 

63 

64def setup_fastencoder_nocompression(protocol): 

65 from xpra.net.packet_encoding import get_enabled_encoders, PERFORMANCE_ORDER 

66 encoders = get_enabled_encoders(PERFORMANCE_ORDER) 

67 assert len(encoders)>0, "no packet encoders available!?" 

68 for encoder in encoders: 

69 try: 

70 protocol.enable_encoder(encoder) 

71 log("protocol using %s", encoder) 

72 break 

73 except Exception as e: 

74 log("failed to enable %s: %s", encoder, e) 

75 #we assume this is local, so no compression: 

76 protocol.enable_compressor("none") 

77 

78 

79class subprocess_callee: 

80 """ 

81 This is the callee side, wrapping the gobject we want to interact with. 

82 All the input received will be converted to method calls on the wrapped object. 

83 Subclasses should register the signal handlers they want to see exported back to the caller. 

84 The convenience connect_export(signal-name, *args) can be used to forward signals unmodified. 

85 You can also call send() to pass packets back to the caller. 

86 (there is no validation of which signals are valid or not) 

87 """ 

88 def __init__(self, input_filename="-", output_filename="-", wrapped_object=None, method_whitelist=None): 

89 self.name = "" 

90 self._input = None 

91 self._output = None 

92 self.input_filename = input_filename 

93 self.output_filename = output_filename 

94 self.method_whitelist = method_whitelist 

95 self.large_packets = [] 

96 #the gobject instance which is wrapped: 

97 self.wrapped_object = wrapped_object 

98 self.send_queue = Queue() 

99 self.protocol = None 

100 register_os_signals(self.handle_signal, self.name) 

101 self.setup_mainloop() 

102 

103 def setup_mainloop(self): 

104 from gi.repository import GLib 

105 self.mainloop = GLib.MainLoop() 

106 self.idle_add = GLib.idle_add 

107 self.timeout_add = GLib.timeout_add 

108 self.source_remove = GLib.source_remove 

109 

110 

111 def connect_export(self, signal_name, *user_data): 

112 """ gobject style signal registration for the wrapped object, 

113 the signals will automatically be forwarded to the wrapper process 

114 using send(signal_name, *signal_args, *user_data) 

115 """ 

116 log("connect_export%s", [signal_name] + list(user_data)) 

117 args = list(user_data) + [signal_name] 

118 self.wrapped_object.connect(signal_name, self.export, *args) 

119 

120 def export(self, *args): 

121 signal_name = args[-1] 

122 log("export(%s, ...)", signal_name) 

123 data = args[1:-1] 

124 self.send(signal_name, *tuple(data)) 

125 

126 

127 def start(self): 

128 self.protocol = self.make_protocol() 

129 self.protocol.start() 

130 try: 

131 self.run() 

132 return 0 

133 except KeyboardInterrupt as e: 

134 log("start() KeyboardInterrupt %s", e) 

135 if str(e): 

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

137 return 0 

138 except Exception: 

139 log.error("error in main loop", exc_info=True) 

140 return 1 

141 finally: 

142 log("run() ended, calling cleanup and protocol close") 

143 self.cleanup() 

144 if self.protocol: 

145 self.protocol.close() 

146 self.protocol = None 

147 i = self._input 

148 if i: 

149 self._input = None 

150 try: 

151 i.close() 

152 except OSError: 

153 log("%s.close()", i, exc_info=True) 

154 o = self._output 

155 if o: 

156 self._output = None 

157 try: 

158 o.close() 

159 except OSError: 

160 log("%s.close()", o, exc_info=True) 

161 

162 def make_protocol(self): 

163 #figure out where we read from and write to: 

164 if self.input_filename=="-": 

165 #disable stdin buffering: 

166 self._input = os.fdopen(sys.stdin.fileno(), 'rb', 0) 

167 setbinarymode(self._input.fileno()) 

168 else: 

169 self._input = open(self.input_filename, 'rb') 

170 if self.output_filename=="-": 

171 #disable stdout buffering: 

172 self._output = os.fdopen(sys.stdout.fileno(), 'wb', 0) 

173 setbinarymode(self._output.fileno()) 

174 else: 

175 self._output = open(self.output_filename, 'wb') 

176 #stdin and stdout wrapper: 

177 conn = TwoFileConnection(self._output, self._input, 

178 abort_test=None, target=self.name, 

179 socktype=self.name, close_cb=self.net_stop) 

180 conn.timeout = 0 

181 protocol = Protocol(self, conn, self.process_packet, get_packet_cb=self.get_packet) 

182 if LOCAL_ALIASES: 

183 protocol.send_aliases = LOCAL_SEND_ALIASES 

184 protocol.receive_aliases = LOCAL_RECEIVE_ALIASES 

185 setup_fastencoder_nocompression(protocol) 

186 protocol.large_packets = self.large_packets 

187 return protocol 

188 

189 

190 def run(self): 

191 self.mainloop.run() 

192 

193 

194 def net_stop(self): 

195 #this is called from the network thread, 

196 #we use idle add to ensure we clean things up from the main thread 

197 log("net_stop() will call stop from main thread") 

198 self.idle_add(self.stop) 

199 

200 

201 def cleanup(self): 

202 pass 

203 

204 def stop(self): 

205 self.cleanup() 

206 p = self.protocol 

207 log("stop() protocol=%s", p) 

208 if p: 

209 self.protocol = None 

210 p.close() 

211 self.do_stop() 

212 

213 def do_stop(self): 

214 log("stop() stopping mainloop %s", self.mainloop) 

215 self.mainloop.quit() 

216 

217 def handle_signal(self, sig): 

218 """ This is for OS signals SIGINT and SIGTERM """ 

219 #next time, just stop: 

220 register_os_signals(self.signal_stop, self.name) 

221 signame = SIGNAMES.get(sig, sig) 

222 log("handle_signal(%s) calling stop from main thread", signame) 

223 self.send("signal", signame) 

224 self.timeout_add(0, self.cleanup) 

225 #give time for the network layer to send the signal message 

226 self.timeout_add(150, self.stop) 

227 

228 def signal_stop(self, sig): 

229 """ This time we really want to exit without waiting """ 

230 signame = SIGNAMES.get(sig, sig) 

231 log("signal_stop(%s) calling stop", signame) 

232 self.stop() 

233 

234 

235 def send(self, *args): 

236 if HEXLIFY_PACKETS: 

237 args = args[:1]+[hexstr(str(x)[:32]) for x in args[1:]] 

238 log("send: adding '%s' message (%s items already in queue)", args[0], self.send_queue.qsize()) 

239 self.send_queue.put(args) 

240 p = self.protocol 

241 if p: 

242 p.source_has_more() 

243 INJECT_FAULT(p) 

244 

245 def get_packet(self): 

246 try: 

247 item = self.send_queue.get(False) 

248 except Exception: 

249 item = None 

250 return (item, None, None, self.send_queue.qsize()>0) 

251 

252 def process_packet(self, proto, packet): 

253 command = bytestostr(packet[0]) 

254 if command==Protocol.CONNECTION_LOST: 

255 log("connection-lost: %s, calling stop", packet[1:]) 

256 self.net_stop() 

257 return 

258 if command==Protocol.GIBBERISH: 

259 log.warn("gibberish received:") 

260 log.warn(" %s", repr_ellipsized(packet[1], limit=80)) 

261 log.warn(" stopping") 

262 self.net_stop() 

263 return 

264 if command=="stop": 

265 log("received stop message") 

266 self.net_stop() 

267 return 

268 if command=="exit": 

269 log("received exit message") 

270 sys.exit(0) 

271 return 

272 #make it easier to hookup signals to methods: 

273 attr = command.replace("-", "_") 

274 if self.method_whitelist is not None and attr not in self.method_whitelist: 

275 log.warn("invalid command: %s (not in whitelist: %s)", attr, self.method_whitelist) 

276 return 

277 wo = self.wrapped_object 

278 if not wo: 

279 log("wrapped object is no more, ignoring method call '%s'", attr) 

280 return 

281 method = getattr(wo, attr, None) 

282 if not method: 

283 log.warn("unknown command: '%s'", attr) 

284 log.warn(" packet: '%s'", repr_ellipsized(str(packet))) 

285 return 

286 if DEBUG_WRAPPER: 

287 log("calling %s.%s%s", wo, attr, str(tuple(packet[1:]))[:128]) 

288 self.idle_add(method, *packet[1:]) 

289 INJECT_FAULT(proto) 

290 

291 

292def exec_kwargs() -> dict: 

293 kwargs = {} 

294 stderr = sys.stderr.fileno() 

295 if WIN32: 

296 from xpra.platform.win32 import REDIRECT_OUTPUT 

297 if REDIRECT_OUTPUT: 

298 #stderr is not valid and would give us this error: 

299 # WindowsError: [Errno 6] The handle is invalid 

300 stderr = open(os.devnull, 'w') 

301 if not WIN32_SHOWWINDOW: 

302 startupinfo = subprocess.STARTUPINFO() 

303 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 

304 startupinfo.wShowWindow = 0 #aka win32.con.SW_HIDE 

305 kwargs["startupinfo"] = startupinfo 

306 kwargs["stderr"] = stderr 

307 return kwargs 

308 

309def exec_env(blacklist=("LS_COLORS", )) -> dict: 

310 env = os.environ.copy() 

311 env["XPRA_SKIP_UI"] = "1" 

312 env["XPRA_FORCE_COLOR_LOG"] = "1" 

313 #let's make things more complicated than they should be: 

314 #on win32, the environment can end up containing unicode, and subprocess chokes on it 

315 for k,v in env.items(): 

316 if k in blacklist: 

317 continue 

318 try: 

319 env[k] = bytestostr(v.encode("utf8")) 

320 except Exception: 

321 env[k] = bytestostr(v) 

322 return env 

323 

324 

325class subprocess_caller: 

326 """ 

327 This is the caller side, wrapping the subprocess. 

328 You can call send() to pass packets to it 

329 which will get converted to method calls on the receiving end, 

330 You can register for signals, in which case your callbacks will be called 

331 when those signals are forwarded back. 

332 (there is no validation of which signals are valid or not) 

333 """ 

334 

335 def __init__(self, description="wrapper"): 

336 self.process = None 

337 self.protocol = None 

338 self.command = None 

339 self.description = description 

340 self.send_queue = Queue() 

341 self.signal_callbacks = {} 

342 self.large_packets = [] 

343 #hook a default packet handlers: 

344 self.connect(Protocol.CONNECTION_LOST, self.connection_lost) 

345 self.connect(Protocol.GIBBERISH, self.gibberish) 

346 from gi.repository import GLib 

347 self.idle_add = GLib.idle_add 

348 self.timeout_add = GLib.timeout_add 

349 self.source_remove = GLib.source_remove 

350 

351 

352 def connect(self, signal, cb, *args): 

353 """ gobject style signal registration """ 

354 self.signal_callbacks.setdefault(signal, []).append((cb, list(args))) 

355 

356 

357 def subprocess_exit(self, *args): 

358 #beware: this may fire more than once! 

359 log("subprocess_exit%s command=%s", args, self.command) 

360 self._fire_callback("exit") 

361 

362 def start(self): 

363 assert self.process is None, "already started" 

364 self.process = self.exec_subprocess() 

365 self.protocol = self.make_protocol() 

366 self.protocol.start() 

367 

368 def abort_test(self, action): 

369 p = self.process 

370 if p is None or p.poll(): 

371 raise ConnectionClosedException("cannot %s: subprocess has terminated" % action) from None 

372 

373 def make_protocol(self): 

374 #make a connection using the process stdin / stdout 

375 conn = TwoFileConnection(self.process.stdin, self.process.stdout, 

376 abort_test=self.abort_test, target=self.description, 

377 socktype=self.description, close_cb=self.subprocess_exit) 

378 conn.timeout = 0 

379 protocol = Protocol(self, conn, self.process_packet, get_packet_cb=self.get_packet) 

380 if LOCAL_ALIASES: 

381 protocol.send_aliases = LOCAL_SEND_ALIASES 

382 protocol.receive_aliases = LOCAL_RECEIVE_ALIASES 

383 setup_fastencoder_nocompression(protocol) 

384 protocol.large_packets = self.large_packets 

385 return protocol 

386 

387 

388 def exec_subprocess(self): 

389 kwargs = exec_kwargs() 

390 env = self.get_env() 

391 log("exec_subprocess() command=%s, env=%s, kwargs=%s", self.command, env, kwargs) 

392 proc = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 

393 env=env, start_new_session=True, **kwargs) 

394 getChildReaper().add_process(proc, self.description, self.command, True, True, callback=self.subprocess_exit) 

395 return proc 

396 

397 def get_env(self): 

398 env = exec_env() 

399 env["XPRA_LOG_PREFIX"] = "%s " % self.description 

400 env["XPRA_FIX_UNICODE_OUT"] = "0" 

401 return env 

402 

403 def cleanup(self): 

404 self.stop() 

405 

406 def stop(self): 

407 self.stop_process() 

408 self.stop_protocol() 

409 

410 def stop_process(self): 

411 log("%s.stop_process() sending stop request to %s", self, self.description) 

412 proc = self.process 

413 if proc and proc.poll() is None: 

414 try: 

415 proc.terminate() 

416 self.process = None 

417 except Exception as e: 

418 log.warn("failed to stop the wrapped subprocess %s: %s", proc, e) 

419 

420 def stop_protocol(self): 

421 p = self.protocol 

422 if p: 

423 self.protocol = None 

424 log("%s.stop_protocol() calling %s", self, p.close) 

425 try: 

426 p.close() 

427 except Exception as e: 

428 log.warn("failed to close the subprocess connection: %s", p, e) 

429 

430 

431 def connection_lost(self, *args): 

432 log("connection_lost%s", args) 

433 self.stop() 

434 

435 def gibberish(self, *args): 

436 log.warn("%s stopping on gibberish:", self.description) 

437 log.warn(" %s", repr_ellipsized(args[1], limit=80)) 

438 self.stop() 

439 

440 

441 def get_packet(self): 

442 try: 

443 item = self.send_queue.get(False) 

444 except Exception: 

445 item = None 

446 return (item, None, None, None, False, self.send_queue.qsize()>0) 

447 

448 def send(self, *packet_data): 

449 self.send_queue.put(packet_data) 

450 p = self.protocol 

451 if p: 

452 p.source_has_more() 

453 if FLUSH: 

454 conn = p._conn 

455 if conn and conn.is_active(): 

456 conn.flush() 

457 INJECT_FAULT(p) 

458 

459 def process_packet(self, proto, packet): 

460 if DEBUG_WRAPPER: 

461 log("process_packet(%s, %s)", proto, [str(x)[:32] for x in packet]) 

462 signal_name = bytestostr(packet[0]) 

463 self._fire_callback(signal_name, packet[1:]) 

464 INJECT_FAULT(proto) 

465 

466 def _fire_callback(self, signal_name, extra_args=()): 

467 callbacks = self.signal_callbacks.get(signal_name) 

468 log("firing callback for '%s': %s", signal_name, callbacks) 

469 if callbacks: 

470 for cb, args in callbacks: 

471 try: 

472 all_args = list(args) + list(extra_args) 

473 self.idle_add(cb, self, *all_args) 

474 except Exception: 

475 log.error("error processing callback %s for %s packet", cb, signal_name, exc_info=True)