Skip to content

Commit

Permalink
Watch/reload routes file for changes, and validate it
Browse files Browse the repository at this point in the history
Also, allow the watcher to watch individual files.
Add an interface for callers to filter what gets watched.
Force refresh everything on the first Notify()
  • Loading branch information
robfig committed Aug 12, 2012
1 parent 007927b commit 6e56e8f
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 50 deletions.
14 changes: 11 additions & 3 deletions harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
)

var (
Expand Down Expand Up @@ -77,6 +79,14 @@ func (h *Harness) Refresh() *rev.Error {
return rebuild("", h.port)
}

func (h *Harness) WatchDir(info os.FileInfo) bool {
return !rev.ContainsString(doNotWatch, info.Name())
}

func (h *Harness) WatchFile(filename string) bool {
return strings.HasSuffix(filename, ".go")
}

func (h *Harness) Run() {
// If the harness exits, be sure to kill the app server.
defer func() {
Expand All @@ -86,10 +96,8 @@ func (h *Harness) Run() {
}
}()

h.Refresh()

watcher = rev.NewWatcher()
watcher.Listen(h, []string{rev.AppPath}, doNotWatch)
watcher.Listen(h, rev.AppPath)

appAddr := getAppAddress()
appPort := getAppPort()
Expand Down
124 changes: 93 additions & 31 deletions router.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rev

import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
Expand Down Expand Up @@ -34,9 +35,10 @@ type arg struct {
constraint *regexp.Regexp
}

// TODO: Use exp/regexp and named groups e.g. (?P<name>a)
var nakedPathParamRegex *regexp.Regexp = regexp.MustCompile(`\{([a-zA-Z_][a-zA-Z_0-9]*)\}`)
var argsPattern *regexp.Regexp = regexp.MustCompile(`\{<(?P<pattern>[^>]+)>(?P<var>[a-zA-Z_0-9]+)\}`)
var (
nakedPathParamRegex = regexp.MustCompile(`\{([a-zA-Z_][a-zA-Z_0-9]*)\}`)
argsPattern = regexp.MustCompile(`\{<(?P<pattern>[^>]+)>(?P<var>[a-zA-Z_0-9]+)\}`)
)

// Prepares the route to be used in matching.
func NewRoute(method, path, action string) (r *Route) {
Expand Down Expand Up @@ -162,6 +164,7 @@ func (r *Route) Match(method string, reqPath string) *RouteMatch {

type Router struct {
Routes []*Route
path string
}

func (router *Router) Route(req *http.Request) *RouteMatch {
Expand All @@ -173,42 +176,27 @@ func (router *Router) Route(req *http.Request) *RouteMatch {
return nil
}

// Groups:
// 1: method
// 4: path
// 5: action
var routePattern *regexp.Regexp = regexp.MustCompile(
"(?i)^(GET|POST|PUT|DELETE|OPTIONS|HEAD|WS|\\*)" +
"[(]?([^)]*)(\\))? +" +
"(.*/[^ ]*) +([^ (]+)(.+)?( *)$")

// Load the routes file.
func LoadRoutes(routePath string) *Router {
// Refresh re-reads the routes file and re-calculates the routing table.
// Returns an error if a specified action could not be found.
func (router *Router) Refresh() *Error {
// Get the routes file content.
contentBytes, err := ioutil.ReadFile(routePath)
contentBytes, err := ioutil.ReadFile(router.path)
if err != nil {
ERROR.Fatalln("Failed to load routes file:", err)
return &Error{
Title: "Failed to load routes file",
Description: err.Error(),
}
}
content := string(contentBytes)
return NewRouter(content)
}

func parseRouteLine(line string) (method, path, action string, found bool) {
var matches []string = routePattern.FindStringSubmatch(line)
if matches == nil {
return
}
method, path, action = matches[1], matches[4], matches[5]
found = true
return
return router.parse(string(contentBytes), true)
}

func NewRouter(routesConf string) *Router {
router := new(Router)
// parse takes the content of a routes file and turns it into the routing table.
func (router *Router) parse(content string, validate bool) *Error {
routes := make([]*Route, 0, 10)

// For each line..
for _, line := range strings.Split(routesConf, "\n") {
for n, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
continue
Expand All @@ -221,10 +209,84 @@ func NewRouter(routesConf string) *Router {

route := NewRoute(method, path, action)
routes = append(routes, route)

if validate {
if err := router.validate(route); err != nil {
err.Path = router.path
err.Line = n + 1
err.SourceLines = strings.Split(content, "\n")
return err
}
}
}

router.Routes = routes
return router
return nil
}

// Check that every specified action exists.
func (router *Router) validate(route *Route) *Error {
// Skip static routes
if route.staticDir != "" {
return nil
}

// Skip variable routes.
if strings.ContainsAny(route.Action, "{}") {
return nil
}

// We should be able to load the action.
parts := strings.Split(route.Action, ".")
if len(parts) != 2 {
return &Error{
Title: "Route validation error",
Description: fmt.Sprintf("Expected two parts (Controller.Action), but got %d: %s",
len(parts), route.Action),
}
}

ct := LookupControllerType(parts[0])
if ct == nil {
return &Error{
Title: "Route validation error",
Description: "Unrecognized controller: " + parts[0],
}
}

mt := ct.Method(parts[1])
if mt == nil {
return &Error{
Title: "Route validation error",
Description: "Unrecognized method: " + parts[1],
}
}
return nil
}

// Groups:
// 1: method
// 4: path
// 5: action
var routePattern *regexp.Regexp = regexp.MustCompile(
"(?i)^(GET|POST|PUT|DELETE|OPTIONS|HEAD|WS|\\*)" +
"[(]?([^)]*)(\\))? +" +
"(.*/[^ ]*) +([^ (]+)(.+)?( *)$")

func parseRouteLine(line string) (method, path, action string, found bool) {
var matches []string = routePattern.FindStringSubmatch(line)
if matches == nil {
return
}
method, path, action = matches[1], matches[4], matches[5]
found = true
return
}

func NewRouter(routesPath string) *Router {
return &Router{
path: routesPath,
}
}

type ActionDefinition struct {
Expand Down
6 changes: 4 additions & 2 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ var routeMatchTestCases = map[*http.Request]*RouteMatch{
}

func TestRouteMatches(t *testing.T) {
router := NewRouter(TEST_ROUTES)
router := NewRouter("")
router.parse(TEST_ROUTES, false)
for req, expected := range routeMatchTestCases {
actual := router.Route(req)
if !eq(t, "Found route", actual != nil, expected != nil) {
Expand Down Expand Up @@ -270,7 +271,8 @@ var reverseRoutingTestCases = map[*ReverseRouteArgs]*ActionDefinition{
}

func TestReverseRouting(t *testing.T) {
router := NewRouter(TEST_ROUTES)
router := NewRouter("")
router.parse(TEST_ROUTES, false)
for routeArgs, expected := range reverseRoutingTestCases {
actual := router.Reverse(routeArgs.action, routeArgs.args)
if !eq(t, "Found route", actual != nil, expected != nil) {
Expand Down
6 changes: 3 additions & 3 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ func handleInternal(w http.ResponseWriter, r *http.Request, ws *websocket.Conn)
// This is called from the generated main file.
func Run(address string, port int) {
routePath := path.Join(BasePath, "conf", "routes")

MainRouter = LoadRoutes(routePath)
MainRouter = NewRouter(routePath)
MainTemplateLoader = NewTemplateLoader(ViewsPath, RevelTemplatePath)

if RunMode == DEV {
MainWatcher = NewWatcher()
MainWatcher.Listen(MainTemplateLoader, []string{ViewsPath, RevelTemplatePath}, []string{})
MainWatcher.Listen(MainTemplateLoader, ViewsPath, RevelTemplatePath)
MainWatcher.Listen(MainRouter, routePath)
}

server := &http.Server{
Expand Down
59 changes: 48 additions & 11 deletions watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,76 @@ type Listener interface {
Refresh() *Error
}

// DiscerningListener allows the receiver to selectively watch files.
type DiscerningListener interface {
Listener
WatchDir(info os.FileInfo) bool
WatchFile(basename string) bool
}

// Watcher allows listeners to register to be notified of changes under a given
// directory.
type Watcher struct {
// Parallel arrays of watcher/listener pairs.
watchers []*fsnotify.Watcher
listeners []Listener
lastError int
watchers []*fsnotify.Watcher
listeners []Listener
forceRefresh bool
lastError int
}

func NewWatcher() *Watcher {
return &Watcher{
lastError: -1,
forceRefresh: true,
lastError: -1,
}
}

// Listen registers for events within the given root directories (recursively).
// The caller may specify directory names to skip (not watch or recurse into).
func (w *Watcher) Listen(listener Listener, roots []string, skip []string) {
func (w *Watcher) Listen(listener Listener, roots ...string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
ERROR.Fatal(err)
}

// Replace the unbuffered Event channel with a buffered one.
// Otherwise multiple change events only come out one at a time, across
// multiple page views.
watcher.Event = make(chan *fsnotify.FileEvent, 10)
// multiple page views. (There appears no way to "pump" the events out of
// the watcher)
watcher.Event = make(chan *fsnotify.FileEvent, 100)
watcher.Error = make(chan error, 10)

// Walk through all files / directories under the root, adding each to watcher.
for _, p := range roots {
fi, err := os.Stat(p)
if err != nil {
ERROR.Println("Failed to stat watched path", p, ":", err)
continue
}

// If it is a file, watch that specific file.
if !fi.IsDir() {
err = watcher.Watch(p)
if err != nil {
ERROR.Println("Failed to watch", p, ":", err)
}
TRACE.Println("Watching:", p)
continue
}

// Else, walk the directory tree.
filepath.Walk(p, func(path string, info os.FileInfo, err error) error {
if err != nil {
ERROR.Println("Error walking path:", err)
return nil
}

if info.IsDir() {
if ContainsString(skip, info.Name()) {
return filepath.SkipDir
if dl, ok := listener.(DiscerningListener); ok {
if !dl.WatchDir(info) {
return filepath.SkipDir
}
}

err = watcher.Watch(path)
if err != nil {
ERROR.Println("Failed to watch", path, ":", err)
Expand Down Expand Up @@ -82,6 +112,12 @@ func (w *Watcher) Notify() *Error {
case ev := <-watcher.Event:
// Ignore changes to dotfiles.
if !strings.HasPrefix(path.Base(ev.Name), ".") {
if dl, ok := listener.(DiscerningListener); ok {
if !dl.WatchFile(ev.Name) {
continue
}
}

refresh = true
}
continue
Expand All @@ -93,7 +129,7 @@ func (w *Watcher) Notify() *Error {
break
}

if refresh || w.lastError == i {
if w.forceRefresh || refresh || w.lastError == i {
err := listener.Refresh()
if err != nil {
w.lastError = i
Expand All @@ -102,6 +138,7 @@ func (w *Watcher) Notify() *Error {
}
}

w.forceRefresh = false
w.lastError = -1
return nil
}

0 comments on commit 6e56e8f

Please sign in to comment.