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
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.