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

6 

7 

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 

11 

12import os 

13import signal 

14from gi.repository import GLib 

15 

16from xpra.util import envint, envbool 

17from xpra.os_util import POSIX 

18from xpra.log import Logger 

19 

20log = Logger("server", "util", "exec") 

21 

22 

23singleton = None 

24def getChildReaper(): 

25 global singleton 

26 if singleton is None: 

27 singleton = ChildReaper() 

28 return singleton 

29 

30 

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 

41 

42 

43class ProcInfo: 

44 def __repr__(self): 

45 return "ProcInfo(%s)" % self.__dict__ 

46 

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 

61 

62 

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) 

91 

92 def cleanup(self): 

93 self.reap() 

94 self.poll() 

95 self._proc_info = [] 

96 self._quit = None 

97 

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 

117 

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 

126 

127 def set_quit_callback(self, cb): 

128 self._quit = cb 

129 

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 

146 

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

151 

152 def _sigchld(self, signum, frame_str): 

153 log("sigchld(%s, %s)", signum, frame_str) 

154 self.reap() 

155 

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 

161 

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) 

171 

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

203 

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) 

216 

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