1 """Site services for use with a Web Site Process Bus."""
2
3 import os
4 import re
5 import signal as _signal
6 import sys
7 import time
8 import threading
9
10 from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 _module__file__base = os.getcwd()
29
30
32 """Plugin base class which auto-subscribes methods for known channels."""
33
34 bus = None
35 """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
36
39
41 """Register this object as a (multi-channel) listener on the bus."""
42 for channel in self.bus.listeners:
43
44 method = getattr(self, channel, None)
45 if method is not None:
46 self.bus.subscribe(channel, method)
47
49 """Unregister this object as a listener on the bus."""
50 for channel in self.bus.listeners:
51
52 method = getattr(self, channel, None)
53 if method is not None:
54 self.bus.unsubscribe(channel, method)
55
56
57
59 """Register bus channels (and listeners) for system signals.
60
61 You can modify what signals your application listens for, and what it does
62 when it receives signals, by modifying :attr:`SignalHandler.handlers`,
63 a dict of {signal name: callback} pairs. The default set is::
64
65 handlers = {'SIGTERM': self.bus.exit,
66 'SIGHUP': self.handle_SIGHUP,
67 'SIGUSR1': self.bus.graceful,
68 }
69
70 The :func:`SignalHandler.handle_SIGHUP`` method calls
71 :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
72 if the process is daemonized, but
73 :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
74 if the process is attached to a TTY. This is because Unix window
75 managers tend to send SIGHUP to terminal windows when the user closes them.
76
77 Feel free to add signals which are not available on every platform. The
78 :class:`SignalHandler` will ignore errors raised from attempting to register
79 handlers for unknown signals.
80 """
81
82 handlers = {}
83 """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
84
85 signals = {}
86 """A map from signal numbers to names."""
87
88 for k, v in vars(_signal).items():
89 if k.startswith('SIG') and not k.startswith('SIG_'):
90 signals[v] = k
91 del k, v
92
109
111
112 self.bus.log('Keyboard Interrupt: shutting down bus')
113 self.bus.exit()
114
116 """Subscribe self.handlers to signals."""
117 for sig, func in self.handlers.items():
118 try:
119 self.set_handler(sig, func)
120 except ValueError:
121 pass
122
124 """Unsubscribe self.handlers from signals."""
125 for signum, handler in self._previous_handlers.items():
126 signame = self.signals[signum]
127
128 if handler is None:
129 self.bus.log("Restoring %s handler to SIG_DFL." % signame)
130 handler = _signal.SIG_DFL
131 else:
132 self.bus.log("Restoring %s handler %r." % (signame, handler))
133
134 try:
135 our_handler = _signal.signal(signum, handler)
136 if our_handler is None:
137 self.bus.log("Restored old %s handler %r, but our "
138 "handler was not registered." %
139 (signame, handler), level=30)
140 except ValueError:
141 self.bus.log("Unable to restore %s handler %r." %
142 (signame, handler), level=40, traceback=True)
143
145 """Subscribe a handler for the given signal (number or name).
146
147 If the optional 'listener' argument is provided, it will be
148 subscribed as a listener for the given signal's channel.
149
150 If the given signal name or number is not available on the current
151 platform, ValueError is raised.
152 """
153 if isinstance(signal, basestring):
154 signum = getattr(_signal, signal, None)
155 if signum is None:
156 raise ValueError("No such signal: %r" % signal)
157 signame = signal
158 else:
159 try:
160 signame = self.signals[signal]
161 except KeyError:
162 raise ValueError("No such signal: %r" % signal)
163 signum = signal
164
165 prev = _signal.signal(signum, self._handle_signal)
166 self._previous_handlers[signum] = prev
167
168 if listener is not None:
169 self.bus.log("Listening for %s." % signame)
170 self.bus.subscribe(signame, listener)
171
173 """Python signal handler (self.set_handler subscribes it for you)."""
174 signame = self.signals[signum]
175 self.bus.log("Caught signal %s." % signame)
176 self.bus.publish(signame)
177
179 """Restart if daemonized, else exit."""
180 if os.isatty(sys.stdin.fileno()):
181
182 self.bus.log("SIGHUP caught but not daemonized. Exiting.")
183 self.bus.exit()
184 else:
185 self.bus.log("SIGHUP caught while daemonized. Restarting.")
186 self.bus.restart()
187
188
189 try:
190 import pwd, grp
191 except ImportError:
192 pwd, grp = None, None
193
194
196 """Drop privileges. uid/gid arguments not available on Windows.
197
198 Special thanks to Gavin Baker: http://antonym.org/node/100.
199 """
200
201 - def __init__(self, bus, umask=None, uid=None, gid=None):
207
211 if val is not None:
212 if pwd is None:
213 self.bus.log("pwd module not available; ignoring uid.",
214 level=30)
215 val = None
216 elif isinstance(val, basestring):
217 val = pwd.getpwnam(val)[2]
218 self._uid = val
219 uid = property(_get_uid, _set_uid,
220 doc="The uid under which to run. Availability: Unix.")
221
225 if val is not None:
226 if grp is None:
227 self.bus.log("grp module not available; ignoring gid.",
228 level=30)
229 val = None
230 elif isinstance(val, basestring):
231 val = grp.getgrnam(val)[2]
232 self._gid = val
233 gid = property(_get_gid, _set_gid,
234 doc="The gid under which to run. Availability: Unix.")
235
239 if val is not None:
240 try:
241 os.umask
242 except AttributeError:
243 self.bus.log("umask function not available; ignoring umask.",
244 level=30)
245 val = None
246 self._umask = val
247 umask = property(_get_umask, _set_umask,
248 doc="""The default permission mode for newly created files and directories.
249
250 Usually expressed in octal format, for example, ``0644``.
251 Availability: Unix, Windows.
252 """)
253
255
256 def current_ids():
257 """Return the current (uid, gid) if available."""
258 name, group = None, None
259 if pwd:
260 name = pwd.getpwuid(os.getuid())[0]
261 if grp:
262 group = grp.getgrgid(os.getgid())[0]
263 return name, group
264
265 if self.finalized:
266 if not (self.uid is None and self.gid is None):
267 self.bus.log('Already running as uid: %r gid: %r' %
268 current_ids())
269 else:
270 if self.uid is None and self.gid is None:
271 if pwd or grp:
272 self.bus.log('uid/gid not set', level=30)
273 else:
274 self.bus.log('Started as uid: %r gid: %r' % current_ids())
275 if self.gid is not None:
276 os.setgid(self.gid)
277 os.setgroups([])
278 if self.uid is not None:
279 os.setuid(self.uid)
280 self.bus.log('Running as uid: %r gid: %r' % current_ids())
281
282
283 if self.finalized:
284 if self.umask is not None:
285 self.bus.log('umask already set to: %03o' % self.umask)
286 else:
287 if self.umask is None:
288 self.bus.log('umask not set', level=30)
289 else:
290 old_umask = os.umask(self.umask)
291 self.bus.log('umask old: %03o, new: %03o' %
292 (old_umask, self.umask))
293
294 self.finalized = True
295
296
297
298 start.priority = 77
299
300
302 """Daemonize the running script.
303
304 Use this with a Web Site Process Bus via::
305
306 Daemonizer(bus).subscribe()
307
308 When this component finishes, the process is completely decoupled from
309 the parent environment. Please note that when this component is used,
310 the return code from the parent process will still be 0 if a startup
311 error occurs in the forked children. Errors in the initial daemonizing
312 process still return proper exit codes. Therefore, if you use this
313 plugin to daemonize, don't use the return code as an accurate indicator
314 of whether the process fully started. In fact, that return code only
315 indicates if the process succesfully finished the first fork.
316 """
317
318 - def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
319 stderr='/dev/null'):
320 SimplePlugin.__init__(self, bus)
321 self.stdin = stdin
322 self.stdout = stdout
323 self.stderr = stderr
324 self.finalized = False
325
327 if self.finalized:
328 self.bus.log('Already deamonized.')
329
330
331
332
333
334
335 if threading.activeCount() != 1:
336 self.bus.log('There are %r active threads. '
337 'Daemonizing now may cause strange failures.' %
338 threading.enumerate(), level=30)
339
340
341
342
343
344
345 sys.stdout.flush()
346 sys.stderr.flush()
347
348
349 try:
350 pid = os.fork()
351 if pid == 0:
352
353 pass
354 else:
355
356 self.bus.log('Forking once.')
357 os._exit(0)
358 except OSError:
359
360 exc = sys.exc_info()[1]
361 sys.exit("%s: fork #1 failed: (%d) %s\n"
362 % (sys.argv[0], exc.errno, exc.strerror))
363
364 os.setsid()
365
366
367 try:
368 pid = os.fork()
369 if pid > 0:
370 self.bus.log('Forking twice.')
371 os._exit(0)
372 except OSError:
373 exc = sys.exc_info()[1]
374 sys.exit("%s: fork #2 failed: (%d) %s\n"
375 % (sys.argv[0], exc.errno, exc.strerror))
376
377 os.chdir("/")
378 os.umask(0)
379
380 si = open(self.stdin, "r")
381 so = open(self.stdout, "a+")
382 se = open(self.stderr, "a+")
383
384
385
386
387 os.dup2(si.fileno(), sys.stdin.fileno())
388 os.dup2(so.fileno(), sys.stdout.fileno())
389 os.dup2(se.fileno(), sys.stderr.fileno())
390
391 self.bus.log('Daemonized to PID: %s' % os.getpid())
392 self.finalized = True
393 start.priority = 65
394
395
397 """Maintain a PID file via a WSPBus."""
398
403
405 pid = os.getpid()
406 if self.finalized:
407 self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
408 else:
409 open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8'))
410 self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
411 self.finalized = True
412 start.priority = 70
413
415 try:
416 os.remove(self.pidfile)
417 self.bus.log('PID file removed: %r.' % self.pidfile)
418 except (KeyboardInterrupt, SystemExit):
419 raise
420 except:
421 pass
422
423
425 """A responsive subclass of threading._Timer whose run() method repeats.
426
427 Use this timer only when you really need a very interruptible timer;
428 this checks its 'finished' condition up to 20 times a second, which can
429 results in pretty high CPU usage
430 """
431
433 while True:
434 self.finished.wait(self.interval)
435 if self.finished.isSet():
436 return
437 try:
438 self.function(*self.args, **self.kwargs)
439 except Exception:
440 self.bus.log("Error in perpetual timer thread function %r." %
441 self.function, level=40, traceback=True)
442
443 raise
444
445
447 """A subclass of threading.Thread whose run() method repeats.
448
449 Use this class for most repeating tasks. It uses time.sleep() to wait
450 for each interval, which isn't very responsive; that is, even if you call
451 self.cancel(), you'll have to wait until the sleep() call finishes before
452 the thread stops. To compensate, it defaults to being daemonic, which means
453 it won't delay stopping the whole process.
454 """
455
456 - def __init__(self, interval, function, args=[], kwargs={}, bus=None):
457 threading.Thread.__init__(self)
458 self.interval = interval
459 self.function = function
460 self.args = args
461 self.kwargs = kwargs
462 self.running = False
463 self.bus = bus
464
467
469 self.running = True
470 while self.running:
471 time.sleep(self.interval)
472 if not self.running:
473 return
474 try:
475 self.function(*self.args, **self.kwargs)
476 except Exception:
477 if self.bus:
478 self.bus.log("Error in background task thread function %r."
479 % self.function, level=40, traceback=True)
480
481 raise
482
485
486
488 """WSPBus listener to periodically run a callback in its own thread."""
489
490 callback = None
491 """The function to call at intervals."""
492
493 frequency = 60
494 """The time in seconds between callback runs."""
495
496 thread = None
497 """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
498
499 - def __init__(self, bus, callback, frequency=60, name=None):
505
518 start.priority = 70
519
533
535 """Stop the callback's background task thread and restart it."""
536 self.stop()
537 self.start()
538
539
541 """Monitor which re-executes the process when files change.
542
543 This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
544 if any of the files it monitors change (or is deleted). By default, the
545 autoreloader monitors all imported modules; you can add to the
546 set by adding to ``autoreload.files``::
547
548 cherrypy.engine.autoreload.files.add(myFile)
549
550 If there are imported files you do *not* wish to monitor, you can adjust the
551 ``match`` attribute, a regular expression. For example, to stop monitoring
552 cherrypy itself::
553
554 cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
555
556 Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
557 the autoreload plugin takes a ``frequency`` argument. The default is
558 1 second; that is, the autoreloader will examine files once each second.
559 """
560
561 files = None
562 """The set of files to poll for modifications."""
563
564 frequency = 1
565 """The interval in seconds at which to poll for modified files."""
566
567 match = '.*'
568 """A regular expression by which to match filenames."""
569
570 - def __init__(self, bus, frequency=1, match='.*'):
575
577 """Start our own background task thread for self.run."""
578 if self.thread is None:
579 self.mtimes = {}
580 Monitor.start(self)
581 start.priority = 70
582
584 """Return a Set of sys.modules filenames to monitor."""
585 files = set()
586 for k, m in sys.modules.items():
587 if re.match(self.match, k):
588 if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'):
589 f = m.__loader__.archive
590 else:
591 f = getattr(m, '__file__', None)
592 if f is not None and not os.path.isabs(f):
593
594 f = os.path.normpath(os.path.join(_module__file__base, f))
595 files.add(f)
596 return files
597
627
628
630 """Manager for HTTP request threads.
631
632 If you have control over thread creation and destruction, publish to
633 the 'acquire_thread' and 'release_thread' channels (for each thread).
634 This will register/unregister the current thread and publish to
635 'start_thread' and 'stop_thread' listeners in the bus as needed.
636
637 If threads are created and destroyed by code you do not control
638 (e.g., Apache), then, at the beginning of every HTTP request,
639 publish to 'acquire_thread' only. You should not publish to
640 'release_thread' in this case, since you do not know whether
641 the thread will be re-used or not. The bus will call
642 'stop_thread' listeners for you when it stops.
643 """
644
645 threads = None
646 """A map of {thread ident: index number} pairs."""
647
655
657 """Run 'start_thread' listeners for the current thread.
658
659 If the current thread has already been seen, any 'start_thread'
660 listeners will not be run again.
661 """
662 thread_ident = get_thread_ident()
663 if thread_ident not in self.threads:
664
665
666 i = len(self.threads) + 1
667 self.threads[thread_ident] = i
668 self.bus.publish('start_thread', i)
669
671 """Release the current thread and run 'stop_thread' listeners."""
672 thread_ident = get_thread_ident()
673 i = self.threads.pop(thread_ident, None)
674 if i is not None:
675 self.bus.publish('stop_thread', i)
676
682 graceful = stop
683