Code

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