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