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# -*- coding: utf-8 -*- 

2# This file is part of Xpra. 

3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com> 

5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

6# later version. See the file COPYING for details. 

7 

8import os.path 

9 

10from xpra.platform.features import COMMAND_SIGNALS 

11from xpra.child_reaper import getChildReaper, reaper_cleanup 

12from xpra.os_util import monotonic_time, bytestostr, OSX, WIN32, POSIX 

13from xpra.util import envint, csv 

14from xpra.make_thread import start_thread 

15from xpra.scripts.parsing import parse_env, get_subcommands 

16from xpra.server.server_util import source_env 

17from xpra.server import EXITING_CODE 

18from xpra.server.mixins.stub_server_mixin import StubServerMixin 

19from xpra.log import Logger 

20 

21log = Logger("exec") 

22 

23TERMINATE_DELAY = envint("XPRA_TERMINATE_DELAY", 1000)/1000.0 

24MENU_RELOAD_DELAY = envint("XPRA_MENU_RELOAD_DELAY", 5) 

25EXPORT_XDG_MENU_DATA = envint("XPRA_EXPORT_XDG_MENU_DATA", True) 

26 

27 

28def noicondata(menu_data): 

29 newdata = {} 

30 for k,v in menu_data.items(): 

31 if k in ("IconData", b"IconData"): 

32 continue 

33 if isinstance(v, dict): 

34 newdata[k] = noicondata(v) 

35 else: 

36 newdata[k] = v 

37 return newdata 

38 

39""" 

40Mixin for servers that can handle file transfers and forwarded printers. 

41Printer forwarding is only supported on Posix servers with the cups backend script. 

42""" 

43class ChildCommandServer(StubServerMixin): 

44 

45 def __init__(self): 

46 self.child_display = None 

47 self.start_commands = [] 

48 self.start_child_commands = [] 

49 self.start_after_connect = [] 

50 self.start_child_after_connect = [] 

51 self.start_on_connect = [] 

52 self.start_child_on_connect = [] 

53 self.start_on_last_client_exit = [] 

54 self.start_child_on_last_client_exit = [] 

55 self.exit_with_children = False 

56 self.children_count = 0 

57 self.start_after_connect_done = False 

58 self.start_new_commands = False 

59 self.source_env = {} 

60 self.start_env = {} 

61 self.exec_cwd = None 

62 self.exec_wrapper = None 

63 self.terminate_children = False 

64 self.children_started = [] 

65 self.child_reaper = None 

66 self.reaper_exit = self.reaper_exit_check 

67 self.watch_manager = None 

68 self.watch_notifier = None 

69 self.xdg_menu_reload_timer = None 

70 #does not belong here... 

71 if not hasattr(self, "_upgrading"): 

72 self._upgrading = False 

73 if not hasattr(self, "session_name"): 

74 self.session_name = "" 

75 

76 def init(self, opts): 

77 self.exit_with_children = opts.exit_with_children 

78 self.terminate_children = opts.terminate_children 

79 self.start_new_commands = opts.start_new_commands 

80 self.start_commands = opts.start 

81 self.start_child_commands = opts.start_child 

82 self.start_after_connect = opts.start_after_connect 

83 self.start_child_after_connect = opts.start_child_after_connect 

84 self.start_on_connect = opts.start_on_connect 

85 self.start_child_on_connect = opts.start_child_on_connect 

86 self.start_on_last_client_exit = opts.start_on_last_client_exit 

87 self.start_child_on_last_client_exit = opts.start_child_on_last_client_exit 

88 if opts.exec_wrapper: 

89 import shlex 

90 self.exec_wrapper = shlex.split(opts.exec_wrapper) 

91 self.child_reaper = getChildReaper() 

92 self.source_env = source_env(opts.source_start) 

93 self.start_env = parse_env(opts.start_env) 

94 

95 def threaded_setup(self): 

96 self.exec_start_commands() 

97 def set_reaper_callback(): 

98 self.child_reaper.set_quit_callback(self.reaper_exit) 

99 self.child_reaper.check() 

100 self.idle_add(set_reaper_callback) 

101 if POSIX and not OSX and self.start_new_commands and EXPORT_XDG_MENU_DATA: 

102 try: 

103 self.setup_menu_watcher() 

104 except Exception as e: 

105 log("threaded_setup()", exc_info=True) 

106 log.error("Error setting up menu watcher:") 

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

108 from xpra.platform.xposix.xdg_helper import load_xdg_menu_data 

109 #start loading in a thread, 

110 #so server startup can complete: 

111 start_thread(load_xdg_menu_data, "load-xdg-menu-data", True) 

112 

113 

114 def setup_menu_watcher(self): 

115 try: 

116 import pyinotify 

117 except ImportError as e: 

118 log("setup_menu_watcher() cannot import pyinotify", exc_info=True) 

119 log.warn("Warning: cannot watch for application menu changes without pyinotify:") 

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

121 return 

122 self.watch_manager = pyinotify.WatchManager() 

123 def menu_data_updated(create, pathname): 

124 log("menu_data_updated(%s, %s)", create, pathname) 

125 self.schedule_xdg_menu_reload() 

126 class EventHandler(pyinotify.ProcessEvent): 

127 def process_IN_CREATE(self, event): 

128 menu_data_updated(True, event.pathname) 

129 def process_IN_DELETE(self, event): 

130 menu_data_updated(False, event.pathname) 

131 mask = pyinotify.IN_DELETE | pyinotify.IN_CREATE #@UndefinedVariable pylint: disable=no-member 

132 handler = EventHandler() 

133 self.watch_notifier = pyinotify.ThreadedNotifier(self.watch_manager, handler) 

134 self.watch_notifier.setDaemon(True) 

135 data_dirs = os.environ.get("XDG_DATA_DIRS", "/usr/share/applications:/usr/local/share/applications").split(":") 

136 watched = [] 

137 for data_dir in data_dirs: 

138 menu_dir = os.path.join(data_dir, "applications") 

139 if not os.path.exists(menu_dir) or menu_dir in watched: 

140 continue 

141 wdd = self.watch_manager.add_watch(menu_dir, mask) 

142 watched.append(menu_dir) 

143 log("watch_notifier=%s, watch=%s", self.watch_notifier, wdd) 

144 self.watch_notifier.start() 

145 if watched: 

146 log.info("watching for applications menu changes in:") 

147 for wd in watched: 

148 log.info(" '%s'", wd) 

149 

150 

151 def cleanup(self): 

152 if self.terminate_children and self._upgrading!=EXITING_CODE: 

153 self.terminate_children_processes() 

154 def noop(): 

155 pass 

156 self.reaper_exit = noop 

157 reaper_cleanup() 

158 xmrt = self.xdg_menu_reload_timer 

159 if xmrt: 

160 self.xdg_menu_reload_timer = None 

161 self.source_remove(xmrt) 

162 wn = self.watch_notifier 

163 if wn: 

164 self.watch_notifier = None 

165 wn.stop() 

166 watch_manager = self.watch_manager 

167 if watch_manager: 

168 self.watch_manager = None 

169 try: 

170 watch_manager.close() 

171 except OSError: 

172 log("error closing watch manager %s", watch_manager, exc_info=True) 

173 

174 

175 def get_server_features(self, _source) -> dict: 

176 return { 

177 "start-new-commands" : self.start_new_commands, 

178 "exit-with-children" : self.exit_with_children, 

179 "server-commands-signals" : COMMAND_SIGNALS, 

180 "server-commands-info" : not WIN32 and not OSX, 

181 "xdg-menu-update" : POSIX and not OSX, 

182 } 

183 

184 

185 def _get_xdg_menu_data(self, force_reload=False): 

186 if not EXPORT_XDG_MENU_DATA: 

187 return None 

188 if not self.start_new_commands: 

189 return None 

190 if OSX: 

191 return None 

192 if POSIX: 

193 from xpra.platform.xposix.xdg_helper import load_xdg_menu_data 

194 return load_xdg_menu_data(force_reload) 

195 if WIN32: 

196 from xpra.platform.win32.menu_helper import load_menu 

197 return load_menu() 

198 log.error("Error: unsupported platform!") 

199 return None 

200 

201 def get_caps(self, source) -> dict: 

202 caps = {} 

203 if not source: 

204 return caps 

205 #don't assume we have a real ClientConnection object: 

206 if getattr(source, "wants_features", False) and getattr(source, "ui_client", False): 

207 caps["xdg-menu"] = {} 

208 if not source.xdg_menu_update: 

209 #we have to send it now: 

210 xdg_menu = self._get_xdg_menu_data() 

211 log("%i entries sent in hello", len(xdg_menu or ())) 

212 if xdg_menu: 

213 l = len(str(xdg_menu)) 

214 #arbitrary: don't use more than half 

215 #of the maximum size of the hello packet: 

216 if l>2*1024*1024: 

217 from xpra.platform.xposix.xdg_helper import remove_icons 

218 xdg_menu = remove_icons(xdg_menu) 

219 log.info("removed icons to reduce the size of the xdg menu data") 

220 log.info("size reduced from %i to %i", l, len(str(xdg_menu))) 

221 caps["xdg-menu"] = xdg_menu 

222 caps["subcommands"] = get_subcommands() 

223 return caps 

224 

225 def send_initial_data(self, ss, caps, send_ui, share_count): 

226 if ss.xdg_menu_update: 

227 #this method may block if the menus are still being loaded, 

228 #so do it in a throw-away thread: 

229 start_thread(self.send_xdg_menu_data, "send-xdg-menu-data", True, (ss,)) 

230 

231 def send_xdg_menu_data(self, ss): 

232 if ss.is_closed(): 

233 return 

234 xdg_menu = self._get_xdg_menu_data() or {} 

235 log("%i entries sent in initial data", len(xdg_menu)) 

236 ss.send_setting_change("xdg-menu", xdg_menu) 

237 

238 def schedule_xdg_menu_reload(self): 

239 xmrt = self.xdg_menu_reload_timer 

240 if xmrt: 

241 self.source_remove(xmrt) 

242 self.xdg_menu_reload_timer = self.timeout_add(MENU_RELOAD_DELAY*1000, self.xdg_menu_reload) 

243 

244 def xdg_menu_reload(self): 

245 self.xdg_menu_reload_timer = None 

246 log("xdg_menu_reload()") 

247 xdg_menu = self._get_xdg_menu_data(True) 

248 for source in tuple(self._server_sources.values()): 

249 if source.xdg_menu_update: 

250 source.send_setting_change("xdg-menu", xdg_menu or {}) 

251 return False 

252 

253 def get_info(self, _proto) -> dict: 

254 info = { 

255 "start" : self.start_commands, 

256 "start-child" : self.start_child_commands, 

257 "start-after-connect" : self.start_after_connect, 

258 "start-child-after-connect" : self.start_child_after_connect, 

259 "start-on-connect" : self.start_on_connect, 

260 "start-child-on-connect" : self.start_child_on_connect, 

261 "exit-with-children" : self.exit_with_children, 

262 "start-after-connect-done" : self.start_after_connect_done, 

263 "start-new" : self.start_new_commands, 

264 } 

265 md = self._get_xdg_menu_data() 

266 if md: 

267 info["start-menu"] = noicondata(md) 

268 for i,procinfo in enumerate(self.children_started): 

269 info[i] = procinfo.get_info() 

270 return {"commands": info} 

271 

272 

273 def last_client_exited(self): 

274 self._exec_commands(self.start_on_last_client_exit, self.start_child_on_last_client_exit) 

275 

276 

277 def get_child_env(self): 

278 #subclasses may add more items (ie: fakexinerama) 

279 env = super().get_child_env() 

280 env.update(self.source_env) 

281 env.update(self.start_env) 

282 if self.child_display: 

283 env["DISPLAY"] = self.child_display 

284 return env 

285 

286 def get_full_child_command(self, cmd, use_wrapper=True) -> list: 

287 #make sure we have it as a list: 

288 cmd = super().get_full_child_command(cmd, use_wrapper) 

289 if not use_wrapper or not self.exec_wrapper: 

290 return cmd 

291 return self.exec_wrapper + cmd 

292 

293 

294 def exec_start_commands(self): 

295 log("exec_start_commands() start=%s, start_child=%s", self.start_commands, self.start_child_commands) 

296 self._exec_commands(self.start_commands, self.start_child_commands) 

297 

298 def exec_after_connect_commands(self): 

299 log("exec_after_connect_commands() start=%s, start_child=%s", 

300 self.start_after_connect, self.start_child_after_connect) 

301 self._exec_commands(self.start_after_connect, self.start_child_after_connect) 

302 

303 def exec_on_connect_commands(self): 

304 log("exec_on_connect_commands() start=%s, start_child=%s", self.start_on_connect, self.start_child_on_connect) 

305 self._exec_commands(self.start_on_connect, self.start_child_on_connect) 

306 

307 def _exec_commands(self, start_list, start_child_list): 

308 started = [] 

309 if start_list: 

310 for x in start_list: 

311 if x: 

312 proc = self.start_command(x, x, ignore=True) 

313 if proc: 

314 started.append(proc) 

315 if start_child_list: 

316 for x in start_child_list: 

317 if x: 

318 proc = self.start_command(x, x, ignore=False) 

319 if proc: 

320 started.append(proc) 

321 procs = tuple(x for x in started if x is not None) 

322 if not self.session_name: 

323 self.idle_add(self.guess_session_name, procs) 

324 

325 def start_command(self, name, child_cmd, ignore=False, callback=None, use_wrapper=True, shell=False, **kwargs): 

326 from subprocess import Popen 

327 env = self.get_child_env() 

328 log("start_command%s exec_wrapper=%s, exec_cwd=%s", 

329 (name, child_cmd, ignore, callback, use_wrapper, shell, kwargs), self.exec_wrapper, self.exec_cwd) 

330 try: 

331 real_cmd = self.get_full_child_command(child_cmd, use_wrapper) 

332 log("full child command(%s, %s)=%s", child_cmd, use_wrapper, real_cmd) 

333 proc = Popen(real_cmd, env=env, shell=shell, cwd=self.exec_cwd, **kwargs) 

334 procinfo = self.add_process(proc, name, real_cmd, ignore=ignore, callback=callback) 

335 log("pid(%s)=%s", real_cmd, proc.pid) 

336 log.info("started command '%s' with pid %s", " ".join(bytestostr(x) for x in real_cmd), proc.pid) 

337 if not ignore: 

338 self.children_count += 1 

339 self.children_started.append(procinfo) 

340 return proc 

341 except (OSError, ValueError) as e: 

342 log("start_command%s", (name, child_cmd, ignore, callback, use_wrapper, shell, kwargs), exc_info=True) 

343 log.error("Error spawning child '%s':" % (child_cmd, )) 

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

345 return None 

346 

347 

348 def add_process(self, process, name, command, ignore=False, callback=None): 

349 return self.child_reaper.add_process(process, name, command, ignore, callback=callback) 

350 

351 def is_child_alive(self, proc) -> bool: 

352 return proc is not None and proc.poll() is None 

353 

354 def reaper_exit_check(self): 

355 log("reaper_exit_check() exit_with_children=%s", self.exit_with_children) 

356 if self.exit_with_children and self.children_count: 

357 log.info("all children have exited and --exit-with-children was specified, exiting") 

358 self.idle_add(self.clean_quit) 

359 

360 def terminate_children_processes(self): 

361 cl = tuple(self.children_started) 

362 self.children_started = [] 

363 log("terminate_children_processes() children=%s", cl) 

364 if not cl: 

365 return 

366 wait_for = [] 

367 self.child_reaper.poll() 

368 for procinfo in cl: 

369 proc = procinfo.process 

370 name = procinfo.name 

371 if self.is_child_alive(proc): 

372 wait_for.append(procinfo) 

373 log("child command '%s' is still alive, calling terminate on %s", name, proc) 

374 try: 

375 proc.terminate() 

376 except Exception as e: 

377 log("failed to terminate %s: %s", proc, e) 

378 del e 

379 if not wait_for: 

380 return 

381 log("waiting for child commands to exit: %s", wait_for) 

382 start = monotonic_time() 

383 while monotonic_time()-start<TERMINATE_DELAY and wait_for: 

384 self.child_reaper.poll() 

385 #this is called from the UI thread, we cannot sleep 

386 #sleep(1) 

387 wait_for = [procinfo for procinfo in wait_for if self.is_child_alive(procinfo.process)] 

388 log("still not terminated: %s", wait_for) 

389 log("done waiting for child commands") 

390 

391 def guess_session_name(self, procs=None): 

392 if not procs: 

393 return 

394 #use the commands to define the session name: 

395 self.child_reaper.poll() 

396 cmd_names = [] 

397 for proc in procs: 

398 proc_info = self.child_reaper.get_proc_info(proc.pid) 

399 if not proc_info: 

400 continue 

401 cmd = proc_info.command 

402 if self.exec_wrapper: 

403 #strip exec wrapper 

404 l = len(self.exec_wrapper) 

405 if len(cmd)>l and cmd[:l]==self.exec_wrapper: 

406 cmd = cmd[l:] 

407 elif len(cmd)>1 and cmd[0] in ("vglrun", "nohup",): 

408 cmd.pop(0) 

409 bcmd = os.path.basename(cmd[0]) 

410 if bcmd not in cmd_names: 

411 cmd_names.append(bcmd) 

412 log("guess_session_name() commands=%s", cmd_names) 

413 if cmd_names: 

414 new_name = csv(cmd_names) 

415 if self.session_name!=new_name: 

416 self.session_name = new_name 

417 self.mdns_update() 

418 

419 

420 def _process_start_command(self, proto, packet): 

421 log("start new command: %s", packet) 

422 if not self.start_new_commands: 

423 log.warn("Warning: received start-command request,") 

424 log.warn(" but the feature is currently disabled") 

425 return 

426 name, command, ignore = packet[1:4] 

427 if isinstance(command, (list, tuple)): 

428 cmd = command 

429 else: 

430 cmd = command.decode("utf-8") 

431 proc = self.start_command(name.decode("utf-8"), cmd, ignore) 

432 if len(packet)>=5: 

433 shared = packet[4] 

434 if proc and not shared: 

435 ss = self.get_server_source(proto) 

436 assert ss 

437 log("adding filter: pid=%s for %s", proc.pid, proto) 

438 ss.add_window_filter("window", "pid", "=", proc.pid) 

439 log("process_start_command: proc=%s", proc) 

440 

441 def _process_command_signal(self, _proto, packet): 

442 pid = packet[1] 

443 signame = bytestostr(packet[2]) 

444 if signame not in COMMAND_SIGNALS: 

445 log.warn("Warning: invalid signal received: '%s'", signame) 

446 return 

447 procinfo = self.child_reaper.get_proc_info(pid) 

448 if not procinfo: 

449 log.warn("Warning: command not found for pid %i", pid) 

450 return 

451 if procinfo.returncode is not None: 

452 log.warn("Warning: command for pid %i has already terminated", pid) 

453 return 

454 import signal 

455 sigval = getattr(signal, signame, None) 

456 if not sigval: 

457 log.error("Error: signal '%s' not found!", signame) 

458 return 

459 log.info("sending signal %s to pid %i", signame, pid) 

460 try: 

461 os.kill(pid, sigval) 

462 except Exception as e: 

463 log.error("Error sending signal '%s' to pid %i", signame, pid) 

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

465 

466 

467 def init_packet_handlers(self): 

468 log("init_packet_handlers() COMMANDS_SIGNALS=%s, start new commands=%s", 

469 COMMAND_SIGNALS, self.start_new_commands) 

470 if COMMAND_SIGNALS: 

471 self.add_packet_handler("command-signal", self._process_command_signal, False) 

472 if self.start_new_commands: 

473 self.add_packet_handler("start-command", self._process_start_command)