Commits

Liam Staskawicz committed f74b707

* initial working version

Comments (0)

Files changed (5)

+include $(GOROOT)/src/Make.inc
+
+TARG = devsrvr
+
+GOFILES = src/srvr.go \
+          src/config.go \
+          src/compilation.go
+
+include $(GOROOT)/src/Make.cmd

sampleconfig/app.json

+{
+    "App": "myapp",
+    "DevServerAddress": "127.0.0.1:9000",
+    "AppAddress": "127.0.0.1:9000",
+    "StartupTime": 10000000
+}

src/compilation.go

+package main
+
+import (
+    "os"
+    "path/filepath"
+)
+
+type modTimeVisitor struct {
+    mostRecentModTime int64
+}
+
+func (v *modTimeVisitor) VisitDir(path string, f *os.FileInfo) bool {
+    // println("visiting dir", path)
+    ext := filepath.Ext(f.Name)
+    if ((ext == ".go" || ext == ".html") && f.Mtime_ns > v.mostRecentModTime) {
+        v.mostRecentModTime = f.Mtime_ns
+    }
+    return true
+}
+
+func (v *modTimeVisitor) VisitFile(path string, f *os.FileInfo) {
+    // println("visiting file", path)
+    ext := filepath.Ext(f.Name)
+    if ((ext == ".go" || ext == ".html") && f.Mtime_ns > v.mostRecentModTime) {
+        v.mostRecentModTime = f.Mtime_ns
+    }
+}
+
+// return the most recent modified time for any file
+// in the given directory hierarchy
+func LastModifiedTime(root string) int64 {
+    visitor := &modTimeVisitor{ mostRecentModTime: 0 }
+    filepath.Walk(root, visitor, nil)
+    return visitor.mostRecentModTime
+}
+package main
+
+import (
+    "log"
+    "json"
+    "io/ioutil"
+    "path"
+)
+
+// data type for config information
+type Config struct {
+    DevServerAddress string
+    AppAddress string
+    App string
+    CrashTimeout int64
+    StartupTime int64
+    LogFile string
+}
+
+// load a json config file with details about the app we're running
+func LoadConfig(configpath string) *Config {    
+    b, err := ioutil.ReadFile(path.Join(configpath, "app.json"))
+    if err != nil {
+        log.Fatal(err.String())
+    }
+    
+    // config file defaults, overwritten by anything in the real file
+    conf := &Config {
+        "127.0.0.1:8080",   // DevServerAddress
+        "127.0.0.1:8081",   // AppAddress
+        "",                 // App
+        100000000,          // CrashTimeout
+        10000000,           // StartupTime
+        "",                 // LogFile
+    }
+    
+    if err = json.Unmarshal(b, &conf); err != nil {
+        log.Fatal("ReadFile: ", err.String())
+    }
+    
+    if conf.App == "" {
+        log.Fatal("must include 'App' in your app.json")
+    }
+    
+    return conf
+}
+package main
+
+import (
+    "os"
+    "http"
+    "url"
+    "log"
+    "flag"
+    "exec"
+    "path/filepath"
+    "time"
+    "bytes"
+)
+
+type Reloader struct {
+    reverseProxy *http.ReverseProxy
+    config *Config
+    appRoot string
+    appCmd *exec.Cmd
+    cmdOutputBuf bytes.Buffer
+}
+
+func main() {
+    var err os.Error
+    var approot string
+    flag.StringVar(&approot, "approot", "", "your app root's directory path")
+    flag.Parse()
+    
+    // if not specified, assume the current working directory
+    if approot == "" {
+        if approot, err = os.Getwd(); err != nil {
+            log.Fatal("Getwd: ", err.String())
+        }
+    }
+    conf := LoadConfig(approot)
+    
+    // fire up the app under development
+    proxy := http.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: conf.AppAddress, Path: "/"})
+    reloader := &Reloader{ config: conf, reverseProxy: proxy, appRoot: approot }
+    
+    // and start proxying requests to it
+    http.Handle("/", reloader)
+    println("serving", conf.DevServerAddress)
+    if err := http.ListenAndServe(conf.DevServerAddress, nil); err != nil {
+        log.Fatal("ListenAndServe: ", err.String())
+    }
+}
+
+// main entry point.
+// if any files have changed, recompile
+// and restart the app before proxying the request
+func (r *Reloader) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+    appPath := filepath.Join(r.appRoot, r.config.App)
+    if r.targetMustBeBuilt(appPath) {
+        if !r.buildWithGoMake(r.appRoot) {
+            w.Write([]byte("so sorry, there appear to be some errors:\n\n"))
+            r.cmdOutputBuf.WriteTo(w)
+            // TODO: a nicer template here
+            return
+        }
+    }
+    r.ensureTargetIsRunning(appPath)
+    r.reverseProxy.ServeHTTP(w, req)
+}
+
+// build a project
+// right now, we only support calling gomake
+// but we could potentially call out to the compiler
+// ourselves in the future if that seems useful
+func (r *Reloader) buildWithGoMake(projectDir string) bool {
+    gomake := exec.Command("gomake")
+    gomake.Dir = projectDir
+    gomake.Stdout = &r.cmdOutputBuf
+    gomake.Stderr = &r.cmdOutputBuf
+    if err := gomake.Run(); err != nil {
+        return false
+    }
+    return true
+}
+
+// determine whether the target app needs to be built.
+// if it doesn't exist, it must be built
+// if it already exists, check last modified timestamps on source files,
+// and if they're newer, it must be built
+func (r *Reloader) targetMustBeBuilt(appPath string) bool {
+    appFileInfo, err := os.Stat(appPath)
+    if err != nil {
+        // assume it doesn't exist - TODO: determine appropriate error type to check against...
+        return true
+    }
+    // check last modified time for both the binary and all its sources
+    modtime := LastModifiedTime(r.appRoot)
+    if modtime > appFileInfo.Mtime_ns {
+        println("modifications detected - recompiling!")
+        if r.appCmd != nil {
+            r.appCmd.Process.Kill()
+            r.appCmd.Wait()
+            r.appCmd = nil
+        }
+        
+        return true
+    }
+    return false
+}
+
+// start up the target app, and keep a pointer to it on the Reloader
+func (r *Reloader) ensureTargetIsRunning(appPath string) {
+    if r.appCmd != nil {
+        return
+    }
+    r.appCmd = exec.Command(appPath)
+    r.appCmd.Dir = r.appRoot
+    if err := r.appCmd.Start(); err != nil {
+        log.Fatal("couldn't start app:", err.String())
+    }
+    time.Sleep(r.config.StartupTime)    // give it a moment to start up
+}