Source

AutoRebuild / autorebuild.py

Full commit
"""
AutoRebuild
===========

A plugin that will perform a rebuild when a file changes.
"""
import os
import glob
import subprocess
import shlex
import signal

from cherrypy.process.plugins import Autoreloader
from cherrypy.process.plugins import SimplePlugin


class AutoRebuild(Autoreloader):

    taskmap = {}

    def tasks(self):
        """
        See if any files have changed for a certain task.
        """
        tasks = []
        for regex, task in self.taskmap.iteritems():
            changed = []
            for filename in glob.iglob(regex):
                oldtime = self.mtimes.get(filename, 0)
                if oldtime is None:
                    continue

                try:
                    mtime = os.stat(filename).st_mtime
                except OSError:
                    mtime = None

                self.mtimes[filename] = mtime

                if mtime is None or mtime > oldtime:
                    changed.append(filename)

            if changed:
                tasks.append((task, changed))
        return tasks

    def run(self):
        for task, changed in self.tasks():
            self.bus.log('Executing %s. These changed: %s' % (task, changed))
            task()


class ServiceStarter(SimplePlugin):
    """
    A simple service starting plugin that allows you to configure
    other processes to start/stop along side your main cherrypy
    process.

    To use: ::

      # subscribe the plugin
      cherrypy.engine.some_proc = ServiceStarter(cherrypy.engine)
      cherrypy.engine.some_proc.subscribe()

      # configure it
      import suprocess

      cherrypy.config.update({
          'engine.some_proc.cmd': 'my_service -p 9000 -d $dbstore',
          'engine.some_proc.kw': {'stderr': subprocess.PIPE,
                                  'stdout': open('/var/log/some_proc.log'),
                                  'shell': True,}
      })

    You can set the 'args' and/or 'kw' attributes on the
    ServiceStarter object. They will be passed directly into the
    subprocess.Popen constructor.
    """

    def __init__(self, *args, **kw):
        super(ServiceStarter, self).__init__(*args, **kw)
        self.proc = None
        self.cmd = None
        self.args = []
        self.kw = {}
        self.pid = None

    def start(self):
        """
        Start our process and store our Popen object and pid.
        """
        if self.cmd and not self.pid:
            # We can use a list or a string for the command.
            if isinstance(self.cmd, basestring):
                self.cmd = shlex.split(self.cmd)

            # Start our process
            self.proc = subprocess.Popen(self.cmd,
                                         *self.args,
                                         **self.kw)
            # Store the pid separately. When cherrypy restarts it does
            # an exec which kills our Popen instance. We can use the
            # pid to stop the process safely.
            self.pid = self.proc.pid

    def kill(self):
        """
        Stop the process via the Popen object or pid.
        """
        if self.proc and self.proc.poll():
            # We trust the process will end happily with a
            # SIGTERM. Might need a hook to help things along if it
            # doesn't stop nicely.
            self.proc.terminate()
            self.proc.wait()
        elif self.pid:
            # We have been restarted which means our proc object is
            # gone. We can still use our pid and os.kill directly.
            os.kill(self.pid, signal.SIGTERM)

    def stop(self):
        self.kill()


if __name__ == '__main__':
    import cherrypy
    from subprocess import call

    class Root(object):
        def index(self):
            return 'Hello World!'
        index.exposed = True

    def build_css():
        call(['make', 'css'])

    cherrypy.engine.tail_proc = ServiceStarter(cherrypy.engine)
    cherrypy.engine.tail_proc.subscribe()

    cherrypy.engine.autorebuild = AutoRebuild(cherrypy.engine)
    cherrypy.engine.autorebuild.subscribe()

    cherrypy.config.update({
            'engine.tail_proc.cmd': 'python -m SimpleHTTPServer 9000',
            'engine.autorebuild.on': True,
            'engine.autorebuild.taskmap': {'*.less': build_css}})

    cherrypy.tree.mount(Root(), '/')
    cherrypy.engine.start()
    cherrypy.engine.block()