Friday, July 23, 2010

Watch Directory For Changes in Groovy

One must-have feature in any modern web stack is the ability to automatically restart/refresh the development server when you edit the source code. It is critical for developer workflow that the feedback loop be as tight as possible. Here source code refers to actual class definitions, templates (JSPs, GSPs, haml files, etc...), static content (javascript, css, images), AND configuration (I'm looking at you, struts.xml).

All web frameworks which do not support automatic updating of all content during development shall hereafter be referred to as "Legacy Web Frameworks". Is your shop using them?

Justin Voss and I have been hard at work on a web micro-framework called Ratpack (inspired by Ruby's Sinatra). Lest it become a Legacy Web Framework right out of the gate, we've implemented basic auto-reloading right away. Feel free to grab this for other uses, as it works independently.

groovy runapp.groovy app/myapp.groovy app

The script app/myapp.groovy will be killed and re-run when any content in the app directory changes.

Since Ratpack uses Jetty, there are probably better solutions. Feedback is welcome.

public abstract class DirWatcher extends TimerTask {
def path
def dir = [:]
// Exclude temp files created by vim, emacs, etc...
FileFilter fileFilter = {file -> !(file.name =~ /\.swp$|\~$|^\./)} as FileFilter
public DirWatcher(String path) {
this.path = path;
def files = new File(path).listFiles(fileFilter);
// transfer to the hashmap be used a reference and keep the lastModfied value
for(File file : files) {
dir.put(file, file.lastModified());
}
}
public final void run() {
def checkedFiles = new HashSet();
def files = new File(path).listFiles(fileFilter);
// scan the files and check for modification/addition
for (File file : files) {
Long current = dir.get(file)
checkedFiles.add(file)
if (current == null) {
// new file
dir.put(file, new Long(file.lastModified()))
onChange(file, "add")
}
else if (current.longValue() != file.lastModified()){
// modified file
dir.put(file, new Long(file.lastModified()))
onChange(file, "modify")
}
}
// now check for deleted files
def deletedFiles = dir.clone().keySet() - checkedFiles
deletedFiles.each {
dir.remove(it)
onChange(it, "delete")
}
}
protected abstract void onChange(File file, String action);
}
class AppRunner extends DirWatcher {
def proc = null
def script
AppRunner(String script, String path) {
super(path)
this.script = script
}
def manageApp() {
runApp()
Timer timer = new Timer()
timer.schedule(this, new Date(), 1000)
}
def runApp() {
proc = "groovy ${script}".execute()
proc.consumeProcessOutput(System.out, System.err)
}
def killApp() {
proc.waitForOrKill(1000)
}
void onChange(File file, String action) {
println ("File "+ file.name +" action: " + action )
if (proc) {
println "KILLING"
killApp()
println "RELOADING"
} else {
println "STARTING"
}
runApp()
}
}
if (args.length == 2) {
new AppRunner(args[0], args[1]).manageApp()
} else {
println "Usage:"
println "groovy runner.groovy [script] [dir to watch]"
}
view raw runapp.groovy hosted with ❤ by GitHub


The NIO.2 Filesystem in JDK7 will make this sort of thing much easier.

1 comment:

Michael Finney said...

That is so awesome. Fast feedback like that lets the mind flow freely and the great web solutions just keep on coming!