Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/process/plugins.py

Revision 2012 (checked in by fumanchu, 2 weeks ago)

Doc tweaks.

  • Property svn:eol-style set to native
Line 
1 """Site services for use with a Web Site Process Bus."""
2
3 import os
4 import re
5 try:
6     set
7 except NameError:
8     from sets import Set as set
9 import signal as _signal
10 import sys
11 import time
12 import threading
13
14
15 class SimplePlugin(object):
16     """Plugin base class which auto-subscribes methods for known channels."""
17    
18     def __init__(self, bus):
19         self.bus = bus
20    
21     def subscribe(self):
22         """Register this object as a (multi-channel) listener on the bus."""
23         for channel in self.bus.listeners:
24             # Subscribe self.start, self.exit, etc. if present.
25             method = getattr(self, channel, None)
26             if method is not None:
27                 self.bus.subscribe(channel, method)
28    
29     def unsubscribe(self):
30         """Unregister this object as a listener on the bus."""
31         for channel in self.bus.listeners:
32             # Unsubscribe self.start, self.exit, etc. if present.
33             method = getattr(self, channel, None)
34             if method is not None:
35                 self.bus.unsubscribe(channel, method)
36
37
38
39 class SignalHandler(object):
40     """Register bus channels (and listeners) for system signals.
41     
42     By default, instantiating this object subscribes the following signals
43     and listeners:
44     
45         TERM: bus.exit
46         HUP : bus.restart
47         USR1: bus.graceful
48     """
49    
50     # Map from signal numbers to names
51     signals = {}
52     for k, v in vars(_signal).items():
53         if k.startswith('SIG') and not k.startswith('SIG_'):
54             signals[v] = k
55     del k, v
56    
57     def __init__(self, bus):
58         self.bus = bus
59         # Set default handlers
60         self.handlers = {'SIGTERM': self.bus.exit,
61                          'SIGHUP': self.handle_SIGHUP,
62                          'SIGUSR1': self.bus.graceful,
63                          }
64        
65         self._previous_handlers = {}
66    
67     def subscribe(self):
68         for sig, func in self.handlers.iteritems():
69             try:
70                 self.set_handler(sig, func)
71             except ValueError:
72                 pass
73    
74     def unsubscribe(self):
75         for signum, handler in self._previous_handlers.iteritems():
76             signame = self.signals[signum]
77            
78             if handler is None:
79                 self.bus.log("Restoring %s handler to SIG_DFL." % signame)
80                 handler = _signal.SIG_DFL
81             else:
82                 self.bus.log("Restoring %s handler %r." % (signame, handler))
83            
84             try:
85                 our_handler = _signal.signal(signum, handler)
86                 if our_handler is None:
87                     self.bus.log("Restored old %s handler %r, but our "
88                                  "handler was not registered." %
89                                  (signame, handler), level=30)
90             except ValueError:
91                 self.bus.log("Unable to restore %s handler %r." %
92                              (signame, handler), level=40, traceback=True)
93    
94     def set_handler(self, signal, listener=None):
95         """Subscribe a handler for the given signal (number or name).
96         
97         If the optional 'listener' argument is provided, it will be
98         subscribed as a listener for the given signal's channel.
99         
100         If the given signal name or number is not available on the current
101         platform, ValueError is raised.
102         """
103         if isinstance(signal, basestring):
104             signum = getattr(_signal, signal, None)
105             if signum is None:
106                 raise ValueError("No such signal: %r" % signal)
107             signame = signal
108         else:
109             try:
110                 signame = self.signals[signal]
111             except KeyError:
112                 raise ValueError("No such signal: %r" % signal)
113             signum = signal
114        
115         prev = _signal.signal(signum, self._handle_signal)
116         self._previous_handlers[signum] = prev
117        
118         if listener is not None:
119             self.bus.log("Listening for %s." % signame)
120             self.bus.subscribe(signame, listener)
121    
122     def _handle_signal(self, signum=None, frame=None):
123         """Python signal handler (self.set_handler subscribes it for you)."""
124         signame = self.signals[signum]
125         self.bus.log("Caught signal %s." % signame)
126         self.bus.publish(signame)
127    
128     def handle_SIGHUP(self):
129         if os.isatty(sys.stdin.fileno()):
130             # not daemonized (may be foreground or background)
131             self.bus.log("SIGHUP caught but not daemonized. Exiting.")
132             self.bus.exit()
133         else:
134             self.bus.log("SIGHUP caught while daemonized. Restarting.")
135             self.bus.restart()
136
137
138 try:
139     import pwd, grp
140 except ImportError:
141     pwd, grp = None, None
142
143
144 class DropPrivileges(SimplePlugin):
145     """Drop privileges. uid/gid arguments not available on Windows.
146     
147     Special thanks to Gavin Baker: http://antonym.org/node/100.
148     """
149    
150     def __init__(self, bus, umask=None, uid=None, gid=None):
151         SimplePlugin.__init__(self, bus)
152         self.finalized = False
153         self.uid = uid
154         self.gid = gid
155         self.umask = umask
156    
157     def _get_uid(self):
158         return self._uid
159     def _set_uid(self, val):
160         if val is not None:
161             if pwd is None:
162                 self.bus.log("pwd module not available; ignoring uid.",
163                              level=30)
164                 val = None
165             elif isinstance(val, basestring):
166                 val = pwd.getpwnam(val)[2]
167         self._uid = val
168     uid = property(_get_uid, _set_uid, doc="The uid under which to run.")
169    
170     def _get_gid(self):
171         return self._gid
172     def _set_gid(self, val):
173         if val is not None:
174             if grp is None:
175                 self.bus.log("grp module not available; ignoring gid.",
176                              level=30)
177                 val = None
178             elif isinstance(val, basestring):
179                 val = grp.getgrnam(val)[2]
180         self._gid = val
181     gid = property(_get_gid, _set_gid, doc="The gid under which to run.")
182    
183     def _get_umask(self):
184         return self._umask
185     def _set_umask(self, val):
186         if val is not None:
187             try:
188                 os.umask
189             except AttributeError:
190                 self.bus.log("umask function not available; ignoring umask.",
191                              level=30)
192                 val = None
193         self._umask = val
194     umask = property(_get_umask, _set_umask, doc="The umask under which to run.")
195    
196     def start(self):
197         # uid/gid
198         def current_ids():
199             """Return the current (uid, gid) if available."""
200             name, group = None, None
201             if pwd:
202                 name = pwd.getpwuid(os.getuid())[0]
203             if grp:
204                 group = grp.getgrgid(os.getgid())[0]
205             return name, group
206        
207         if self.finalized:
208             if not (self.uid is None and self.gid is None):
209                 self.bus.log('Already running as uid: %r gid: %r' %
210                              current_ids())
211         else:
212             if self.uid is None and self.gid is None:
213                 if pwd or grp:
214                     self.bus.log('uid/gid not set', level=30)
215             else:
216                 self.bus.log('Started as uid: %r gid: %r' % current_ids())
217                 if self.gid is not None:
218                     os.setgid(gid)
219                 if self.uid is not None:
220                     os.setuid(uid)
221                 self.bus.log('Running as uid: %r gid: %r' % current_ids())
222        
223         # umask
224         if self.finalized:
225             if self.umask is not None:
226                 self.bus.log('umask already set to: %03o' % self.umask)
227         else:
228             if self.umask is None:
229                 self.bus.log('umask not set', level=30)
230             else:
231                 old_umask = os.umask(self.umask)
232                 self.bus.log('umask old: %03o, new: %03o' %
233                              (old_umask, self.umask))
234        
235         self.finalized = True
236     start.priority = 75
237
238
239 class Daemonizer(SimplePlugin):
240     """Daemonize the running script.
241     
242     Use this with a Web Site Process Bus via:
243         
244         Daemonizer(bus).subscribe()
245     
246     When this component finishes, the process is completely decoupled from
247     the parent environment. Please note that when this component is used,
248     the return code from the parent process will still be 0 if a startup
249     error occurs in the forked children. Errors in the initial daemonizing
250     process still return proper exit codes. Therefore, if you use this
251     plugin to daemonize, don't use the return code as an accurate indicator
252     of whether the process fully started. In fact, that return code only
253     indicates if the process succesfully finished the first fork.
254     """
255    
256     def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
257                  stderr='/dev/null'):
258         SimplePlugin.__init__(self, bus)
259         self.stdin = stdin
260         self.stdout = stdout
261         self.stderr = stderr
262         self.finalized = False
263    
264     def start(self):
265         if self.finalized:
266             self.bus.log('Already deamonized.')
267        
268         # forking has issues with threads:
269         # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
270         # "The general problem with making fork() work in a multi-threaded
271         #  world is what to do with all of the threads..."
272         # So we check for active threads:
273         if threading.activeCount() != 1:
274             self.bus.log('There are %r active threads. '
275                          'Daemonizing now may cause strange failures.' %
276                          threading.enumerate(), level=30)
277        
278         # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
279         # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
280         # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
281        
282         # Finish up with the current stdout/stderr
283         sys.stdout.flush()
284         sys.stderr.flush()
285        
286         # Do first fork.
287         try:
288             pid = os.fork()
289             if pid == 0:
290                 # This is the child process. Continue.
291                 pass
292             else:
293                 # This is the first parent. Exit, now that we've forked.
294                 self.bus.log('Forking once.')
295                 os._exit(0)
296         except OSError, exc:
297             # Python raises OSError rather than returning negative numbers.
298             sys.exit("%s: fork #1 failed: (%d) %s\n"
299                      % (sys.argv[0], exc.errno, exc.strerror))
300        
301         os.setsid()
302        
303         # Do second fork
304         try:
305             pid = os.fork()
306             if pid > 0:
307                 self.bus.log('Forking twice.')
308                 os._exit(0) # Exit second parent
309         except OSError, exc:
310             sys.exit("%s: fork #2 failed: (%d) %s\n"
311                      % (sys.argv[0], exc.errno, exc.strerror))
312        
313         os.chdir("/")
314         os.umask(0)
315        
316         si = open(self.stdin, "r")
317         so = open(self.stdout, "a+")
318         se = open(self.stderr, "a+", 0)
319
320         # os.dup2(fd, fd2) will close fd2 if necessary,
321         # so we don't explicitly close stdin/out/err.
322         # See http://docs.python.org/lib/os-fd-ops.html
323         os.dup2(si.fileno(), sys.stdin.fileno())
324         os.dup2(so.fileno(), sys.stdout.fileno())
325         os.dup2(se.fileno(), sys.stderr.fileno())
326        
327         self.bus.log('Daemonized to PID: %s' % os.getpid())
328         self.finalized = True
329     start.priority = 65
330
331
332 class PIDFile(SimplePlugin):
333     """Maintain a PID file via a WSPBus."""
334    
335     def __init__(self, bus, pidfile):
336         SimplePlugin.__init__(self, bus)
337         self.pidfile = pidfile
338         self.finalized = False
339    
340     def start(self):
341         pid = os.getpid()
342         if self.finalized:
343             self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
344         else:
345             open(self.pidfile, "wb").write(str(pid))
346             self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
347             self.finalized = True
348     start.priority = 70
349    
350     def exit(self):
351         try:
352             os.remove(self.pidfile)
353             self.bus.log('PID file removed: %r.' % self.pidfile)
354         except (KeyboardInterrupt, SystemExit):
355             raise
356         except:
357             pass
358
359
360 class PerpetualTimer(threading._Timer):
361     """A subclass of threading._Timer whose run() method repeats."""
362    
363     def run(self):
364         while True:
365             self.finished.wait(self.interval)
366             if self.finished.isSet():
367                 return
368             self.function(*self.args, **self.kwargs)
369
370
371 class Monitor(SimplePlugin):
372     """WSPBus listener to periodically run a callback in its own thread.
373     
374     bus: a Web Site Process Bus object.
375     callback: the function to call at intervals.
376     frequency: the time in seconds between callback runs.
377     """
378    
379     frequency = 60
380    
381     def __init__(self, bus, callback, frequency=60):
382         SimplePlugin.__init__(self, bus)
383         self.callback = callback
384         self.frequency = frequency
385         self.thread = None
386    
387     def start(self):
388         """Start our callback in its own perpetual timer thread."""
389         if self.frequency > 0:
390             threadname = self.__class__.__name__
391             if self.thread is None:
392                 self.thread = PerpetualTimer(self.frequency, self.callback)
393                 self.thread.setName(threadname)
394                 self.thread.start()
395                 self.bus.log("Started monitor thread %r." % threadname)
396             else:
397                 self.bus.log("Monitor thread %r already started." % threadname)
398     start.priority = 70
399    
400     def stop(self):
401         """Stop our callback's perpetual timer thread."""
402         if self.thread is None:
403             self.bus.log("No thread running for %s." % self.__class__.__name__)
404         else:
405             if self.thread is not threading.currentThread():
406                 name = self.thread.getName()
407                 self.thread.cancel()
408                 self.thread.join()
409                 self.bus.log("Stopped thread %r." % name)
410             self.thread = None
411    
412     def graceful(self):
413         """Stop the callback's perpetual timer thread and restart it."""
414         self.stop()
415         self.start()
416
417
418 class Autoreloader(Monitor):
419     """Monitor which re-executes the process when files change."""
420    
421     frequency = 1
422     match = '.*'
423    
424     def __init__(self, bus, frequency=1, match='.*'):
425         self.mtimes = {}
426         self.files = set()
427         self.match = match
428         Monitor.__init__(self, bus, self.run, frequency)
429    
430     def start(self):
431         """Start our own perpetual timer thread for self.run."""
432         if self.thread is None:
433             self.mtimes = {}
434         Monitor.start(self)
435     start.priority = 70
436    
437     def run(self):
438         """Reload the process if registered files have been modified."""
439         sysfiles = set()
440         for k, m in sys.modules.items():
441             if re.match(self.match, k):
442                 if hasattr(m, '__loader__'):
443                     if hasattr(m.__loader__, 'archive'):
444                         k = m.__loader__.archive
445                 k = getattr(m, '__file__', None)
446                 sysfiles.add(k)
447        
448         for filename in sysfiles | self.files:
449             if filename:
450                 if filename.endswith('.pyc'):
451                     filename = filename[:-1]
452                
453                 oldtime = self.mtimes.get(filename, 0)
454                 if oldtime is None:
455                     # Module with no .py file. Skip it.
456                     continue
457                
458                 try:
459                     mtime = os.stat(filename).st_mtime
460                 except OSError:
461                     # Either a module with no .py file, or it's been deleted.
462                     mtime = None
463                
464                 if filename not in self.mtimes:
465                     # If a module has no .py file, this will be None.
466                     self.mtimes[filename] = mtime
467                 else:
468                     if mtime is None or mtime > oldtime:
469                         # The file has been deleted or modified.
470                         self.bus.log("Restarting because %s changed." % filename)
471                         self.thread.cancel()
472                         self.bus.log("Stopped thread %r." %