Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/child_reaper.py : 97%
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) 2010-2019 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
8# This class is used by the posix server to ensure
9# we reap the dead pids so that they don't become zombies,
10# also used for implementing --exit-with-children
12import os
13import signal
14from gi.repository import GLib
16from xpra.util import envint, envbool
17from xpra.os_util import POSIX
18from xpra.log import Logger
20log = Logger("server", "util", "exec")
23singleton = None
24def getChildReaper():
25 global singleton
26 if singleton is None:
27 singleton = ChildReaper()
28 return singleton
31def reaper_cleanup():
32 global singleton
33 s = singleton
34 if not s:
35 return
36 singleton.cleanup()
37 #keep it around,
38 #so we don't try to reinitialize it from the wrong thread
39 #(signal requires the main thread)
40 #singleton = None
43class ProcInfo:
44 def __repr__(self):
45 return "ProcInfo(%s)" % self.__dict__
47 def get_info(self) -> dict:
48 info = {
49 "pid" : self.pid,
50 "name" : self.name,
51 "command" : self.command,
52 "ignore" : self.ignore,
53 "forget" : self.forget,
54 #not base types:
55 #callback, process
56 "dead" : self.dead,
57 }
58 if self.returncode is not None:
59 info["returncode"] = self.returncode
60 return info
63# Note that this class has async subtleties -- e.g., it is possible for a
64# child to exit and us to receive the SIGCHLD before our fork() returns (and
65# thus before we even know the pid of the child). So be careful:
66# We can also end up with multiple procinfo structures with the same pid,
67# and that should be fine too
68#
69# WNOHANG is a tricky beast, see:
70# https://github.com/gevent/gevent/issues/622
71class ChildReaper:
72 #note: the quit callback will fire only once!
73 def __init__(self, quit_cb=None):
74 log("ChildReaper(%s)", quit_cb)
75 self._quit = quit_cb
76 self._proc_info = []
77 USE_PROCESS_POLLING = not POSIX or envbool("XPRA_USE_PROCESS_POLLING")
78 if USE_PROCESS_POLLING:
79 POLL_DELAY = envint("XPRA_POLL_DELAY", 2)
80 log("using process polling every %s seconds", POLL_DELAY)
81 GLib.timeout_add(POLL_DELAY*1000, self.poll)
82 else:
83 signal.signal(signal.SIGCHLD, self.sigchld)
84 # Check once after the mainloop is running, just in case the exit
85 # conditions are satisfied before we even enter the main loop.
86 # (Programming with unix the signal API sure is annoying.)
87 def check_once():
88 self.check()
89 return False # Only call once
90 GLib.timeout_add(0, check_once)
92 def cleanup(self):
93 self.reap()
94 self.poll()
95 self._proc_info = []
96 self._quit = None
98 def add_process(self, process, name : str, command, ignore=False, forget=False, callback=None):
99 pid = process.pid
100 assert pid>0, "process has no pid!"
101 procinfo = ProcInfo()
102 procinfo.pid = pid
103 procinfo.name = name
104 procinfo.command = command
105 procinfo.ignore = ignore
106 procinfo.forget = forget
107 procinfo.callback = callback
108 procinfo.process = process
109 procinfo.returncode = process.poll()
110 procinfo.dead = procinfo.returncode is not None
111 log("add_process(%s, %s, %s, %s, %s) pid=%s", process, name, command, ignore, forget, pid)
112 #could have died already:
113 self._proc_info.append(procinfo)
114 if procinfo.dead:
115 self.add_dead_process(procinfo)
116 return procinfo
118 def poll(self):
119 #poll each process that is not dead yet:
120 log("poll() procinfo list: %s", self._proc_info)
121 for procinfo in tuple(self._proc_info):
122 process = procinfo.process
123 if not procinfo.dead and process and process.poll() is not None:
124 self.add_dead_process(procinfo)
125 return True
127 def set_quit_callback(self, cb):
128 self._quit = cb
130 def check(self):
131 #see if we are meant to exit-with-children
132 #see if we still have procinfos alive (and not meant to be ignored)
133 self.poll()
134 watched = tuple(procinfo for procinfo in tuple(self._proc_info)
135 if not procinfo.ignore)
136 alive = tuple(procinfo for procinfo in watched
137 if not procinfo.dead)
138 cb = self._quit
139 log("check() watched=%s, alive=%s, quit callback=%s", watched, alive, cb)
140 if watched and not alive:
141 if cb:
142 self._quit = None
143 cb()
144 return False
145 return True
147 def sigchld(self, signum, frame):
148 #we risk race conditions if doing anything in the signal handler,
149 #better run in the main thread asap:
150 GLib.idle_add(self._sigchld, signum, str(frame))
152 def _sigchld(self, signum, frame_str):
153 log("sigchld(%s, %s)", signum, frame_str)
154 self.reap()
156 def get_proc_info(self, pid : int):
157 for proc_info in tuple(self._proc_info):
158 if proc_info.pid==pid:
159 return proc_info
160 return None
162 def add_dead_pid(self, pid : int):
163 #find the procinfo for this pid:
164 matches = [procinfo for procinfo in self._proc_info if procinfo.pid==pid and not procinfo.dead]
165 log("add_dead_pid(%s) matches=%s", pid, matches)
166 if not matches:
167 #not one of ours? odd.
168 return
169 for procinfo in matches:
170 self.add_dead_process(procinfo)
172 def add_dead_process(self, procinfo):
173 log("add_dead_process(%s)", procinfo)
174 process = procinfo.process
175 if procinfo.dead or not process:
176 return
177 procinfo.returncode = process.poll()
178 procinfo.dead = procinfo.returncode is not None
179 cb = procinfo.callback
180 log("add_dead_process returncode=%s, dead=%s, callback=%s", procinfo.returncode, procinfo.dead, cb)
181 if not procinfo.dead:
182 log.warn("Warning: process '%s' is still running", procinfo.name)
183 return
184 if process and cb:
185 procinfo.callback = None
186 GLib.idle_add(cb, process)
187 #once it's dead, clear the reference to the process:
188 #this should free up some resources
189 #and also help to ensure we don't end up here again
190 procinfo.process = None
191 if procinfo.ignore:
192 log("child '%s' with pid %s has terminated (ignored)", procinfo.name, procinfo.pid)
193 else:
194 log.info("child '%s' with pid %s has terminated", procinfo.name, procinfo.pid)
195 if procinfo.forget:
196 #forget it:
197 try:
198 self._proc_info.remove(procinfo)
199 except ValueError: # pragma: no cover
200 log("failed to remove %s from proc info list", procinfo, exc_info=True)
201 log("updated procinfo=%s", procinfo)
202 self.check()
204 def reap(self):
205 self.poll()
206 while POSIX:
207 log("reap() calling os.waitpid%s", (-1, "WNOHANG"))
208 try:
209 pid = os.waitpid(-1, os.WNOHANG)[0]
210 except OSError:
211 break
212 log("reap() waitpid=%s", pid)
213 if pid == 0:
214 break
215 self.add_dead_pid(pid)
217 def get_info(self) -> dict:
218 iv = tuple(self._proc_info)
219 info = {
220 "children" : {
221 "total" : len(iv),
222 "dead" : len(tuple(True for x in iv if x.dead)),
223 "ignored" : len(tuple(True for x in iv if x.ignore)),
224 }
225 }
226 pi = sorted(self._proc_info, key=lambda x: x.pid, reverse=True)
227 cinfo = {}
228 for i, procinfo in enumerate(pi):
229 d = {}
230 for k in ("name", "command", "ignore", "forget", "returncode", "dead", "pid"):
231 v = getattr(procinfo, k)
232 if v is None:
233 continue
234 d[k] = v
235 cinfo[i] = d
236 info["child"] = cinfo
237 return info