Liam Staskawicz avatar Liam Staskawicz committed a76a9b9

reorg: remove config file requirement - use args instead, clean up and bit, and update for Go 1

Comments (0)

Files changed (4)

compilation.go

-package main
-
-import (
-	"os"
-	"path/filepath"
-	"time"
-)
-
-// return the most recent modified time for any file
-// in the given directory hierarchy
-func SourceModifiedMoreRecentlyThan(root string, modtime time.Time) bool {
-	mostRecentModTime := modtime
-	filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
-		thistime := info.ModTime()
-		if mostRecentModTime.After(modtime) {
-			return filepath.SkipDir // don't bother
-		}
-		if thistime.After(modtime) {
-			ext := filepath.Ext(info.Name())
-			// TODO - make extensions configurable
-			if ext == ".go" || ext == ".html" || ext == ".tmpl" {
-				mostRecentModTime = thistime
-			}
-		}
-		return nil
-	})
-	return mostRecentModTime.After(modtime)
-}

config.go

-package main
-
-import (
-	"encoding/json"
-	"io/ioutil"
-	"log"
-	"path"
-	"time"
-)
-
-// data type for config information
-type Config struct {
-	DevServerAddress string
-	AppAddress       string
-	App              string
-	CrashTimeout     time.Duration
-	StartupTime      time.Duration
-	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.Error())
-	}
-
-	// 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
-		time.Second / 2,  // StartupTime
-		"",               // LogFile
-	}
-
-	if err = json.Unmarshal(b, &conf); err != nil {
-		log.Fatal("ReadFile: ", err.Error())
-	}
-
-	if conf.App == "" {
-		log.Fatal("must include 'App' in your app.json")
-	}
-
-	return conf
-}
 import (
 	"bytes"
 	"flag"
+	"fmt"
 	"io"
 	"log"
 	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 	"time"
 )
 
-type Reloader struct {
-	reverseProxy *httputil.ReverseProxy
-	config       *Config
-	appRoot      string
-	appCmd       *exec.Cmd
-	appOutBuf    bytes.Buffer
+const (
+	processName = "devsrvr-slave.exe"
+)
+
+var addr = flag.String("addr", ":8000", "devsrvr address")
+var startup = flag.Duration("startup", time.Second*1, "time to wait for app to start before checking for errors")
+
+type Target struct {
+	cmd     *exec.Cmd
+	outbuf  bytes.Buffer
+	addr    string
+	approot string
+	modtime time.Time
+}
+
+var target Target
+var proxy *httputil.ReverseProxy
+
+func usage() {
+	fmt.Fprintf(os.Stderr, `usage: devsrvr [-addr :8000] approot
+
+devsrvr proxies all requests for the given address to the
+target web app named by approot.  It recompiles
+and restarts the target process as needed.
+
+The target web app must also have a -addr argument that
+specifies the address to listen on.
+
+Additional options:
+`)
+	flag.PrintDefaults()
 }
 
 func main() {
-	var err error
-	var approot string
-	flag.StringVar(&approot, "approot", "", "your app root's directory path")
+
+	flag.StringVar(&target.addr, "targetaddr", ":8888", "target web app address")
 	flag.Parse()
+	args := flag.Args()
 
-	// if not specified, assume the current working directory
-	if approot == "" {
-		if approot, err = os.Getwd(); err != nil {
-			log.Fatal("Getwd: ", err.Error())
-		}
+	if len(args) != 1 {
+		usage()
+		return
 	}
-	conf := LoadConfig(approot)
 
-	// fire up the app under development
-	proxy := httputil.NewSingleHostReverseProxy(
+	target.approot = args[0]
+
+	proxy = httputil.NewSingleHostReverseProxy(
 		&url.URL{
 			Scheme: "http",
-			Host:   conf.AppAddress,
+			Host:   target.addr,
 			Path:   "/",
 		},
 	)
-	reloader := &Reloader{
-		config:       conf,
-		reverseProxy: proxy,
-		appRoot:      approot,
-	}
 
-	// and start proxying requests to it
-	http.Handle("/", reloader)
-	log.Println("serving", conf.DevServerAddress)
-	if err := http.ListenAndServe(conf.DevServerAddress, nil); err != nil {
-		log.Fatal("ListenAndServe: ", err.Error())
+	announce()
+	log.Fatal(http.ListenAndServe(*addr, http.HandlerFunc(serve)))
+}
+
+func announce() {
+	abspath, err := filepath.Abs(target.approot)
+	if err != nil {
+		log.Fatal(err)
 	}
+	log.Println("devsrvr, serving", abspath, "on", *addr)
 }
 
 // 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 output, err := r.buildWithGoMake(r.appRoot); err != nil {
+func serve(w http.ResponseWriter, req *http.Request) {
+
+	if target.IsStale() {
+		if output, err := target.Build(); err != nil {
 			w.Write([]byte("so sorry, there appear to be some errors:\n\n"))
 			w.Write(output)
 			// TODO: a nicer template here
 			return
 		}
 	}
-	r.ensureTargetIsRunning(appPath)
-	if !r.checkForTargetErrors(w) {
-		r.reverseProxy.ServeHTTP(w, req)
+
+	target.EnsureRunning()
+	if !target.checkForErrors(w) {
+		proxy.ServeHTTP(w, req)
 	}
 
 	// TODO - figure out how to write errors back to browser
 	// r.appOutBuf.Reset()
 }
 
-// build a project
-// right now, we only support calling gomake
-// but we could potentially call out to the compiler or some other build tool
-// in the future if that seems useful
-func (r *Reloader) buildWithGoMake(projectDir string) ([]byte, error) {
-	gomake := exec.Command("gomake")
-	gomake.Dir = projectDir
-	return gomake.CombinedOutput()
+func (t *Target) ProcessName() string {
+	return filepath.Join(t.approot, processName)
 }
 
-// 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...
+// build the target via `go build`
+func (t *Target) Build() ([]byte, error) {
+	gobuild := exec.Command("go", "build", "-o", t.ProcessName())
+	gobuild.Dir = t.approot
+	return gobuild.CombinedOutput()
+}
+
+func (t *Target) EditsDetected() bool {
+	currentModTime := t.modtime
+	filepath.Walk(t.approot, func(path string, info os.FileInfo, err error) error {
+
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		// search for everything except directories,
+		// the process executable we've created,
+		// and any . files or dirs.
+		// this allows us to rebuild even when there are changes to files
+		// that `go build` wouldn't detect as deps, such as templates
+		name := info.Name()
+		if info.IsDir() {
+			if strings.HasPrefix(name, ".") {
+				return filepath.SkipDir
+			} else {
+				return nil
+			}
+		}
+
+		thistime := info.ModTime()
+		if thistime.After(currentModTime) &&
+			!strings.HasPrefix(name, ".") &&
+			name != processName {
+			currentModTime = thistime
+		}
+
+		return nil
+	})
+
+	if currentModTime.After(t.modtime) {
+		t.modtime = currentModTime
 		return true
 	}
+
+	return false
+}
+
+// determine whether the target app needs to be built.
+func (t *Target) IsStale() bool {
+
+	// if we've just started up, inherit the modtime from the binary
+	// produced by a previous run if possible
+	if t.modtime.IsZero() {
+		if fi, err := os.Stat(t.ProcessName()); err == nil {
+			t.modtime = fi.ModTime()
+		}
+	}
+
 	// check last modified time for both the binary and all its sources
-	if SourceModifiedMoreRecentlyThan(r.appRoot, appFileInfo.ModTime()) {
-		println("modifications detected - recompiling!")
-		if r.appCmd != nil {
-			r.appCmd.Process.Kill()
-			r.appCmd.Wait()
-			r.appCmd = nil
+	if t.EditsDetected() {
+		log.Println("modifications detected - recompiling!")
+		if t.cmd != nil {
+			t.cmd.Process.Kill()
+			t.cmd.Wait()
+			t.cmd = 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 {
+func (t *Target) EnsureRunning() {
+
+	// already running?
+	if t.cmd != nil {
 		return
 	}
-	r.appCmd = exec.Command(appPath)
-	r.appCmd.Dir = r.appRoot
-	r.appCmd.Stdout = io.MultiWriter(os.Stdout, &r.appOutBuf)
-	r.appCmd.Stderr = io.MultiWriter(os.Stderr, &r.appOutBuf)
-	if err := r.appCmd.Start(); err != nil {
-		log.Fatal("couldn't start app:", err.Error())
+
+	t.cmd = exec.Command(t.ProcessName(), "-addr", t.addr)
+	t.cmd.Dir = t.approot
+	t.cmd.Stdout = io.MultiWriter(os.Stdout, &t.outbuf)
+	t.cmd.Stderr = io.MultiWriter(os.Stderr, &t.outbuf)
+	if err := t.cmd.Start(); err != nil {
+		log.Fatal("couldn't start app:", err)
 	}
-	time.Sleep(r.config.StartupTime) // give it a moment to start up...surely there's a better way?
+
+	time.Sleep(*startup)
 }
 
-func (r *Reloader) checkForTargetErrors(errDest io.Writer) bool {
-	// check for error
-	waitmsg, err := r.appCmd.Process.Wait(os.WNOHANG)
-	// TODO - on darwin, even if WaitStatus is 256, Stopped() returns false...
-	// just check directly for now, but it's not nice
-	if err != nil || waitmsg.WaitStatus != 0 { //}.Stopped()
+func (t *Target) checkForErrors(errDest io.Writer) bool {
+
+	// still alive?
+	pid := t.cmd.Process.Pid
+	proc, err := os.FindProcess(pid)
+	if err != nil || proc.Pid != pid {
 		errDest.Write([]byte("so sorry, not running anymore:\n\n"))
 		// NOTE: write outbuf bytes, but leave them in the buffer so we still
 		// keep the same error message in case we see subsequent reloads before
 		// the error is resolved
-		errDest.Write(r.appOutBuf.Bytes())
+		errDest.Write(t.outbuf.Bytes())
 		return true
 	}
-	r.appOutBuf.Reset()
+
+	t.outbuf.Reset()
 	return false
 }

sampleconfig/app.json

-{
-    "App": "myapp",
-    "DevServerAddress": "127.0.0.1:8000",
-    "AppAddress": "127.0.0.1:9000",
-    "StartupTime": 10000000
-}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.