Initial rewrite of revel/cmd to provide vendor support and enhanced CLI options

This commit is contained in:
NotZippy
2018-09-13 13:21:10 -07:00
parent d2ac018544
commit d0baaeb9e9
40 changed files with 4435 additions and 1125 deletions

View File

@@ -13,7 +13,9 @@ import (
"os/exec"
"time"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"log"
)
// App contains the configuration for running a Revel app. (Not for the app itself)
@@ -22,16 +24,17 @@ type App struct {
BinaryPath string // Path to the app executable
Port int // Port to pass as a command line argument.
cmd AppCmd // The last cmd returned.
Paths *model.RevelContainer
}
// NewApp returns app instance with binary path in it
func NewApp(binPath string) *App {
return &App{BinaryPath: binPath}
func NewApp(binPath string, paths *model.RevelContainer) *App {
return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort}
}
// Cmd returns a command to run the app server using the current configuration.
func (a *App) Cmd() AppCmd {
a.cmd = NewAppCmd(a.BinaryPath, a.Port)
func (a *App) Cmd(runMode string) AppCmd {
a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths)
return a.cmd
}
@@ -47,22 +50,22 @@ type AppCmd struct {
}
// NewAppCmd returns the AppCmd with parameters initialized for running app
func NewAppCmd(binPath string, port int) AppCmd {
func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelContainer) AppCmd {
cmd := exec.Command(binPath,
fmt.Sprintf("-port=%d", port),
fmt.Sprintf("-importPath=%s", revel.ImportPath),
fmt.Sprintf("-runMode=%s", revel.RunMode))
fmt.Sprintf("-importPath=%s", paths.ImportPath),
fmt.Sprintf("-runMode=%s", runMode))
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
return AppCmd{cmd}
}
// Start the app server, and wait until it is ready to serve requests.
func (cmd AppCmd) Start() error {
listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool)}
func (cmd AppCmd) Start(c *model.CommandConfig) error {
listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool),c}
cmd.Stdout = listeningWriter
revel.RevelLog.Debug("Exec app:", "path", cmd.Path, "args", cmd.Args)
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args)
if err := cmd.Cmd.Start(); err != nil {
revel.RevelLog.Fatal("Error running:", "error", err)
utils.Logger.Fatal("Error running:", "error", err)
}
select {
@@ -70,7 +73,7 @@ func (cmd AppCmd) Start() error {
return errors.New("revel/harness: app died")
case <-time.After(30 * time.Second):
revel.RevelLog.Debug("Killing revel server process did not respond after wait timeout", "processid", cmd.Process.Pid)
log.Println("Killing revel server process did not respond after wait timeout", "processid", cmd.Process.Pid)
cmd.Kill()
return errors.New("revel/harness: app timed out")
@@ -84,19 +87,19 @@ func (cmd AppCmd) Start() error {
// Run the app server inline. Never returns.
func (cmd AppCmd) Run() {
revel.RevelLog.Debug("Exec app:", "path", cmd.Path, "args", cmd.Args)
log.Println("Exec app:", "path", cmd.Path, "args", cmd.Args)
if err := cmd.Cmd.Run(); err != nil {
revel.RevelLog.Fatal("Error running:", "error", err)
utils.Logger.Fatal("Error running:", "error", err)
}
}
// Kill terminates the app server if it's running.
func (cmd AppCmd) Kill() {
if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) {
revel.RevelLog.Debug("Killing revel server pid", "pid", cmd.Process.Pid)
utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid)
err := cmd.Process.Kill()
if err != nil {
revel.RevelLog.Fatal("Failed to kill revel server:", "error", err)
utils.Logger.Fatal("Failed to kill revel server:", "error", err)
}
}
}
@@ -111,18 +114,25 @@ func (cmd AppCmd) waitChan() <-chan struct{} {
return ch
}
// A io.Writer that copies to the destination, and listens for "Listening on.."
// A io.Writer that copies to the destination, and listens for "Revel engine is listening on.."
// in the stream. (Which tells us when the revel server has finished starting up)
// This is super ghetto, but by far the simplest thing that should work.
type startupListeningWriter struct {
dest io.Writer
notifyReady chan bool
c *model.CommandConfig
}
func (w *startupListeningWriter) Write(p []byte) (n int, err error) {
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening")) {
func (w *startupListeningWriter) Write(p []byte) (int, error) {
if w.notifyReady != nil && bytes.Contains(p, []byte("Revel engine is listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
if w.c.HistoricMode {
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
}
return w.dest.Write(p)
}

View File

@@ -13,18 +13,19 @@ import (
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"text/template"
"time"
"sort"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/parser"
"github.com/revel/cmd/utils"
)
var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"")
type ByString []*TypeInfo
type ByString []*model.TypeInfo
func (c ByString) Len() int { return len(c) }
func (c ByString) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
@@ -35,17 +36,17 @@ func (c ByString) Less(i, j int) bool { return c[i].String() < c[j].String() }
// 2. Run the appropriate "go build" command.
// Requires that revel.Init has been called previously.
// Returns the path to the built binary, and an error if there was a problem building it.
func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
func Build(c *model.CommandConfig, paths *model.RevelContainer, buildFlags ...string) (app *App, compileError *utils.Error) {
// First, clear the generated files (to avoid them messing with ProcessSource).
cleanSource("tmp", "routes")
cleanSource(paths, "tmp", "routes")
sourceInfo, compileError := ProcessSource(revel.CodePaths)
sourceInfo, compileError := parser.ProcessSource(paths)
if compileError != nil {
return nil, compileError
return
}
// Add the db.import to the import paths.
if dbImportPath, found := revel.Config.String("db.import"); found {
if dbImportPath, found := paths.Config.String("db.import"); found {
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, strings.Split(dbImportPath, ",")...)
}
@@ -60,28 +61,28 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
"ImportPaths": calcImportAliases(sourceInfo),
"TestSuites": sourceInfo.TestSuites(),
}
genSource("tmp", "main.go", RevelMainTemplate, templateArgs)
genSource("routes", "routes.go", RevelRoutesTemplate, templateArgs)
genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs)
genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs)
// Read build config.
buildTags := revel.Config.StringDefault("build.tags", "")
buildTags := paths.Config.StringDefault("build.tags", "")
// Build the user program (all code under app).
// It relies on the user having "go" installed.
goPath, err := exec.LookPath("go")
if err != nil {
revel.RevelLog.Fatalf("Go executable not found in PATH.")
utils.Logger.Fatal("Go executable not found in PATH.")
}
// Detect if deps tool should be used (is there a vendor folder ?)
useVendor := revel.DirExists(filepath.Join(revel.BasePath, "vendor"))
basePath := revel.BasePath
useVendor := utils.DirExists(filepath.Join(paths.BasePath, "vendor"))
basePath := paths.BasePath
for !useVendor {
basePath = filepath.Dir(basePath)
found := false
// Check to see if we are still in the GOPATH
for _, path := range filepath.SplitList(build.Default.GOPATH) {
if strings.HasPrefix(basePath, path) {
for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
if strings.HasPrefix(basePath, gopath) {
found = true
break
}
@@ -89,31 +90,31 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
if !found {
break
} else {
useVendor = revel.DirExists(filepath.Join(basePath, "vendor"))
useVendor = utils.DirExists(filepath.Join(basePath, "vendor"))
}
}
var depPath string
if useVendor {
revel.RevelLog.Info("Vendor folder detected, scanning for deps in path")
utils.Logger.Info("Vendor folder detected, scanning for deps in path")
depPath, err = exec.LookPath("dep")
if err != nil {
// Do not halt build unless a new package needs to be imported
revel.RevelLog.Warn("Build: `dep` executable not found in PATH, but vendor folder detected." +
utils.Logger.Warn("Build: `dep` executable not found in PATH, but vendor folder detected." +
"Packages can only be added automatically to the vendor folder using the `dep` tool. " +
"You can install the `dep` tool by doing a `go get -u github.com/golang/dep/cmd/dep`")
}
} else {
revel.RevelLog.Info("No vendor folder detected, not using dependency manager to import files")
utils.Logger.Info("No vendor folder detected, not using dependency manager to import files")
}
pkg, err := build.Default.Import(revel.ImportPath, "", build.FindOnly)
pkg, err := build.Default.Import(paths.ImportPath, "", build.FindOnly)
if err != nil {
revel.RevelLog.Fatal("Failure importing", "path", revel.ImportPath)
utils.Logger.Fatal("Failure importing", "path", paths.ImportPath)
}
// Binary path is a combination of $GOBIN/revel.d directory, app's import path and its name.
binName := filepath.Join(pkg.BinDir, "revel.d", revel.ImportPath, filepath.Base(revel.BasePath))
binName := filepath.Join(pkg.BinDir, "revel.d", paths.ImportPath, filepath.Base(paths.BasePath))
// Change binary path for Windows build
goos := runtime.GOOS
@@ -125,47 +126,90 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
}
gotten := make(map[string]struct{})
contains := func (s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
for {
appVersion := getAppVersion()
appVersion := getAppVersion(paths)
buildTime := time.Now().UTC().Format(time.RFC3339)
versionLinkerFlags := fmt.Sprintf("-X %s/app.AppVersion=%s -X %s/app.BuildTime=%s",
revel.ImportPath, appVersion, revel.ImportPath, buildTime)
paths.ImportPath, appVersion, paths.ImportPath, buildTime)
// Append any build flags specified, they will override existing flags
flags := []string{}
if len(c.BuildFlags)==0 {
flags = []string{
"build",
"-i",
"-ldflags", versionLinkerFlags,
"-tags", buildTags,
"-o", binName}
} else {
if !contains(c.BuildFlags,"build") {
flags = []string{"build"}
}
flags = append(flags,c.BuildFlags...)
if !contains(flags, "-ldflags") {
flags = append(flags,"-ldflags", versionLinkerFlags)
}
if !contains(flags, "-tags") {
flags = append(flags,"-tags", buildTags)
}
if !contains(flags, "-o") {
flags = append(flags,"-o", binName)
}
}
flags := []string{
"build",
"-i",
"-ldflags", versionLinkerFlags,
"-tags", buildTags,
"-o", binName}
// Add in build flags
flags = append(flags, buildFlags...)
// This is Go main path
gopath := c.GoPath
for _, o := range paths.ModulePathMap {
gopath += string(filepath.ListSeparator) + o
}
// Note: It's not applicable for filepath.* usage
flags = append(flags, path.Join(revel.ImportPath, "app", "tmp"))
flags = append(flags, path.Join(paths.ImportPath, "app", "tmp"))
buildCmd := exec.Command(goPath, flags...)
revel.RevelLog.Debug("Exec:", "args", buildCmd.Args)
buildCmd.Env = append(os.Environ(),
"GOPATH="+gopath,
)
utils.Logger.Info("Exec:", "args", buildCmd.Args)
output, err := buildCmd.CombinedOutput()
// If the build succeeded, we're done.
if err == nil {
return NewApp(binName), nil
return NewApp(binName, paths), nil
}
revel.RevelLog.Error(string(output))
// Since there was an error, capture the output in case we need to report it
stOutput := string(output)
// See if it was an import error that we can go get.
matches := importErrorPattern.FindAllStringSubmatch(string(output), -1)
matches := importErrorPattern.FindAllStringSubmatch(stOutput, -1)
utils.Logger.Info("Build failed checking for missing imports", "message", stOutput, "missing_imports", len(matches))
if matches == nil {
return nil, newCompileError(output)
utils.Logger.Info("Build failed no missing imports", "message", stOutput)
return nil, newCompileError(paths, output)
}
utils.Logger.Warn("Detected missing packages, importing them", "packages", len(matches))
for _, match := range matches {
// Ensure we haven't already tried to go get it.
pkgName := match[1]
utils.Logger.Info("Trying to import ", "package", pkgName)
if _, alreadyTried := gotten[pkgName]; alreadyTried {
return nil, newCompileError(output)
utils.Logger.Error("Failed to import ", "package", pkgName)
return nil, newCompileError(paths, output)
}
gotten[pkgName] = struct{}{}
@@ -174,23 +218,26 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
var getCmd *exec.Cmd
if useVendor {
if depPath == "" {
revel.RevelLog.Error("Build: Vendor folder found, but the `dep` tool was not found, " +
utils.Logger.Warn("Build: Vendor folder found, but the `dep` tool was not found, " +
"if you use a different vendoring (package management) tool please add the following packages by hand, " +
"or install the `dep` tool into your gopath by doing a `go get -u github.com/golang/dep/cmd/dep`. " +
"For more information and usage of the tool please see http://github.com/golang/dep")
for _, pkg := range matches {
revel.RevelLog.Error("Missing package", "package", pkg[1])
utils.Logger.Warn("Missing package", "package", pkg[1])
}
}
getCmd = exec.Command(depPath, "ensure", "-add", pkgName)
getCmd.Dir = paths.AppPath
} else {
getCmd = exec.Command(goPath, "get", pkgName)
}
revel.RevelLog.Debug("Exec:", "args", getCmd.Args)
utils.Logger.Info("Exec:", "args", getCmd.Args)
getOutput, err := getCmd.CombinedOutput()
if err != nil {
revel.RevelLog.Error(string(getOutput))
return nil, newCompileError(output)
utils.Logger.Error("Build failed", "message", stOutput)
utils.Logger.Error("Failed to fetch the output", string(getOutput))
return nil, newCompileError(paths, output)
}
}
@@ -198,7 +245,7 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
}
// TODO remove this unreachable code and document it
revel.RevelLog.Fatalf("Not reachable")
utils.Logger.Fatal("Not reachable")
return nil, nil
}
@@ -208,7 +255,7 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
// variable
// - Read the output of "git describe" if the source is in a git repository
// If no version can be determined, an empty string is returned.
func getAppVersion() string {
func getAppVersion(paths *model.RevelContainer) string {
if version := os.Getenv("APP_VERSION"); version != "" {
return version
}
@@ -216,17 +263,17 @@ func getAppVersion() string {
// Check for the git binary
if gitPath, err := exec.LookPath("git"); err == nil {
// Check for the .git directory
gitDir := filepath.Join(revel.BasePath, ".git")
gitDir := filepath.Join(paths.BasePath, ".git")
info, err := os.Stat(gitDir)
if (err != nil && os.IsNotExist(err)) || !info.IsDir() {
return ""
}
gitCmd := exec.Command(gitPath, "--git-dir="+gitDir, "--work-tree="+revel.BasePath, "describe", "--always", "--dirty")
revel.RevelLog.Debug("Exec:", "args", gitCmd.Args)
gitCmd := exec.Command(gitPath, "--git-dir="+gitDir, "--work-tree="+paths.BasePath, "describe", "--always", "--dirty")
utils.Logger.Info("Exec:", "args", gitCmd.Args)
output, err := gitCmd.Output()
if err != nil {
revel.RevelLog.Warn("Cannot determine git repository version:", "error", err)
utils.Logger.Error("Cannot determine git repository version:", "error", err)
return ""
}
@@ -236,19 +283,19 @@ func getAppVersion() string {
return ""
}
func cleanSource(dirs ...string) {
func cleanSource(paths *model.RevelContainer, dirs ...string) {
for _, dir := range dirs {
cleanDir(dir)
cleanDir(paths, dir)
}
}
func cleanDir(dir string) {
revel.RevelLog.Info("Cleaning dir " + dir)
tmpPath := filepath.Join(revel.AppPath, dir)
func cleanDir(paths *model.RevelContainer, dir string) {
utils.Logger.Info("Cleaning dir ", "dir", dir)
tmpPath := filepath.Join(paths.AppPath, dir)
f, err := os.Open(tmpPath)
if err != nil {
if !os.IsNotExist(err) {
revel.RevelLog.Error("Failed to clean dir:", "error", err)
utils.Logger.Error("Failed to clean dir:", "error", err)
}
} else {
defer func() {
@@ -258,7 +305,7 @@ func cleanDir(dir string) {
infos, err := f.Readdir(0)
if err != nil {
if !os.IsNotExist(err) {
revel.RevelLog.Error("Failed to clean dir:", "error", err)
utils.Logger.Fatal("Failed to clean dir:", "error", err)
}
} else {
for _, info := range infos {
@@ -266,12 +313,12 @@ func cleanDir(dir string) {
if info.IsDir() {
err := os.RemoveAll(pathName)
if err != nil {
revel.RevelLog.Error("Failed to remove dir:", "error", err)
utils.Logger.Fatal("Failed to remove dir:", "error", err)
}
} else {
err := os.Remove(pathName)
if err != nil {
revel.RevelLog.Error("Failed to remove file:", "error", err)
utils.Logger.Fatal("Failed to remove file:", "error", err)
}
}
}
@@ -281,39 +328,21 @@ func cleanDir(dir string) {
// genSource renders the given template to produce source code, which it writes
// to the given directory and file.
func genSource(dir, filename, templateSource string, args map[string]interface{}) {
sourceCode := revel.ExecuteTemplate(
template.Must(template.New("").Parse(templateSource)),
args)
func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) {
cleanSource(paths, dir)
// Create a fresh dir.
cleanSource(dir)
tmpPath := filepath.Join(revel.AppPath, dir)
err := os.Mkdir(tmpPath, 0777)
if err != nil && !os.IsExist(err) {
revel.RevelLog.Fatalf("Failed to make '%v' directory: %v", dir, err)
}
// Create the file
file, err := os.Create(filepath.Join(tmpPath, filename))
err := utils.MustGenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args)
if err != nil {
revel.RevelLog.Fatalf("Failed to create file: %v", err)
}
defer func() {
_ = file.Close()
}()
if _, err = file.WriteString(sourceCode); err != nil {
revel.RevelLog.Fatalf("Failed to write to file: %v", err)
utils.Logger.Fatal("Failed to generate template for source file", "error", err)
}
}
// Looks through all the method args and returns a set of unique import paths
// that cover all the method arg types.
// Additionally, assign package aliases when necessary to resolve ambiguity.
func calcImportAliases(src *SourceInfo) map[string]string {
func calcImportAliases(src *model.SourceInfo) map[string]string {
aliases := make(map[string]string)
typeArrays := [][]*TypeInfo{src.ControllerSpecs(), src.TestSuites()}
typeArrays := [][]*model.TypeInfo{src.ControllerSpecs(), src.TestSuites()}
for _, specs := range typeArrays {
for _, spec := range specs {
addAlias(aliases, spec.ImportPath, spec.PackageName)
@@ -340,6 +369,7 @@ func calcImportAliases(src *SourceInfo) map[string]string {
return aliases
}
// Adds an alias to the map of alias names
func addAlias(aliases map[string]string, importPath, pkgName string) {
alias, ok := aliases[importPath]
if ok {
@@ -349,6 +379,7 @@ func addAlias(aliases map[string]string, importPath, pkgName string) {
aliases[importPath] = alias
}
// Generates a package alias
func makePackageAlias(aliases map[string]string, pkgName string) string {
i := 0
alias := pkgName
@@ -359,6 +390,7 @@ func makePackageAlias(aliases map[string]string, pkgName string) string {
return alias
}
// Returns true if this value is in the map
func containsValue(m map[string]string, val string) bool {
for _, v := range m {
if v == val {
@@ -370,15 +402,15 @@ func containsValue(m map[string]string, val string) bool {
// Parse the output of the "go build" command.
// Return a detailed Error.
func newCompileError(output []byte) *revel.Error {
func newCompileError(paths *model.RevelContainer, output []byte) *utils.Error {
errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`).
FindSubmatch(output)
if errorMatch == nil {
errorMatch = regexp.MustCompile(`(?m)^(.*?)\:(\d+)\:\s(.*?)$`).FindSubmatch(output)
errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch(output)
if errorMatch == nil {
revel.RevelLog.Error("Failed to parse build errors", "error", string(output))
return &revel.Error{
utils.Logger.Error("Failed to parse build errors", "error", string(output))
return &utils.Error{
SourceType: "Go code",
Title: "Go Compilation Error",
Description: "See console for build error.",
@@ -387,16 +419,30 @@ func newCompileError(output []byte) *revel.Error {
errorMatch = append(errorMatch, errorMatch[3])
revel.RevelLog.Error("Build errors", "errors", string(output))
utils.Logger.Error("Build errors", "errors", string(output))
}
findInPaths := func(relFilename string) string {
// Extract the paths from the gopaths, and search for file there first
gopaths := filepath.SplitList(build.Default.GOPATH)
for _, gp := range gopaths {
newPath := filepath.Join(gp, relFilename)
if utils.Exists(newPath) {
return newPath
}
}
newPath, _ := filepath.Abs(relFilename)
utils.Logger.Warn("Could not find in GO path", "file", relFilename)
return newPath
}
// Read the source for the offending file.
var (
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
absFilename, _ = filepath.Abs(relFilename)
line, _ = strconv.Atoi(string(errorMatch[2]))
description = string(errorMatch[4])
compileError = &revel.Error{
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
absFilename = findInPaths(relFilename)
line, _ = strconv.Atoi(string(errorMatch[2]))
description = string(errorMatch[4])
compileError = &utils.Error{
SourceType: "Go code",
Title: "Go Compilation Error",
Path: relFilename,
@@ -405,16 +451,16 @@ func newCompileError(output []byte) *revel.Error {
}
)
errorLink := revel.Config.StringDefault("error.link", "")
errorLink := paths.Config.StringDefault("error.link", "")
if errorLink != "" {
compileError.SetLink(errorLink)
}
fileStr, err := revel.ReadLines(absFilename)
fileStr, err := utils.ReadLines(absFilename)
if err != nil {
compileError.MetaError = absFilename + ": " + err.Error()
revel.RevelLog.Error(compileError.MetaError)
utils.Logger.Error("Unable to readlines "+compileError.MetaError, "error", err)
return compileError
}
@@ -424,6 +470,9 @@ func newCompileError(output []byte) *revel.Error {
// RevelMainTemplate template for app/tmp/main.go
const RevelMainTemplate = `// GENERATED CODE - DO NOT EDIT
// This file is the main file for Revel.
// It registers all the controllers and provides details for the Revel server engine to
// properly inject parameters directly into the action endpoints.
package main
import (
@@ -480,6 +529,8 @@ func main() {
// RevelRoutesTemplate template for app/conf/routes
const RevelRoutesTemplate = `// GENERATED CODE - DO NOT EDIT
// This file provides a way of creating URL's based on all the actions
// found in all the controllers.
package routes
import "github.com/revel/revel"

View File

@@ -28,8 +28,12 @@ import (
"strings"
"sync/atomic"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"github.com/revel/cmd/watcher"
"sync"
"html/template"
"io/ioutil"
)
var (
@@ -41,20 +45,75 @@ var (
// Harness reverse proxies requests to the application server.
// It builds / runs / rebuilds / restarts the server when code is changed.
type Harness struct {
app *App
serverHost string
port int
proxy *httputil.ReverseProxy
watcher *revel.Watcher
mutex *sync.Mutex
app *App // The application
useProxy bool // True if proxy is in use
serverHost string // The proxy server host
port int // The proxy serber port
proxy *httputil.ReverseProxy // The proxy
watcher *watcher.Watcher // The file watched
mutex *sync.Mutex // A mutex to prevent concurrent updates
paths *model.RevelContainer // The Revel container
config *model.CommandConfig // The configuration
runMode string // The runmode the harness is running in
}
func renderError(iw http.ResponseWriter, ir *http.Request, err error) {
context := revel.NewGoContext(nil)
context.Request.SetRequest(ir)
context.Response.SetResponse(iw)
c := revel.NewController(context)
c.RenderError(err).Apply(c.Request, c.Response)
func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err error) {
// Render error here
// Grab the template from three places
// 1) Application/views/errors
// 2) revel_home/views/errors
// 3) views/errors
templateSet := template.New("__root__")
seekViewOnPath:=func(view string) (path string) {
path = filepath.Join(h.paths.ViewsPath, "errors", view)
if !utils.Exists(path) {
path = filepath.Join(h.paths.RevelPath, "templates", "errors", view)
}
println(path)
data,err := ioutil.ReadFile(path)
if err!=nil {
utils.Logger.Error("Unable to read template file", path)
}
_,err = templateSet.New("errors/"+view).Parse(string(data))
if err!=nil {
utils.Logger.Error("Unable to parse template file", path)
}
return
}
target := []string{seekViewOnPath("500.html"),seekViewOnPath("500-dev.html")}
if !utils.Exists(target[0]) {
fmt.Fprint(iw, "An error occurred %s", err)
return
}
var revelError *utils.Error
switch e := err.(type) {
case *utils.Error:
revelError = e
case error:
revelError = &utils.Error{
Title: "Server Error",
Description: e.Error(),
}
}
if revelError == nil {
panic("no error provided")
}
viewArgs := map[string]interface{}{}
viewArgs["RunMode"] = h.paths.RunMode
viewArgs["DevMode"] = h.paths.DevMode
viewArgs["Error"] = revelError
// Render the template from the file
err = templateSet.ExecuteTemplate(iw,"errors/500.html",viewArgs)
if err!=nil {
utils.Logger.Error("Failed to execute","error",err)
}
fmt.Println("template ",templateSet.Templates()[0].Name(), templateSet.Templates()[1].Name())
//utils.MustRenderTemplateToStream(iw,target, viewArgs)
}
// ServeHTTP handles all requests.
@@ -68,11 +127,12 @@ func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Flush any change events and rebuild app if necessary.
// Render an error page if the rebuild / restart failed.
err := h.watcher.Notify()
println("Serving ", err)
if err != nil {
// In a thread safe manner update the flag so that a request for
// /favicon.ico does not trigger a rebuild
atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1)
renderError(w, r, err)
h.renderError(w, r, err)
return
}
@@ -83,7 +143,7 @@ func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Reverse proxy the request.
// (Need special code for websockets, courtesy of bradfitz)
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
proxyWebsocket(w, r, h.serverHost)
h.proxyWebsocket(w, r, h.serverHost)
} else {
h.proxy.ServeHTTP(w, r)
}
@@ -91,24 +151,26 @@ func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewHarness method returns a reverse proxy that forwards requests
// to the given port.
func NewHarness() *Harness {
func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode string, noProxy bool) *Harness {
// Get a template loader to render errors.
// Prefer the app's views/errors directory, and fall back to the stock error pages.
revel.MainTemplateLoader = revel.NewTemplateLoader(
[]string{filepath.Join(revel.RevelPath, "templates")})
if err := revel.MainTemplateLoader.Refresh(); err != nil {
revel.RevelLog.Error("Template loader error", "error", err)
}
//revel.MainTemplateLoader = revel.NewTemplateLoader(
// []string{filepath.Join(revel.RevelPath, "templates")})
//if err := revel.MainTemplateLoader.Refresh(); err != nil {
// revel.RevelLog.Error("Template loader error", "error", err)
//}
addr := revel.HTTPAddr
port := revel.Config.IntDefault("harness.port", 0)
addr := paths.HTTPAddr
port := paths.Config.IntDefault("harness.port", 0)
scheme := "http"
if revel.HTTPSsl {
if paths.HTTPSsl {
scheme = "https"
}
// If the server is running on the wildcard address, use "localhost"
if addr == "" {
utils.Logger.Warn("No http.addr specified in the app.conf listening on localhost interface only. " +
"This will not allow external access to your application")
addr = "localhost"
}
@@ -123,9 +185,14 @@ func NewHarness() *Harness {
serverHost: serverURL.String()[len(scheme+"://"):],
proxy: httputil.NewSingleHostReverseProxy(serverURL),
mutex: &sync.Mutex{},
paths: paths,
useProxy: !noProxy,
config: c,
runMode: runMode,
}
if revel.HTTPSsl {
if paths.HTTPSsl {
serverHarness.proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
@@ -134,8 +201,13 @@ func NewHarness() *Harness {
}
// Refresh method rebuilds the Revel application and run it on the given port.
func (h *Harness) Refresh() (err *revel.Error) {
// called by the watcher
func (h *Harness) Refresh() (err *utils.Error) {
// Allow only one thread to rebuild the process
// If multiple requests to rebuild are queued only the last one is executed on
// So before a build is started we wait for a second to determine if
// more requests for a build are triggered.
// Once no more requests are triggered the build will be processed
h.mutex.Lock()
defer h.mutex.Unlock()
@@ -143,18 +215,24 @@ func (h *Harness) Refresh() (err *revel.Error) {
h.app.Kill()
}
revel.RevelLog.Debug("Rebuild Called")
h.app, err = Build()
utils.Logger.Info("Rebuild Called")
h.app, err = Build(h.config, h.paths)
if err != nil {
utils.Logger.Error("Build detected an error", "error", err)
return
}
h.app.Port = h.port
if err2 := h.app.Cmd().Start(); err2 != nil {
return &revel.Error{
Title: "App failed to start up",
Description: err2.Error(),
if h.useProxy {
h.app.Port = h.port
if err2 := h.app.Cmd(h.runMode).Start(h.config); err2 != nil {
utils.Logger.Error("Could not start application", "error", err2)
return &utils.Error{
Title: "App failed to start up",
Description: err2.Error(),
}
}
} else {
h.app = nil
}
return
@@ -163,7 +241,7 @@ func (h *Harness) Refresh() (err *revel.Error) {
// WatchDir method returns false to file matches with doNotWatch
// otheriwse true
func (h *Harness) WatchDir(info os.FileInfo) bool {
return !revel.ContainsString(doNotWatch, info.Name())
return !utils.ContainsString(doNotWatch, info.Name())
}
// WatchFile method returns true given filename HasSuffix of ".go"
@@ -176,34 +254,37 @@ func (h *Harness) WatchFile(filename string) bool {
// server, which it runs and rebuilds as necessary.
func (h *Harness) Run() {
var paths []string
if revel.Config.BoolDefault("watch.gopath", false) {
if h.paths.Config.BoolDefault("watch.gopath", false) {
gopaths := filepath.SplitList(build.Default.GOPATH)
paths = append(paths, gopaths...)
}
paths = append(paths, revel.CodePaths...)
h.watcher = revel.NewWatcher()
paths = append(paths, h.paths.CodePaths...)
h.watcher = watcher.NewWatcher(h.paths, false)
h.watcher.Listen(h, paths...)
h.watcher.Notify()
go func() {
addr := fmt.Sprintf("%s:%d", revel.HTTPAddr, revel.HTTPPort)
revel.RevelLog.Infof("Listening on %s", addr)
if h.useProxy {
go func() {
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
utils.Logger.Infof("Proxy server is listening on %s", addr)
println("Proxy server is listening on ", addr)
var err error
if revel.HTTPSsl {
err = http.ListenAndServeTLS(
addr,
revel.HTTPSslCert,
revel.HTTPSslKey,
h)
} else {
err = http.ListenAndServe(addr, h)
}
if err != nil {
revel.RevelLog.Error("Failed to start reverse proxy:", "error", err)
}
}()
var err error
if h.paths.HTTPSsl {
err = http.ListenAndServeTLS(
addr,
h.paths.HTTPSslCert,
h.paths.HTTPSslKey,
h)
} else {
err = http.ListenAndServe(addr, h)
}
if err != nil {
utils.Logger.Error("Failed to start reverse proxy:", "error", err)
}
}()
}
// Kill the app on signal.
ch := make(chan os.Signal)
signal.Notify(ch, os.Interrupt, os.Kill)
@@ -218,25 +299,25 @@ func (h *Harness) Run() {
func getFreePort() (port int) {
conn, err := net.Listen("tcp", ":0")
if err != nil {
revel.RevelLog.Fatal("Unable to fetch a freee port address", "error", err)
utils.Logger.Fatal("Unable to fetch a freee port address", "error", err)
}
port = conn.Addr().(*net.TCPAddr).Port
err = conn.Close()
if err != nil {
revel.RevelLog.Fatal("Unable to close port", "error", err)
utils.Logger.Fatal("Unable to close port", "error", err)
}
return port
}
// proxyWebsocket copies data between websocket client and server until one side
// closes the connection. (ReverseProxy doesn't work with websocket requests.)
func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
func (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
var (
d net.Conn
err error
)
if revel.HTTPSsl {
if h.paths.HTTPSsl {
// since this proxy isn't used in production,
// it's OK to set InsecureSkipVerify to true
// no need to add another configuration option.
@@ -246,7 +327,7 @@ func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
}
if err != nil {
http.Error(w, "Error contacting backend server.", 500)
revel.RevelLog.Error("Error dialing websocket backend ", "host", host, "error", err)
utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err)
return
}
hj, ok := w.(http.Hijacker)
@@ -256,21 +337,21 @@ func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
}
nc, _, err := hj.Hijack()
if err != nil {
revel.RevelLog.Error("Hijack error", "error", err)
utils.Logger.Error("Hijack error", "error", err)
return
}
defer func() {
if err = nc.Close(); err != nil {
revel.RevelLog.Error("Connection close error", "error", err)
utils.Logger.Error("Connection close error", "error", err)
}
if err = d.Close(); err != nil {
revel.RevelLog.Error("Dial close error", "error", err)
utils.Logger.Error("Dial close error", "error", err)
}
}()
err = r.Write(d)
if err != nil {
revel.RevelLog.Error("Error copying request to target", "error", err)
utils.Logger.Error("Error copying request to target", "error", err)
return
}

View File

@@ -1,851 +0,0 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package harness
// This file handles the app code introspection.
// It catalogs the controllers, their methods, and their arguments.
import (
"go/ast"
"go/build"
"go/parser"
"go/scanner"
"go/token"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/revel/revel"
"log"
)
// SourceInfo is the top-level struct containing all extracted information
// about the app source code, used to generate main.go.
type SourceInfo struct {
// StructSpecs lists type info for all structs found under the code paths.
// They may be queried to determine which ones (transitively) embed certain types.
StructSpecs []*TypeInfo
// ValidationKeys provides a two-level lookup. The keys are:
// 1. The fully-qualified function name,
// e.g. "github.com/revel/examples/chat/app/controllers.(*Application).Action"
// 2. Within that func's file, the line number of the (overall) expression statement.
// e.g. the line returned from runtime.Caller()
// The result of the lookup the name of variable being validated.
ValidationKeys map[string]map[int]string
// A list of import paths.
// Revel notices files with an init() function and imports that package.
InitImportPaths []string
// controllerSpecs lists type info for all structs found under
// app/controllers/... that embed (directly or indirectly) revel.Controller
controllerSpecs []*TypeInfo
// testSuites list the types that constitute the set of application tests.
testSuites []*TypeInfo
}
// TypeInfo summarizes information about a struct type in the app source code.
type TypeInfo struct {
StructName string // e.g. "Application"
ImportPath string // e.g. "github.com/revel/examples/chat/app/controllers"
PackageName string // e.g. "controllers"
MethodSpecs []*MethodSpec
// Used internally to identify controllers that indirectly embed *revel.Controller.
embeddedTypes []*embeddedTypeName
}
// methodCall describes a call to c.Render(..)
// It documents the argument names used, in order to propagate them to RenderArgs.
type methodCall struct {
Path string // e.g. "myapp/app/controllers.(*Application).Action"
Line int
Names []string
}
// MethodSpec holds the information of one Method
type MethodSpec struct {
Name string // Name of the method, e.g. "Index"
Args []*MethodArg // Argument descriptors
RenderCalls []*methodCall // Descriptions of Render() invocations from this Method.
}
// MethodArg holds the information of one argument
type MethodArg struct {
Name string // Name of the argument.
TypeExpr TypeExpr // The name of the type, e.g. "int", "*pkg.UserType"
ImportPath string // If the arg is of an imported type, this is the import path.
}
type embeddedTypeName struct {
ImportPath, StructName string
}
// Maps a controller simple name (e.g. "Login") to the methods for which it is a
// receiver.
type methodMap map[string][]*MethodSpec
// ProcessSource parses the app controllers directory and
// returns a list of the controller types found.
// Otherwise CompileError if the parsing fails.
func ProcessSource(roots []string) (*SourceInfo, *revel.Error) {
var (
srcInfo *SourceInfo
compileError *revel.Error
)
for _, root := range roots {
rootImportPath := importPathFromPath(root)
if rootImportPath == "" {
revel.RevelLog.Warn("Skipping empty code path", "path", root)
continue
}
// Start walking the directory tree.
_ = revel.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
revel.RevelLog.Error("Error scanning app source:", "error", err)
return nil
}
if !info.IsDir() || info.Name() == "tmp" {
return nil
}
// Get the import path of the package.
pkgImportPath := rootImportPath
if root != path {
pkgImportPath = rootImportPath + "/" + filepath.ToSlash(path[len(root)+1:])
}
// Parse files within the path.
var pkgs map[string]*ast.Package
fset := token.NewFileSet()
pkgs, err = parser.ParseDir(fset, path, func(f os.FileInfo) bool {
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
}, 0)
if err != nil {
if errList, ok := err.(scanner.ErrorList); ok {
var pos = errList[0].Pos
compileError = &revel.Error{
SourceType: ".go source",
Title: "Go Compilation Error",
Path: pos.Filename,
Description: errList[0].Msg,
Line: pos.Line,
Column: pos.Column,
SourceLines: revel.MustReadLines(pos.Filename),
}
errorLink := revel.Config.StringDefault("error.link", "")
if errorLink != "" {
compileError.SetLink(errorLink)
}
return compileError
}
// This is exception, err alredy checked above. Here just a print
ast.Print(nil, err)
revel.RevelLog.Fatal("Failed to parse dir", "error", err)
}
// Skip "main" packages.
delete(pkgs, "main")
// If there is no code in this directory, skip it.
if len(pkgs) == 0 {
return nil
}
// Ignore packages that end with _test
for i := range pkgs {
if len(i) > 6 {
if string(i[len(i)-5:]) == "_test" {
delete(pkgs, i)
}
}
}
// There should be only one package in this directory.
if len(pkgs) > 1 {
for i := range pkgs {
println("Found package ", i)
}
revel.RevelLog.Error("Most unexpected! Multiple packages in a single directory:", "packages", pkgs)
}
var pkg *ast.Package
for _, v := range pkgs {
pkg = v
}
srcInfo = appendSourceInfo(srcInfo, processPackage(fset, pkgImportPath, path, pkg))
return nil
})
}
return srcInfo, compileError
}
func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo {
if srcInfo1 == nil {
return srcInfo2
}
srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...)
srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...)
for k, v := range srcInfo2.ValidationKeys {
if _, ok := srcInfo1.ValidationKeys[k]; ok {
revel.RevelLog.Warn("Key conflict when scanning validation calls:", "key", k)
continue
}
srcInfo1.ValidationKeys[k] = v
}
return srcInfo1
}
func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *SourceInfo {
var (
structSpecs []*TypeInfo
initImportPaths []string
methodSpecs = make(methodMap)
validationKeys = make(map[string]map[int]string)
scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") ||
strings.Contains(pkgImportPath, "/controllers/")
scanTests = strings.HasSuffix(pkgImportPath, "/tests") ||
strings.Contains(pkgImportPath, "/tests/")
)
// For each source file in the package...
log.Println("Exaiming files in path", pkgPath)
for _, file := range pkg.Files {
// Imports maps the package key to the full import path.
// e.g. import "sample/app/models" => "models": "sample/app/models"
imports := map[string]string{}
// For each declaration in the source file...
for _, decl := range file.Decls {
addImports(imports, decl, pkgPath)
if scanControllers {
// Match and add both structs and methods
structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset)
appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports)
} else if scanTests {
structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset)
}
// If this is a func... (ignore nil for external (non-Go) function)
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Body != nil {
// Scan it for validation calls
lineKeys := getValidationKeys(fset, funcDecl, imports)
if len(lineKeys) > 0 {
validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys
}
// Check if it's an init function.
if funcDecl.Name.Name == "init" {
initImportPaths = []string{pkgImportPath}
}
}
}
}
// Add the method specs to the struct specs.
for _, spec := range structSpecs {
spec.MethodSpecs = methodSpecs[spec.StructName]
}
return &SourceInfo{
StructSpecs: structSpecs,
ValidationKeys: validationKeys,
InitImportPaths: initImportPaths,
}
}
// getFuncName returns a name for this func or method declaration.
// e.g. "(*Application).SayHello" for a method, "SayHello" for a func.
func getFuncName(funcDecl *ast.FuncDecl) string {
prefix := ""
if funcDecl.Recv != nil {
recvType := funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
} else {
prefix = recvType.(*ast.Ident).Name
}
prefix += "."
}
return prefix + funcDecl.Name.Name
}
func addImports(imports map[string]string, decl ast.Decl, srcDir string) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.IMPORT {
return
}
for _, spec := range genDecl.Specs {
importSpec := spec.(*ast.ImportSpec)
var pkgAlias string
if importSpec.Name != nil {
pkgAlias = importSpec.Name.Name
if pkgAlias == "_" {
continue
}
}
quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\""
fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes
// If the package was not aliased (common case), we have to import it
// to see what the package name is.
// TODO: Can improve performance here a lot:
// 1. Do not import everything over and over again. Keep a cache.
// 2. Exempt the standard library; their directories always match the package name.
// 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly
if pkgAlias == "" {
pkg, err := build.Import(fullPath, srcDir, 0)
if err != nil {
// We expect this to happen for apps using reverse routing (since we
// have not yet generated the routes). Don't log that.
if !strings.HasSuffix(fullPath, "/app/routes") {
revel.RevelLog.Debug("Could not find import:", "path", fullPath)
}
continue
}
pkgAlias = pkg.Name
}
imports[pkgAlias] = fullPath
}
}
// If this Decl is a struct type definition, it is summarized and added to specs.
// Else, specs is returned unchanged.
func appendStruct(specs []*TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*TypeInfo {
// Filter out non-Struct type declarations.
spec, found := getStructTypeDecl(decl, fset)
if !found {
return specs
}
structType := spec.Type.(*ast.StructType)
// At this point we know it's a type declaration for a struct.
// Fill in the rest of the info by diving into the fields.
// Add it provisionally to the Controller list -- it's later filtered using field info.
controllerSpec := &TypeInfo{
StructName: spec.Name.Name,
ImportPath: pkgImportPath,
PackageName: pkg.Name,
}
for _, field := range structType.Fields.List {
// If field.Names is set, it's not an embedded type.
if field.Names != nil {
continue
}
// A direct "sub-type" has an ast.Field as either:
// Ident { "AppController" }
// SelectorExpr { "rev", "Controller" }
// Additionally, that can be wrapped by StarExprs.
fieldType := field.Type
pkgName, typeName := func() (string, string) {
// Drill through any StarExprs.
for {
if starExpr, ok := fieldType.(*ast.StarExpr); ok {
fieldType = starExpr.X
continue
}
break
}
// If the embedded type is in the same package, it's an Ident.
if ident, ok := fieldType.(*ast.Ident); ok {
return "", ident.Name
}
if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok {
if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok {
return pkgIdent.Name, selectorExpr.Sel.Name
}
}
return "", ""
}()
// If a typename wasn't found, skip it.
if typeName == "" {
continue
}
// Find the import path for this type.
// If it was referenced without a package name, use the current package import path.
// Else, look up the package's import path by name.
var importPath string
if pkgName == "" {
importPath = pkgImportPath
} else {
var ok bool
if importPath, ok = imports[pkgName]; !ok {
revel.RevelLog.Error("Failed to find import path for ", "package", pkgName, "type", typeName)
continue
}
}
controllerSpec.embeddedTypes = append(controllerSpec.embeddedTypes, &embeddedTypeName{
ImportPath: importPath,
StructName: typeName,
})
}
return append(specs, controllerSpec)
}
// If decl is a Method declaration, it is summarized and added to the array
// underneath its receiver type.
// e.g. "Login" => {MethodSpec, MethodSpec, ..}
func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) {
// Func declaration?
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok {
return
}
// Have a receiver?
if funcDecl.Recv == nil {
return
}
// Is it public?
if !funcDecl.Name.IsExported() {
return
}
// Does it return a Result?
if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 {
return
}
selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr)
if !ok {
return
}
if selExpr.Sel.Name != "Result" {
return
}
if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != revel.RevelImportPath {
return
}
method := &MethodSpec{
Name: funcDecl.Name.Name,
}
// Add a description of the arguments to the method.
for _, field := range funcDecl.Type.Params.List {
for _, name := range field.Names {
var importPath string
typeExpr := NewTypeExpr(pkgName, field.Type)
if !typeExpr.Valid {
revel.RevelLog.Warnf("Didn't understand argument '%s' of action %s. Ignoring.", name, getFuncName(funcDecl))
return // We didn't understand one of the args. Ignore this action.
}
// Local object
if typeExpr.PkgName == pkgName {
importPath = pkgImportPath
} else if typeExpr.PkgName != "" {
var ok bool
if importPath, ok = imports[typeExpr.PkgName]; !ok {
revel.RevelLog.Errorf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName(""))
}
}
method.Args = append(method.Args, &MethodArg{
Name: name.Name,
TypeExpr: typeExpr,
ImportPath: importPath,
})
}
}
// Add a description of the calls to Render from the method.
// Inspect every node (e.g. always return true).
method.RenderCalls = []*methodCall{}
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// Is it a function call?
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// Is it calling (*Controller).Render?
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// The type of the receiver is not easily available, so just store every
// call to any method called Render.
if selExpr.Sel.Name != "Render" {
return true
}
// Add this call's args to the renderArgs.
pos := fset.Position(callExpr.Lparen)
methodCall := &methodCall{
Line: pos.Line,
Names: []string{},
}
for _, arg := range callExpr.Args {
argIdent, ok := arg.(*ast.Ident)
if !ok {
continue
}
methodCall.Names = append(methodCall.Names, argIdent.Name)
}
method.RenderCalls = append(method.RenderCalls, methodCall)
return true
})
var recvTypeName string
var recvType = funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
recvTypeName = recvStarType.X.(*ast.Ident).Name
} else {
recvTypeName = recvType.(*ast.Ident).Name
}
mm[recvTypeName] = append(mm[recvTypeName], method)
}
// Scan app source code for calls to X.Y(), where X is of type *Validation.
//
// Recognize these scenarios:
// - "Y" = "Validation" and is a member of the receiver.
// (The common case for inline validation)
// - "X" is passed in to the func as a parameter.
// (For structs implementing Validated)
//
// The line number to which a validation call is attributed is that of the
// surrounding ExprStmt. This is so that it matches what runtime.Callers()
// reports.
//
// The end result is that we can set the default validation key for each call to
// be the same as the local variable.
func getValidationKeys(fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string {
var (
lineKeys = make(map[int]string)
// Check the func parameters and the receiver's members for the *revel.Validation type.
validationParam = getValidationParameter(funcDecl, imports)
)
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// e.g. c.Validation.Required(arg) or v.Required(arg)
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// e.g. c.Validation.Required or v.Required
funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
switch x := funcSelector.X.(type) {
case *ast.SelectorExpr: // e.g. c.Validation
if x.Sel.Name != "Validation" {
return true
}
case *ast.Ident: // e.g. v
if validationParam == nil || x.Obj != validationParam {
return true
}
default:
return true
}
if len(callExpr.Args) == 0 {
return true
}
// Given the validation expression, extract the key.
key := callExpr.Args[0]
switch expr := key.(type) {
case *ast.BinaryExpr:
// If the argument is a binary expression, take the first expression.
// (e.g. c.Validation.Required(myName != ""))
key = expr.X
case *ast.UnaryExpr:
// If the argument is a unary expression, drill in.
// (e.g. c.Validation.Required(!myBool)
key = expr.X
case *ast.BasicLit:
// If it's a literal, skip it.
return true
}
if typeExpr := NewTypeExpr("", key); typeExpr.Valid {
lineKeys[fset.Position(callExpr.Pos()).Line] = typeExpr.TypeName("")
}
return true
})
return lineKeys
}
// Check to see if there is a *revel.Validation as an argument.
func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object {
for _, field := range funcDecl.Type.Params.List {
starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation
if !ok {
continue
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation
if !ok {
continue
}
xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev
if !ok {
continue
}
if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == revel.RevelImportPath {
return field.Names[0].Obj
}
}
return nil
}
func (s *TypeInfo) String() string {
return s.ImportPath + "." + s.StructName
}
func (s *embeddedTypeName) String() string {
return s.ImportPath + "." + s.StructName
}
// getStructTypeDecl checks if the given decl is a type declaration for a
// struct. If so, the TypeSpec is returned.
func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.TYPE {
return
}
if len(genDecl.Specs) == 0 {
revel.RevelLog.Warnf("Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
return
}
spec = genDecl.Specs[0].(*ast.TypeSpec)
_, found = spec.Type.(*ast.StructType)
return
}
// TypesThatEmbed returns all types that (directly or indirectly) embed the
// target type, which must be a fully qualified type name,
// e.g. "github.com/revel/revel.Controller"
func (s *SourceInfo) TypesThatEmbed(targetType, packageFilter string) (filtered []*TypeInfo) {
// Do a search in the "embedded type graph", starting with the target type.
var (
nodeQueue = []string{targetType}
processed []string
)
for len(nodeQueue) > 0 {
controllerSimpleName := nodeQueue[0]
nodeQueue = nodeQueue[1:]
processed = append(processed, controllerSimpleName)
// Look through all known structs.
for _, spec := range s.StructSpecs {
// If this one has been processed or is already in nodeQueue, then skip it.
if revel.ContainsString(processed, spec.String()) ||
revel.ContainsString(nodeQueue, spec.String()) {
continue
}
// Look through the embedded types to see if the current type is among them.
for _, embeddedType := range spec.embeddedTypes {
// If so, add this type's simple name to the nodeQueue, and its spec to
// the filtered list.
if controllerSimpleName == embeddedType.String() {
nodeQueue = append(nodeQueue, spec.String())
filtered = append(filtered, spec)
break
}
}
}
}
// Strip out any specifications that contain a lower case
for exit := false; !exit; exit = true {
for i, filteredItem := range filtered {
if unicode.IsLower([]rune(filteredItem.StructName)[0]) {
revel.RevelLog.Debug("Skipping adding spec for unexported type",
"type", filteredItem.StructName,
"package", filteredItem.ImportPath)
filtered = append(filtered[:i], filtered[i+1:]...)
exit = false
break
}
}
}
// Check for any missed types that where from expected packages
for _, spec := range s.StructSpecs {
if spec.PackageName == packageFilter {
found := false
for _, filteredItem := range filtered {
if filteredItem.StructName == spec.StructName {
found = true
break
}
}
if !found {
revel.RevelLog.Warn("Type found in package: "+packageFilter+
", but did not embed from: "+filepath.Base(targetType),
"name", spec.StructName, "path", spec.ImportPath)
}
}
}
return
}
// ControllerSpecs returns the all the contollers that embeds
// `revel.Controller`
func (s *SourceInfo) ControllerSpecs() []*TypeInfo {
if s.controllerSpecs == nil {
s.controllerSpecs = s.TypesThatEmbed(revel.RevelImportPath+".Controller", "controllers")
}
return s.controllerSpecs
}
// TestSuites returns the all the Application tests that embeds
// `testing.TestSuite`
func (s *SourceInfo) TestSuites() []*TypeInfo {
if s.testSuites == nil {
s.testSuites = s.TypesThatEmbed(revel.RevelImportPath+"/testing.TestSuite", "testsuite")
}
return s.testSuites
}
// TypeExpr provides a type name that may be rewritten to use a package name.
type TypeExpr struct {
Expr string // The unqualified type expression, e.g. "[]*MyType"
PkgName string // The default package idenifier
pkgIndex int // The index where the package identifier should be inserted.
Valid bool
}
// TypeName returns the fully-qualified type name for this expression.
// The caller may optionally specify a package name to override the default.
func (e TypeExpr) TypeName(pkgOverride string) string {
pkgName := revel.FirstNonEmpty(pkgOverride, e.PkgName)
if pkgName == "" {
return e.Expr
}
return e.Expr[:e.pkgIndex] + pkgName + "." + e.Expr[e.pkgIndex:]
}
// NewTypeExpr returns the syntactic expression for referencing this type in Go.
func NewTypeExpr(pkgName string, expr ast.Expr) TypeExpr {
switch t := expr.(type) {
case *ast.Ident:
if IsBuiltinType(t.Name) {
pkgName = ""
}
return TypeExpr{t.Name, pkgName, 0, true}
case *ast.SelectorExpr:
e := NewTypeExpr(pkgName, t.X)
return TypeExpr{t.Sel.Name, e.Expr, 0, e.Valid}
case *ast.StarExpr:
e := NewTypeExpr(pkgName, t.X)
return TypeExpr{"*" + e.Expr, e.PkgName, e.pkgIndex + 1, e.Valid}
case *ast.ArrayType:
e := NewTypeExpr(pkgName, t.Elt)
return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid}
case *ast.MapType:
if identKey, ok := t.Key.(*ast.Ident); ok && IsBuiltinType(identKey.Name) {
e := NewTypeExpr(pkgName, t.Value)
return TypeExpr{"map[" + identKey.Name + "]" + e.Expr, e.PkgName, e.pkgIndex + len("map["+identKey.Name+"]"), e.Valid}
}
revel.RevelLog.Error("Failed to generate name for field. Make sure the field name is valid.")
case *ast.Ellipsis:
e := NewTypeExpr(pkgName, t.Elt)
return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid}
default:
revel.RevelLog.Error("Failed to generate name for field. Make sure the field name is valid.", "package", pkgName, "expresion",expr)
}
return TypeExpr{Valid: false}
}
var builtInTypes = map[string]struct{}{
"bool": {},
"byte": {},
"complex128": {},
"complex64": {},
"error": {},
"float32": {},
"float64": {},
"int": {},
"int16": {},
"int32": {},
"int64": {},
"int8": {},
"rune": {},
"string": {},
"uint": {},
"uint16": {},
"uint32": {},
"uint64": {},
"uint8": {},
"uintptr": {},
}
// IsBuiltinType checks the given type is built-in types of Go
func IsBuiltinType(name string) bool {
_, ok := builtInTypes[name]
return ok
}
func importPathFromPath(root string) string {
if vendorIdx := strings.Index(root, "/vendor/"); vendorIdx != -1 {
return filepath.ToSlash(root[vendorIdx+8:])
}
for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
srcPath := filepath.Join(gopath, "src")
if strings.HasPrefix(root, srcPath) {
return filepath.ToSlash(root[len(srcPath)+1:])
}
}
srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg")
if strings.HasPrefix(root, srcPath) {
revel.RevelLog.Warn("Code path should be in GOPATH, but is in GOROOT:", "path", root)
return filepath.ToSlash(root[len(srcPath)+1:])
}
revel.RevelLog.Error("Unexpected! Code path is not in GOPATH:", "path", root)
return ""
}

View File

@@ -1,195 +0,0 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package harness
import (
"go/ast"
"go/parser"
"go/token"
"reflect"
"strings"
"testing"
"github.com/revel/revel"
)
const validationKeysSource = `
package test
func (c *Application) testFunc(a, b int, user models.User) revel.Result {
// Line 5
c.Validation.Required(a)
c.Validation.Required(a).Message("Error message")
c.Validation.Required(a).
Message("Error message")
// Line 11
c.Validation.Required(user.Name)
c.Validation.Required(user.Name).Message("Error message")
// Line 15
c.Validation.MinSize(b, 12)
c.Validation.MinSize(b, 12).Message("Error message")
c.Validation.MinSize(b,
12)
// Line 21
c.Validation.Required(b == 5)
}
func (m Model) Validate(v *revel.Validation) {
// Line 26
v.Required(m.name)
v.Required(m.name == "something").
Message("Error Message")
v.Required(!m.bool)
}
`
var expectedValidationKeys = []map[int]string{
{
6: "a",
7: "a",
8: "a",
12: "user.Name",
13: "user.Name",
16: "b",
17: "b",
19: "b",
22: "b",
}, {
27: "m.name",
28: "m.name",
30: "m.bool",
},
}
// This tests the recording of line number to validation key of the preceeding
// example source.
func TestGetValidationKeys(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "validationKeysSource", validationKeysSource, 0)
if err != nil {
t.Fatal(err)
}
if len(file.Decls) != 2 {
t.Fatal("Expected 2 decl in the source, found", len(file.Decls))
}
for i, decl := range file.Decls {
lineKeys := getValidationKeys(fset, decl.(*ast.FuncDecl), map[string]string{"revel": revel.RevelImportPath})
for k, v := range expectedValidationKeys[i] {
if lineKeys[k] != v {
t.Errorf("Not found - %d: %v - Actual Map: %v", k, v, lineKeys)
}
}
if len(lineKeys) != len(expectedValidationKeys[i]) {
t.Error("Validation key map not the same size as expected:", lineKeys)
}
}
}
var TypeExprs = map[string]TypeExpr{
"int": {"int", "", 0, true},
"*int": {"*int", "", 1, true},
"[]int": {"[]int", "", 2, true},
"...int": {"[]int", "", 2, true},
"[]*int": {"[]*int", "", 3, true},
"...*int": {"[]*int", "", 3, true},
"MyType": {"MyType", "pkg", 0, true},
"*MyType": {"*MyType", "pkg", 1, true},
"[]MyType": {"[]MyType", "pkg", 2, true},
"...MyType": {"[]MyType", "pkg", 2, true},
"[]*MyType": {"[]*MyType", "pkg", 3, true},
"...*MyType": {"[]*MyType", "pkg", 3, true},
"map[int]MyType": {"map[int]MyType", "pkg", 8, true},
"map[int]*MyType": {"map[int]*MyType", "pkg", 9, true},
}
func TestTypeExpr(t *testing.T) {
for typeStr, expected := range TypeExprs {
// Handle arrays and ... myself, since ParseExpr() does not.
array := strings.HasPrefix(typeStr, "[]")
if array {
typeStr = typeStr[2:]
}
ellipsis := strings.HasPrefix(typeStr, "...")
if ellipsis {
typeStr = typeStr[3:]
}
expr, err := parser.ParseExpr(typeStr)
if err != nil {
t.Error("Failed to parse test expr:", typeStr)
continue
}
if array {
expr = &ast.ArrayType{Lbrack: expr.Pos(), Len: nil, Elt: expr}
}
if ellipsis {
expr = &ast.Ellipsis{Ellipsis: expr.Pos(), Elt: expr}
}
actual := NewTypeExpr("pkg", expr)
if !reflect.DeepEqual(expected, actual) {
t.Error("Fail, expected", expected, ", was", actual)
}
}
}
func TestProcessBookingSource(t *testing.T) {
revel.Init("prod", "github.com/revel/examples/booking", "")
sourceInfo, err := ProcessSource([]string{revel.AppPath})
if err != nil {
t.Fatal("Failed to process booking source with error:", err)
}
controllerPackage := "github.com/revel/examples/booking/app/controllers"
expectedControllerSpecs := []*TypeInfo{
{"GorpController", controllerPackage, "controllers", nil, nil},
{"Application", controllerPackage, "controllers", nil, nil},
{"Hotels", controllerPackage, "controllers", nil, nil},
}
if len(sourceInfo.ControllerSpecs()) != len(expectedControllerSpecs) {
t.Errorf("Unexpected number of controllers found. Expected %d, Found %d",
len(expectedControllerSpecs), len(sourceInfo.ControllerSpecs()))
}
NEXT_TEST:
for _, expected := range expectedControllerSpecs {
for _, actual := range sourceInfo.ControllerSpecs() {
if actual.StructName == expected.StructName {
if actual.ImportPath != expected.ImportPath {
t.Errorf("%s expected to have import path %s, actual %s",
actual.StructName, expected.ImportPath, actual.ImportPath)
}
if actual.PackageName != expected.PackageName {
t.Errorf("%s expected to have package name %s, actual %s",
actual.StructName, expected.PackageName, actual.PackageName)
}
continue NEXT_TEST
}
}
t.Errorf("Expected to find controller %s, but did not. Actuals: %s",
expected.StructName, sourceInfo.ControllerSpecs())
}
}
func BenchmarkProcessBookingSource(b *testing.B) {
revel.Init("", "github.com/revel/examples/booking", "")
revel.GetRootLogHandler().Disable()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := ProcessSource(revel.CodePaths)
if err != nil {
b.Error("Unexpected error:", err)
}
}
}