Leo's Technical Blog

Generic On-The-Fly Deployment

Introduction

user

Leo Soto


python, tips, java

Generic On-The-Fly Deployment

Posted by Leo Soto on .
Featured

python, tips, java

Generic On-The-Fly Deployment

Posted by Leo Soto on .

Once spoiled by dynamic-languages web frameworks, it is hard to go back to one where a manual build cycle is needed before seeing the changes made to the source code on the running application. It must be instantaneous.

As you can infer from my latest posts, I am now working with Flex. And Java Servlets, using Resin as the server. That environment is not even close to be as developer-friendly as Django, or Rails or many others modern web frameworks. Perhaps I am being a bit unfair, because Resin tries to be developer-friendly, not only reloading the context when something changes, but also providing a special directory where you can put your java sources and it even compiles them. But the project layout is not configured the way Resin likes it, so it did not work.

Other developers use a IDE plugin to deploy the compiled classes automatically. But, is really an IDE needed to simply (re)build a project after some file(s) changes? I do not think so. After all, it is just matter of monitoring the file system and firing the build process if a change is detected. So I came with a simple, handy utility, which monitor a specified directory and run an arbitrary command when a change is detected.

Integrating it with our build script is trivial:

 > cd \eclipse3.3\eclipse\workspace\MyProject
 > python dirwatch.py -c "ant deploy-development"

Moreover, Eclipse integration is easy too: Run-> External Tools -> Open External Tools Dialog -> Program -> New:

Location: c:\python25\python.exe
Working Directory: ${workspace_loc:/MyProject}
Arguments: -u "c:\path\to\dirwatch.py" -c "ant deploy-development"

The whole point of the exercise? Decouple the convenience of automatic deploymeny from a particular IDE, where it does not belong. Just like building.

So here is the program (currently only works on Windows, but should be easy to port to Unix-like systems, using inotify or FAM):

#!/usr/bin/env python  
"""
dirwatch:

Monitor a directory for changes. Executes a command after a change is detected.  
"""

import os  
import time  
import win32file  
import win32event  
import win32con  
from optparse import OptionParser

VERSION = "1.0"

def dirwatch(path_to_watch, seconds_to_wait, commands):  
    print time.asctime(), "Watching %s" % path_to_watch
    import sys
    change_handle = win32file.FindFirstChangeNotification (
        path_to_watch,
        1, # => recursive
        win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
        win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
        win32con.FILE_NOTIFY_CHANGE_SIZE |
        win32con.FILE_NOTIFY_CHANGE_LAST_WRITE
    )
    # Loop forever, listing any file changes.
    try:
        last_change_time = None
        while True:
            result = win32event.WaitForSingleObject(change_handle, 1000)
            if result  win32con.WAIT_OBJECT_0:
                if last_change_time:
                    if time.time() - last_change_time > seconds_to_wait:
                        msg = "More change(s) detected, " \
                              "waiting %d more second(s)"
                    else:
                        msg = None
                else:
                    msg = "Change detected, waiting %d second(s)"
                if msg:
                    print time.asctime(), msg  % seconds_to_wait
                last_change_time = time.time()
                win32file.FindNextChangeNotification(change_handle)
            if last_change_time:
                if time.time() - last_change_time > seconds_to_wait:
                    for command in commands:
                        print time.asctime(), "Executing '%s'" % command
                        os.system(command)
                    last_change_time = None
    finally:
        win32file.FindCloseChangeNotification(change_handle)

def parse_options(args):  
    parser = OptionParser(usage="%prog [-c command[;command...]] [directory]",
                          version="%prog " + VERSION)

    parser.add_option("-w", "--wait", dest="seconds_to_wait", metavar="SECONDS",
                      default="1",
                      help="seconds to wait after last change before the "
                           "of the command(s).")
    parser.add_option("-c", "--command", dest="command",
                      default="echo change detected",
                      help="command to execute after a change is detected")
    return parser.parse_args(args)
def main(args):  
    options, args = parse_options(args)
    try: path_to_watch = args[1] or "."
    except: path_to_watch = "."
    dirwatch(path_to_watch, int(options.seconds_to_wait),
             options.command.split(';'))

if __name__  "__main__":  
    import sys
    main(sys.argv)