spoon-1.0.6/0000775000175000017500000000000013066652017013734 5ustar chirilachirila00000000000000spoon-1.0.6/spoon/0000775000175000017500000000000013066652017015072 5ustar chirilachirila00000000000000spoon-1.0.6/spoon/server.py0000664000175000017500000001511013066646615016757 0ustar chirilachirila00000000000000"""Exposes UDP/TCP servers that can handle requests and can be stopped gracefully or reloaded. """ from __future__ import absolute_import import os import errno import socket import signal import logging import threading try: import socketserver except ImportError: import SocketServer as socketserver def _eintr_retry(func, *args): """restart a system call interrupted by EINTR""" while True: try: return func(*args) except OSError as e: if e.args[0] != errno.EINTR: raise class _Gulp(object): """Handle a single request.""" def handle(self): """Get the command from the client and pass it to the correct handler. """ raise NotImplementedError() class _StreamRequestHandler(socketserver.StreamRequestHandler, object): """Converted to newstyle class.""" class _DatagramRequestHandler(socketserver.DatagramRequestHandler, object): """Converted to newstyle class.""" class TCPGulp(_Gulp, _StreamRequestHandler): """Handle a single TCP request.""" class UDPGulp(_Gulp, _DatagramRequestHandler): """Handle a single UDP request.""" class _TCPServer(socketserver.TCPServer, object): """Converted to newstyle class.""" class _UDPServer(socketserver.UDPServer, object): """Converted to newstyle class.""" class _SpoonMixIn(object): """A server that consumes Gulps in a single thread and single process. """ server_logger = "spoon-server" handler_klass = TCPGulp # Custom signal handling signal_reload = signal.SIGUSR1 signal_shutdown = signal.SIGTERM # Socket options. ipv6_only = False allow_reuse_address = True # Command line defaults command_line_defaults = { "port": 5000, "interface": "::0", "pid_file": None, "log_file": None, "sentry_dsn": None, "spork": None, } def __init__(self, address): self.log = logging.getLogger(self.server_logger) self.socket = None if ":" in address[0]: self.address_family = socket.AF_INET6 else: self.address_family = socket.AF_INET self.log.debug("Listening on %s", address) super(_SpoonMixIn, self).__init__(address, self.handler_klass, bind_and_activate=False) self.load_config() self._setup_socket() # Finally, set signals if self.signal_reload is not None: signal.signal(self.signal_reload, self.reload_handler) if self.signal_shutdown is not None: signal.signal(self.signal_shutdown, self.shutdown_handler) def _setup_socket(self): self.socket = socket.socket(self.address_family, self.socket_type) if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if not self.ipv6_only: try: self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) except (AttributeError, socket.error) as e: self.log.debug("Unable to set IPV6_V6ONLY to false %s", e) self.server_bind() self.server_activate() def serve_forever(self, poll_interval=0.1): super(_SpoonMixIn, self).serve_forever(poll_interval=poll_interval) def load_config(self): """Reads the configuration files, this is called when the reload handler is received. Can be reimplemented. """ def shutdown_handler(self, *args, **kwargs): """Handler for the SIGTERM signal. This should be used to kill the daemon and ensure proper clean-up. """ self.log.info("SIGTERM received. Shutting down.") t = threading.Thread(target=self.shutdown) t.start() def reload_handler(self, *args, **kwargs): """Handler for the SIGUSR1 signal. This should be used to reload the configuration files. """ self.log.info("SIGUSR1 received. Reloading configuration.") t = threading.Thread(target=self.load_config) t.start() def handle_error(self, request, client_address): self.log.error("Error while processing request from: %s", client_address, exc_info=True) class _SporkMixIn(_SpoonMixIn): """The same as Spoon, but allows consuming Gulps with more than one spoon by pre-forking when starting the server. The parent Spoon process will then wait for all his child process to complete. """ prefork = 4 def __init__(self, address): """The same as Server.__init__ but requires a list of databases instead of a single database connection. """ self.pids = None _SpoonMixIn.__init__(self, address) def serve_forever(self, poll_interval=0.1): """Fork the current process and wait for all children to finish.""" if self.prefork is None or self.prefork <= 1: return super(_SporkMixIn, self).serve_forever( poll_interval=poll_interval) pids = [] for dummy in range(self.prefork): pid = os.fork() if not pid: super(_SporkMixIn, self).serve_forever( poll_interval=poll_interval) os._exit(0) else: self.log.info("Forked worker %s", pid) pids.append(pid) self.pids = pids for pid in self.pids: _eintr_retry(os.waitpid, pid, 0) def shutdown(self): """If this is the parent process send the TERM signal to all children, else call the super method. """ for pid in self.pids or (): os.kill(pid, self.signal_shutdown) if self.pids is None: super(_SporkMixIn, self).shutdown() def load_config(self): """If this is the parent process send the USR1 signal to all children, else call the super method. """ for pid in self.pids or (): os.kill(pid, self.signal_reload) if self.pids is None: super(_SporkMixIn, self).load_config() class TCPSpoon(_SpoonMixIn, _TCPServer): """A TCP Socket server that handles everything in a single process. """ class TCPSpork(_SporkMixIn, _TCPServer): """A TCP Socket server that pre-forks a number of child processes. """ class UDPSpoon(_SpoonMixIn, _UDPServer): """A UDP Socket server that handles everything in a single process. """ class UDPSpork(_SporkMixIn, _UDPServer): """A UDP Socket server that pre-forks a number of child processes. """ spoon-1.0.6/spoon/__init__.py0000664000175000017500000000002613066650375017206 0ustar chirilachirila00000000000000__version__ = "1.0.6" spoon-1.0.6/spoon/daemon.py0000664000175000017500000001775513066651646016735 0ustar chirilachirila00000000000000"""Exposes tools for daemonizing.""" from __future__ import print_function import os import sys import signal import logging import argparse import importlib import logging.handlers try: import raven import raven.transport from raven.handlers.logging import SentryHandler except ImportError: _has_raven = False else: _has_raven = True def detach(stdout="/dev/null", stderr=None, stdin="/dev/null", pidfile=None, logger=None): """This forks the current process into a daemon. The stdin, stdout, and stderr arguments are file names that will be opened and be used to replace the standard file descriptors in sys.stdin, sys.stdout, and sys.stderr. These arguments are optional and default to /dev/null. Note that stderr is opened unbuffered, so if it shares a file with stdout then interleaved output may not appear in the order that you expect.""" if logger is None: logger = logging # Do first fork. try: pid = os.fork() if pid > 0: # Exit first parent. sys.exit(0) except OSError as err: logger.critical("Fork #1 failed: (%d) %s", err.errno, err.strerror) sys.exit(1) # Decouple from parent environment. os.chdir("/") os.umask(0) os.setsid() # Do second fork. try: pid = os.fork() if pid > 0: # Exit second parent. sys.exit(0) except OSError as err: logger.critical("Fork #2 failed: (%d) %s", err.errno, err.strerror) sys.exit(1) # Open file descriptors and print start message. if not stderr: stderr = stdout stdi = open(stdin, "r") stdo = open(stdout, "a+") stde = open(stderr, "ab+", 0) pid = str(os.getpid()) if pidfile: with open(pidfile, "w+") as pidf: pidf.write("%s\n" % pid) # Redirect standard file descriptors. os.dup2(stdi.fileno(), sys.stdin.fileno()) os.dup2(stdo.fileno(), sys.stdout.fileno()) os.dup2(stde.fileno(), sys.stderr.fileno()) def run_daemon(server, pidfile, daemonize=True): """Run the server as a daemon :param server: cutlery (a Spoon or Spork) :param pidfile: the file to keep the parent PID :param daemonize: if True fork the processes into a daemon. :return: """ logger = logging.getLogger(server.server_logger) if daemonize: detach(pidfile=pidfile, logger=logger) elif pidfile: with open(pidfile, "w+") as pidf: pidf.write("%s\n" % os.getpid()) try: server.serve_forever() finally: try: os.remove(pidfile) except OSError: pass def send_action(action, pidfile, logger=None): """Send a signal to an existing running daemon.""" if logger is None: logger = logging if not os.path.exists(pidfile): logger.critical("No pid file available: %s", pidfile) return with open(pidfile) as pidf: pid = int(pidf.read()) if action == "reload": os.kill(pid, signal.SIGUSR1) elif action == "stop": os.kill(pid, signal.SIGTERM) def _setup_logging(logger, options): formatter = logging.Formatter('%(asctime)s %(process)s %(levelname)s ' '%(message)s') logger.setLevel(logging.DEBUG) if options["log_file"]: filename = options["log_file"] file_handler = logging.handlers.WatchedFileHandler(filename) file_handler.setFormatter(formatter) if options["debug"]: file_handler.setLevel(logging.DEBUG) else: file_handler.setLevel(logging.INFO) logger.addHandler(file_handler) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.CRITICAL) stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) if options["sentry_dsn"] and _has_raven: client = raven.Client(options["sentry_dsn"], enable_breadcrumbs=False, transport=raven.transport.HTTPTransport) # Add Sentry handle to application logger. sentry_handler = SentryHandler(client) sentry_handler.setLevel(logging.WARNING) logger.addHandler(sentry_handler) null_loggers = [ logging.getLogger("sentry.errors"), logging.getLogger("sentry.errors.uncaught") ] for null_logger in null_loggers: null_logger.handlers = [logging.NullHandler()] if options["debug"]: stream_handler.setLevel(logging.DEBUG) elif options["info"]: stream_handler.setLevel(logging.INFO) def _is_process_running(logger, options): pid_file = options["pid_file"] if not os.path.exists(pid_file): logger.debug("No other process running.") return False with open(pid_file, "r") as pidf: pid = pidf.read().strip() if not os.path.exists("/proc/%s" % pid): logger.info("Stale pid file, removing.") os.remove(pid_file) return False logger.critical("Process still running, cannot start: %s", pid) return True def _main(): """Parse command line arguments and process action related to daemons. """ parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("klass", help="The spoon class.") parser.add_argument("command", choices=["start", "stop", "reload", "restart"], help="The command to be issued.") parser.add_argument("-s", "--spork", default=None, help="Set the number of sporked workers") parser.add_argument("-p", "--pid-file", default=None, help="Set the PID file for the daemon.") parser.add_argument("-P", "--port", default=None, type=int, help="Port to listen on") parser.add_argument("-I", "--interface", default=None, help="Interface to listen on") parser.add_argument("-n", "--nice", dest="nice", type=int, help="'nice' level", default=10) parser.add_argument("-d", "--debug", action="store_true", default=None, dest="debug", help="enable debugging output") parser.add_argument("-i", "--info", action="store_true", default=None, dest="info", help="enable informational output") parser.add_argument("-L", "--log-file", default=None, help="Set the log file.") parser.add_argument("-S", "--sentry-dsn", default=None, help="Set the sentry DSN for logging.") parser.add_argument("-D", "--no-daemon", default=False, action="store_true", help="Don't daemonize process") cmd_options = parser.parse_args() os.nice(cmd_options.nice) module, klass = cmd_options.klass.rsplit(".", 1) klass = getattr(importlib.import_module(module), klass) logger = logging.getLogger(klass.server_logger) options = dict(klass.command_line_defaults) for key in options.keys(): # Override with cmd line options. value = getattr(cmd_options, key, None) if value is not None: options[key] = value for key, value in cmd_options.__dict__.items(): if key not in options: options[key] = value _setup_logging(logger, options) if cmd_options.command in ("stop", "restart"): send_action("stop", options["pid_file"], logger) if cmd_options.command == "reload": send_action("reload", options["pid_file"], logger) if cmd_options.command in ("start", "restart"): if _is_process_running(logger, options): return logger.info("Starting %s (%s)", cmd_options.klass, options["spork"]) klass.prefork = int(options["spork"]) server = klass((options["interface"], options["port"])) run_daemon(server, options["pid_file"], not cmd_options.no_daemon) if __name__ == "__main__": _main() spoon-1.0.6/PKG-INFO0000664000175000017500000000207013066652017015030 0ustar chirilachirila00000000000000Metadata-Version: 1.1 Name: spoon Version: 1.0.6 Summary: Simple to use pre-forking server interface. Home-page: UNKNOWN Author: SpamExperts Author-email: UNKNOWN License: GPL Description: UNKNOWN Keywords: server Platform: POSIX Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy spoon-1.0.6/setup.py0000664000175000017500000000236613066646615015464 0ustar chirilachirila00000000000000#! /usr/bin/env python from __future__ import absolute_import import spoon import distutils.core REQUIRES = [] DESCRIPTION = """Simple to use pre-forking server interface.""" CLASSIFIERS = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] distutils.core.setup( name='spoon', description=DESCRIPTION, author="SpamExperts", version=spoon.__version__, license='GPL', platforms='POSIX', keywords='server', classifiers=CLASSIFIERS, # scripts=[], requires=REQUIRES, packages=[ 'spoon', ], )