Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/mixins/child_command_server.py : 71%
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.
8import os.path
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
21log = Logger("exec")
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)
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
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):
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 = ""
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)
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)
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)
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)
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 }
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
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
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,))
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)
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)
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
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}
273 def last_client_exited(self):
274 self._exec_commands(self.start_on_last_client_exit, self.start_child_on_last_client_exit)
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
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
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)
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)
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)
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)
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
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)
351 def is_child_alive(self, proc) -> bool:
352 return proc is not None and proc.poll() is None
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)
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")
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()
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)
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)
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)