After getting the source code information, we can build the code to generate the binary file.

Build

There are two steps here.

// Build the app:
// 1. Generate the the main.go file.
// 2. Run the appropriate "go build" command.

The main.go are generated by a template file.

tmpl := template.Must(template.New("").Parse(REGISTER_CONTROLLERS))
registerControllerSource := rev.ExecuteTemplate(tmpl, map[string]interface{}{
	"Controllers":    sourceInfo.ControllerSpecs,
	"ValidationKeys": sourceInfo.ValidationKeys,
	"ImportPaths":    calcImportAliases(sourceInfo),
	"TestSuites":     sourceInfo.TestSuites,
})

And the source code informations will be passed into as a parameter map. Let’s take a look at the template file. Firstly, the import packages part:

import (
	"flag"
	"reflect"
	"github.com/robfig/revel"{{range $k, $v := $.importpaths}}
	{{$v}} "{{$k}}"{{end}}
)

The .importpaths is a map[string]string which is generated by calcImportAliases function. This function loop through all the ControllerSpecs and TestSuites to find all the import package. Note that if there is a duplication, it will append the index to the package name which is used as the package alias name. e.g. controller, controller0, controller1 ...

for containsValue(aliases, alias) {
	alias = fmt.Sprintf("%s%d", pkgName, i)
	i++
}

Next part is the command line flags

var (
	runMode    *string = flag.String("runMode", "", "Run mode.")
	port       *int    = flag.Int("port", 0, "By default, read from app.conf")
	importPath *string = flag.String("importPath", "", "Go Import Path for the app.")
	srcPath    *string = flag.String("srcPath", "", "Path to the source root.")

	// So compiler won't complain if the generated code doesn't reference reflect package...
	_ = reflect.Invalid
)

Here is a interesting place, revel don’t use reflect package. To prevent the accident from happening, it uses a trick which is like the comment says.

Then, of course, register the Controllers

{{range $i, $c := .Controllers}}
rev.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),
[]*rev.MethodType{
	{{range .MethodSpecs}}&rev.MethodType{
		Name: "{{.Name}}",
		Args: []*rev.MethodArg{
		{{range .Args}}
			&rev.MethodArg{
				Name: "{{.Name}}",
				Type: reflect.TypeOf((*{{index $.ImportPaths .ImportPath | .TypeExpr.TypeName}})(nil))
			},
		{{end}}
		},
		RenderArgNames: map[int][]string{ {{range .RenderCalls}}
			{{.Line}}: []string{ {{range .Names}}
				"{{.}}",{{end}}
			},{{end}}
		},
	},
	{{end}}
})
{{end}}

At last, ValidationKeys and TestSuites

rev.DefaultValidationKeys = map[string]map[int]string{ {{range $path, $lines := .ValidationKeys}}
	"{{$path}}": { {{range $line, $key := $lines}}
		{{$line}}: "{{$key}}",{{end}}
	},{{end}}
}
rev.TestSuites = []interface{}{ {{range .TestSuites}}
	(*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}}
}

Don’t forget to start daemon

rev.Run(*port)

This all the content of the main.go file. After talking about the main.go, revel has to compile the source code to a runnable binary file. If you import a package that is not installed in the local system. Revel will use go get XXX to install it and do a rebuild process.

for {
	buildCmd := exec.Command(goPath, "build", "-o", binName, path.Join(rev.ImportPath, "app", "tmp"))
	rev.TRACE.Println("Exec:", buildCmd.Args)
	output, err := buildCmd.CombinedOutput()

	// If the build succeeded, we're done.
	if err == nil {
		return NewApp(binName), nil
	}

	...

	// Execute "go get <pkg>"
	getCmd := exec.Command(goPath, "get", pkgName)
	rev.TRACE.Println("Exec:", getCmd.Args)
	getOutput, err := getCmd.CombinedOutput()
	if err != nil {
		rev.TRACE.Println(string(getOutput))
		return nil, newCompileError(output)
	}

	// Success getting the import, attempt to build again.
}

rev.App is a wrapper of the *exec.Cmd, it is too simple to talk in detail. Just a place, when you start the server with rev.Appcmd.start, it will wait until it is ready to serve request. This function is accomplish with a filter output interface.

func (cmd AppCmd) Start() {
	listeningWriter := startupListeningWriter{os.Stdout, make(chan bool)}
	cmd.Stdout = listeningWriter
	rev.TRACE.Println("Exec app:", cmd.Path, cmd.Args)
	if err := cmd.Cmd.Start(); err != nil {
		rev.ERROR.Fatalln("Error running:", err)
	}
	<-listeningWriter.notifyReady
}

And startupListeningWriter is

type startupListeningWriter struct {
	dest        io.Writer
	notifyReady chan bool
}

func (w startupListeningWriter) Write(p []byte) (n int, err error) {
	if w.notifyReady != nil && bytes.Contains(p, []byte("Listening")) {
		w.notifyReady <- true
		w.notifyReady = nil
	}
	return w.dest.Write(p)
}

Rebuild

Revel will automatically rebuild the source code if some of source code have been changed. This function make online-debug code easy. And how does this work?

Watcher

All these works are done by the rev.Watcher.

There are some represents to be introduced firstly. If you want keep track of some specified changes, you will be a Listener.

// Listener is an interface for receivers of filesystem events.
type Listener interface {
	// Refresh is invoked by the watcher on relevant filesystem events.
	// If the listener returns an error, it is served to the user on the current request.
	Refresh() *Error
}

Refresh() is the callback function for the changes. More than that, you could even filter some files or directories that you won’t concern about. In that case, you will be a DiscerningListener.

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

What is surprising, you may take action when listener get refresh. It is also all right, and you will be regard as a Auditor.

// Auditor gets notified each time a listener gets refreshed.
type Auditor interface {
	OnRefresh(listener Listener)
}

rev.Watcher is central structure to manage all the above saying. Of course, rev.Watcher is based on fsnotify.Watcher.

// 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
	auditor      Auditor
	forceRefresh bool
	lastError    int
	notifyMutex  sync.Mutex
}

The global the hierarchy relation is like this

watcher

Harness has a ability to be a DiscerningListener

func (h *Harness) Refresh() (err *rev.Error) {
	if h.app != nil {
		h.app.Kill()
	}

	rev.TRACE.Println("Rebuild")
	h.app, err = Build()
	if err != nil {
		return
	}

	h.app.Port = h.port
	h.app.Cmd().Start()
	return
}

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")
}
  • doNotWatch is []string{"tmp", "views"}
  • it only concern about all the *.go files.
  • and its refresh callback is to rebuild the application with the new source.

How to start a harness

If you want to run the application in “watched” mode, you must set the watch and watch.code true in configure file. Otherwise, revel just compile the source once, and doesn’t rebuild the application if need.

// If the app is run in "watched" mode, use the harness to run it.
if rev.Config.BoolDefault("watch", true) && rev.Config.BoolDefault("watch.code", true) {
	rev.HttpPort = port
	harness.NewHarness().Run() // Never returns.
}

// Else, just build and run the app.
app, err := harness.Build()
if err != nil {
	errorf("Failed to build app: %s", err)
}
app.Port = port
app.Cmd().Run()

And in Harness.Run(), a watcher will be initialed.

func (h *Harness) Run() {
	watcher = rev.NewWatcher()
	watcher.Listen(h, rev.CodePaths...)
	...

On every http request reached, the harness will do a refresh. To do this, the harness also satisfies the http.Handler

func (hp *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Don't rebuild the app for favicon requests.
	if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" {
		return
	}

	// Flush any change events and rebuild app if necessary.
	// Render an error page if the rebuild / restart failed.
	err := watcher.Notify()
	...
}

FIN.