utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. #
  2. #
  3. # Util classes for Nagios plugins
  4. #
  5. #
  6. #==========================================================================
  7. #
  8. # Version: = '$Id: utils.py 2 2002-02-28 06:42:51Z egalstad $'
  9. #
  10. # (C) Rob W.W. Hooft, Nonius BV, 1998
  11. #
  12. # Contact r.hooft@euromail.net for questions/suggestions.
  13. # See: <http://starship.python.net/crew/hooft/>
  14. # Distribute freely.
  15. #
  16. # jaclu@galdrion.com 2000-07-14
  17. # Some changes in error handling of Run() to avoid error garbage
  18. # when used from Nagios plugins
  19. # I also removed the following functions: AbortableWait() and _buttonkill()
  20. # since they are only usable with Tkinter
  21. import sys,os,signal,time,string
  22. class error(Exception):
  23. pass
  24. class _ready(Exception):
  25. pass
  26. def which(filename):
  27. """Find the file 'filename' in the execution path. If no executable
  28. file is found, return None"""
  29. for dir in string.split(os.environ['PATH'],os.pathsep):
  30. fn=os.path.join(dir,filename)
  31. if os.path.exists(fn):
  32. if os.stat(fn)[0]&0111:
  33. return fn
  34. else:
  35. return None
  36. class Task:
  37. """Manage asynchronous subprocess tasks.
  38. This differs from the 'subproc' package!
  39. - 'subproc' connects to the subprocess via pipes
  40. - 'task' lets the subprocess run autonomously.
  41. After starting the task, we can just:
  42. - ask whether it is finished yet
  43. - wait until it is finished
  44. - perform an 'idle' task (e.g. Tkinter's mainloop) while waiting for
  45. subprocess termination
  46. - kill the subprocess with a specific signal
  47. - ask for the exit code.
  48. Summarizing:
  49. - 'subproc' is a sophisticated os.popen()
  50. - 'task' is a sophisticated os.system()
  51. Another difference of task with 'subproc':
  52. - If the Task() object is deleted, before the subprocess status
  53. was retrieved, the child process will stay.
  54. It will never be waited for (i.e., the process will turn into
  55. a zombie. Not a good idea in general).
  56. Public data:
  57. None.
  58. Public methods:
  59. __init__, __str__, Run, Wait, Kill, Done, Status.
  60. """
  61. def __init__(self,command):
  62. """Constructor.
  63. arguments:
  64. command: the command to run, in the form of a string,
  65. or a tuple or list of words.
  66. """
  67. if type(command)==type(''):
  68. self.cmd=command
  69. self.words=string.split(command)
  70. elif type(command)==type([]) or type(command)==type(()):
  71. # Surround each word by ' '. Limitation: words cannot contain ' chars
  72. self.cmd="'"+string.join(command,"' '")+"'"
  73. self.words=tuple(command)
  74. else:
  75. raise error("command must be tuple, list, or string")
  76. self.pid=None
  77. self.status=None
  78. def Run(self,usesh=0,detach=0,stdout=None,stdin=None,stderr=None):
  79. """Actually run the process.
  80. This method should be called exactly once.
  81. optional arguments:
  82. usesh=0: if 1, run 'sh -c command', if 0, split the
  83. command into words, and run it by ourselves.
  84. If usesh=1, the 'Kill' method might not do what
  85. you want (it will kill the 'sh' process, not the
  86. command).
  87. detach=0: if 1, run 'sh -c 'command&' (regardless of
  88. 'usesh'). Since the 'sh' process will immediately
  89. terminate, the task created will be inherited by
  90. 'init', so you can safely forget it. Remember that if
  91. detach=1, Kill(), Done() and Status() will manipulate
  92. the 'sh' process; there is no way to find out about the
  93. detached process.
  94. stdout=None: filename to use as stdout for the child process.
  95. If None, the stdout of the parent will be used.
  96. stdin= None: filename to use as stdin for the child process.
  97. If None, the stdin of the parent will be used.
  98. stderr=None: filename to use as stderr for the child process.
  99. If None, the stderr of the parent will be used.
  100. return value:
  101. None
  102. """
  103. if self.pid!=None:
  104. raise error("Second run on task forbidden")
  105. self.pid=os.fork()
  106. if not self.pid:
  107. for fn in range(3,256): # Close all non-standard files in a safe way
  108. try:
  109. os.close(fn)
  110. except os.error:
  111. pass
  112. #
  113. # jaclu@galdrion.com 2000-07-14
  114. #
  115. # I changed this bit somewhat, since Nagios plugins
  116. # should send only limited errors to the caller
  117. # The original setup here corupted output when there was an error.
  118. # Instead the caller should check result of Wait() and anything
  119. # not zero should be reported as a failure.
  120. #
  121. try:
  122. if stdout: # Replace stdout by file
  123. os.close(1)
  124. i=os.open(stdout,os.O_CREAT|os.O_WRONLY|os.O_TRUNC,0666)
  125. if i!=1:
  126. sys.stderr.write("stdout not opened on 1!\n")
  127. if stdin: # Replace stdin by file
  128. os.close(0)
  129. i=os.open(stdin,os.O_RDONLY)
  130. if i!=0:
  131. sys.stderr.write("stdin not opened on 0!\n")
  132. if stderr: # Replace stderr by file
  133. os.close(2)
  134. i=os.open(stderr,os.O_CREAT|os.O_WRONLY|os.O_TRUNC,0666)
  135. if i!=2:
  136. sys.stdout.write("stderr not opened on 2!\n")
  137. #try:
  138. if detach:
  139. os.execv('/bin/sh',('sh','-c',self.cmd+'&'))
  140. elif usesh:
  141. os.execv('/bin/sh',('sh','-c',self.cmd))
  142. else:
  143. os.execvp(self.words[0],self.words)
  144. except:
  145. #print self.words
  146. #sys.stderr.write("Subprocess '%s' execution failed!\n"%self.cmd)
  147. sys.exit(1)
  148. else:
  149. # Mother process
  150. if detach:
  151. # Should complete "immediately"
  152. self.Wait()
  153. def Wait(self,idlefunc=None,interval=0.1):
  154. """Wait for the subprocess to terminate.
  155. If the process has already terminated, this function will return
  156. immediately without raising an error.
  157. optional arguments:
  158. idlefunc=None: a callable object (function, class, bound method)
  159. that will be called every 0.1 second (or see
  160. the 'interval' variable) while waiting for
  161. the subprocess to terminate. This can be the
  162. Tkinter 'update' procedure, such that the GUI
  163. doesn't die during the run. If this is set to
  164. 'None', the process will really wait. idlefunc
  165. should ideally not take a very long time to
  166. complete...
  167. interval=0.1: The interval (in seconds) with which the 'idlefunc'
  168. (if any) will be called.
  169. return value:
  170. the exit status of the subprocess (0 if successful).
  171. """
  172. if self.status!=None:
  173. # Already finished
  174. return self.status
  175. if callable(idlefunc):
  176. while 1:
  177. try:
  178. pid,status=os.waitpid(self.pid,os.WNOHANG)
  179. if pid==self.pid:
  180. self.status=status
  181. return status
  182. else:
  183. idlefunc()
  184. time.sleep(interval)
  185. except KeyboardInterrupt:
  186. # Send the interrupt to the inferior process.
  187. self.Kill(signal=signal.SIGINT)
  188. elif idlefunc:
  189. raise error("Non-callable idle function")
  190. else:
  191. while 1:
  192. try:
  193. pid,status=os.waitpid(self.pid,0)
  194. self.status=status
  195. return status
  196. except KeyboardInterrupt:
  197. # Send the interrupt to the inferior process.
  198. self.Kill(signal=signal.SIGINT)
  199. def Kill(self,signal=signal.SIGTERM):
  200. """Send a signal to the running subprocess.
  201. optional arguments:
  202. signal=SIGTERM: number of the signal to send.
  203. (see os.kill)
  204. return value:
  205. see os.kill()
  206. """
  207. if self.status==None:
  208. # Only if it is not already finished
  209. return os.kill(self.pid,signal)
  210. def Done(self):
  211. """Ask whether the process has already finished.
  212. return value:
  213. 1: yes, the process has finished.
  214. 0: no, the process has not finished yet.
  215. """
  216. if self.status!=None:
  217. return 1
  218. else:
  219. pid,status=os.waitpid(self.pid,os.WNOHANG)
  220. if pid==self.pid:
  221. #print "OK:",pid,status
  222. self.status=status
  223. return 1
  224. else:
  225. #print "NOK:",pid,status
  226. return 0
  227. def Status(self):
  228. """Ask for the status of the task.
  229. return value:
  230. None: process has not finished yet (maybe not even started).
  231. any integer: process exit status.
  232. """
  233. self.Done()
  234. return self.status
  235. def __str__(self):
  236. if self.pid!=None:
  237. if self.status!=None:
  238. s2="done, exit status=%d"%self.status
  239. else:
  240. s2="running"
  241. else:
  242. s2="prepared"
  243. return "<%s: '%s', %s>"%(self.__class__.__name__,self.cmd,s2)
  244. #==========================================================================
  245. #
  246. #
  247. # Class: TimeoutHandler
  248. # License: GPL
  249. # Copyright (c) 2000 Jacob Lundqvist (jaclu@galdrion.com)
  250. #
  251. # Version: 1.0 2000-07-14
  252. #
  253. # Description:
  254. # On init, suply a call-back kill_func that should be called on timeout
  255. #
  256. # Make sure that what ever you are doing is calling Check periodically
  257. #
  258. # To check if timeout was triggered call WasTimeOut returns (true/false)
  259. #
  260. import time,sys
  261. class TimeoutHandler:
  262. def __init__(self,kill_func,time_to_live=10,debug=0):
  263. 'Generic time-out handler.'
  264. self.kill_func=kill_func
  265. self.start_time=time.time()
  266. self.stop_time=+self.start_time+int(time_to_live)
  267. self.debug=debug
  268. self.aborted=0
  269. def Check(self):
  270. 'Call this periodically to check for time-out.'
  271. if self.debug:
  272. sys.stdout.write('.')
  273. sys.stdout.flush()
  274. if time.time()>=self.stop_time:
  275. self.TimeOut()
  276. def TimeOut(self):
  277. 'Trigger the time-out callback.'
  278. self.aborted=1
  279. if self.debug:
  280. print 'Timeout, aborting'
  281. self.kill_func()
  282. def WasTimeOut(self):
  283. 'Indicates if timeout was triggered 1=yes, 0=no.'
  284. if self.debug:
  285. print ''
  286. print 'call duration: %.2f seconds' % (time.time()-self.start_time)
  287. return self.aborted