| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 269 |
|
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
|
|---|
| 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 |
|
|---|
| 279 |
|
|---|
| 280 |
|
|---|
| 281 |
|
|---|
| 282 |
|
|---|
| 283 |
sys.stdout.flush() |
|---|
| 284 |
sys.stderr.flush() |
|---|
| 285 |
|
|---|
| 286 |
|
|---|
| 287 |
try: |
|---|
| 288 |
pid = os.fork() |
|---|
| 289 |
if pid == 0: |
|---|
| 290 |
|
|---|
| 291 |
pass |
|---|
| 292 |
else: |
|---|
| 293 |
|
|---|
| 294 |
self.bus.log('Forking once.') |
|---|
| 295 |
os._exit(0) |
|---|
| 296 |
except OSError, exc: |
|---|
| 297 |
|
|---|
| 298 |
sys.exit("%s: fork #1 failed: (%d) %s\n" |
|---|
| 299 |
% (sys.argv[0], exc.errno, exc.strerror)) |
|---|
| 300 |
|
|---|
| 301 |
os.setsid() |
|---|
| 302 |
|
|---|
| 303 |
|
|---|
| 304 |
try: |
|---|
| 305 |
pid = os.fork() |
|---|
| 306 |
if pid > 0: |
|---|
| 307 |
self.bus.log('Forking twice.') |
|---|
| 308 |
os._exit(0) |
|---|
| 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 |
|
|---|
| 321 |
|
|---|
| 322 |
|
|---|
| 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 |
|
|---|
| 456 |
continue |
|---|
| 457 |
|
|---|
| 458 |
try: |
|---|
| 459 |
mtime = os.stat(filename).st_mtime |
|---|
| 460 |
except OSError: |
|---|
| 461 |
|
|---|
| 462 |
mtime = None |
|---|
| 463 |
|
|---|
| 464 |
if filename not in self.mtimes: |
|---|
| 465 |
|
|---|
| 466 |
self.mtimes[filename] = mtime |
|---|
| 467 |
else: |
|---|
| 468 |
if mtime is None or mtime > oldtime: |
|---|
| 469 |
|
|---|
| 470 |
self.bus.log("Restarting because %s changed." % filename) |
|---|
| 471 |
self.thread.cancel() |
|---|
| 472 |
self.bus.log("Stopped thread %r." % |
|---|