diff --git a/LICENSE b/LICENSE index 93cd605..92246e3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (C) 2012-2016 The Revel Framework Authors. +Copyright (C) 2012-2018 The Revel Framework Authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/harness/app.go b/harness/app.go index 5423f8c..1bab46c 100644 --- a/harness/app.go +++ b/harness/app.go @@ -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) } diff --git a/harness/build.go b/harness/build.go index 295c1ea..7e91324 100644 --- a/harness/build.go +++ b/harness/build.go @@ -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" diff --git a/harness/harness.go b/harness/harness.go index 1c9be43..5e6456d 100644 --- a/harness/harness.go +++ b/harness/harness.go @@ -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 } diff --git a/logger/doc.go b/logger/doc.go new file mode 100644 index 0000000..4392734 --- /dev/null +++ b/logger/doc.go @@ -0,0 +1,11 @@ + +/* + Package logger contains filters and handles for the logging utilities in Revel. + These facilities all currently use the logging library called log15 at + https://github.com/inconshreveable/log15 + + Wrappers for the handlers are written here to provide a kind of isolation layer for Revel + in case sometime in the future we would like to switch to another source to implement logging + + */ +package logger diff --git a/logger/format.go b/logger/format.go new file mode 100644 index 0000000..e5276f1 --- /dev/null +++ b/logger/format.go @@ -0,0 +1,206 @@ +package logger + +import ( + "bytes" + "fmt" + "github.com/revel/log15" + "reflect" + "strconv" + "sync" + "time" +) + +const ( + timeFormat = "2006-01-02T15:04:05-0700" + termTimeFormat = "2006/01/02 15:04:05" + termSmallTimeFormat = "15:04:05" + floatFormat = 'f' + errorKey = "REVEL_ERROR" +) + +var ( + // Name the log level + toRevel = map[log15.Lvl]string{log15.LvlDebug: "DEBUG", + log15.LvlInfo: "INFO", log15.LvlWarn: "WARN", log15.LvlError: "ERROR", log15.LvlCrit: "CRIT"} +) + +// Outputs to the terminal in a format like below +// INFO 09:11:32 server-engine.go:169: Request Stats +func TerminalFormatHandler(noColor bool, smallDate bool) LogFormat { + dateFormat := termTimeFormat + if smallDate { + dateFormat = termSmallTimeFormat + } + return log15.FormatFunc(func(r *log15.Record) []byte { + // Bash coloring http://misc.flogisoft.com/bash/tip_colors_and_formatting + var color = 0 + switch r.Lvl { + case log15.LvlCrit: + // Magenta + color = 35 + case log15.LvlError: + // Red + color = 31 + case log15.LvlWarn: + // Yellow + color = 33 + case log15.LvlInfo: + // Green + color = 32 + case log15.LvlDebug: + // Cyan + color = 36 + } + + b := &bytes.Buffer{} + caller := findInContext("caller", r.Ctx) + module := findInContext("module", r.Ctx) + if noColor == false && color > 0 { + if len(module) > 0 { + fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %6s %13s: %-40s ", color, toRevel[r.Lvl], r.Time.Format(dateFormat), module, caller, r.Msg) + } else { + fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %13s: %-40s ", color, toRevel[r.Lvl], r.Time.Format(dateFormat), caller, r.Msg) + } + } else { + fmt.Fprintf(b, "%-5s %s %6s %13s: %-40s", toRevel[r.Lvl], r.Time.Format(dateFormat), module, caller, r.Msg) + } + + for i := 0; i < len(r.Ctx); i += 2 { + if i != 0 { + b.WriteByte(' ') + } + + k, ok := r.Ctx[i].(string) + if k == "caller" || k == "fn" || k == "module" { + continue + } + v := formatLogfmtValue(r.Ctx[i+1]) + if !ok { + k, v = errorKey, formatLogfmtValue(k) + } + + // TODO: we should probably check that all of your key bytes aren't invalid + if noColor == false && color > 0 { + fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m=%s", color, k, v) + } else { + b.WriteString(k) + b.WriteByte('=') + b.WriteString(v) + } + } + + b.WriteByte('\n') + + return b.Bytes() + }) +} +func findInContext(key string, ctx []interface{}) string { + for i := 0; i < len(ctx); i += 2 { + k := ctx[i].(string) + if key == k { + return formatLogfmtValue(ctx[i+1]) + } + } + return "" +} + +// formatValue formats a value for serialization +func formatLogfmtValue(value interface{}) string { + if value == nil { + return "nil" + } + + if t, ok := value.(time.Time); ok { + // Performance optimization: No need for escaping since the provided + // timeFormat doesn't have any escape characters, and escaping is + // expensive. + return t.Format(termTimeFormat) + } + value = formatShared(value) + switch v := value.(type) { + case bool: + return strconv.FormatBool(v) + case float32: + return strconv.FormatFloat(float64(v), floatFormat, 3, 64) + case float64: + return strconv.FormatFloat(v, floatFormat, 7, 64) + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return fmt.Sprintf("%d", value) + case string: + return escapeString(v) + default: + return escapeString(fmt.Sprintf("%+v", value)) + } +} +func formatShared(value interface{}) (result interface{}) { + defer func() { + if err := recover(); err != nil { + if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { + result = "nil" + } else { + panic(err) + } + } + }() + + switch v := value.(type) { + case time.Time: + return v.Format(timeFormat) + + case error: + return v.Error() + + case fmt.Stringer: + return v.String() + + default: + return v + } +} + +var stringBufPool = sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, +} + +func escapeString(s string) string { + needsQuotes := false + needsEscape := false + for _, r := range s { + if r <= ' ' || r == '=' || r == '"' { + needsQuotes = true + } + if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { + needsEscape = true + } + } + if needsEscape == false && needsQuotes == false { + return s + } + e := stringBufPool.Get().(*bytes.Buffer) + e.WriteByte('"') + for _, r := range s { + switch r { + case '\\', '"': + e.WriteByte('\\') + e.WriteByte(byte(r)) + case '\n': + e.WriteString("\\n") + case '\r': + e.WriteString("\\r") + case '\t': + e.WriteString("\\t") + default: + e.WriteRune(r) + } + } + e.WriteByte('"') + var ret string + if needsQuotes { + ret = e.String() + } else { + ret = string(e.Bytes()[1 : e.Len()-1]) + } + e.Reset() + stringBufPool.Put(e) + return ret +} diff --git a/logger/handlers.go b/logger/handlers.go new file mode 100644 index 0000000..e60a626 --- /dev/null +++ b/logger/handlers.go @@ -0,0 +1,350 @@ +package logger + +import ( + "io" + "os" + + colorable "github.com/mattn/go-colorable" + "github.com/revel/log15" + "gopkg.in/natefinch/lumberjack.v2" +) + +// Filters out records which do not match the level +// Uses the `log15.FilterHandler` to perform this task +func LevelHandler(lvl LogLevel, h LogHandler) LogHandler { + l15Lvl := log15.Lvl(lvl) + return log15.FilterHandler(func(r *log15.Record) (pass bool) { + return r.Lvl == l15Lvl + }, h) +} + +// Filters out records which do not match the level +// Uses the `log15.FilterHandler` to perform this task +func MinLevelHandler(lvl LogLevel, h LogHandler) LogHandler { + l15Lvl := log15.Lvl(lvl) + return log15.FilterHandler(func(r *log15.Record) (pass bool) { + return r.Lvl <= l15Lvl + }, h) +} + +// Filters out records which match the level +// Uses the `log15.FilterHandler` to perform this task +func NotLevelHandler(lvl LogLevel, h LogHandler) LogHandler { + l15Lvl := log15.Lvl(lvl) + return log15.FilterHandler(func(r *log15.Record) (pass bool) { + return r.Lvl != l15Lvl + }, h) +} + +// Adds in a context called `caller` to the record (contains file name and line number like `foo.go:12`) +// Uses the `log15.CallerFileHandler` to perform this task +func CallerFileHandler(h LogHandler) LogHandler { + return log15.CallerFileHandler(h) +} + +// Adds in a context called `caller` to the record (contains file name and line number like `foo.go:12`) +// Uses the `log15.CallerFuncHandler` to perform this task +func CallerFuncHandler(h LogHandler) LogHandler { + return log15.CallerFuncHandler(h) +} + +// Filters out records which match the key value pair +// Uses the `log15.MatchFilterHandler` to perform this task +func MatchHandler(key string, value interface{}, h LogHandler) LogHandler { + return log15.MatchFilterHandler(key, value, h) +} + +// If match then A handler is called otherwise B handler is called +func MatchAbHandler(key string, value interface{}, a, b LogHandler) LogHandler { + return log15.FuncHandler(func(r *log15.Record) error { + for i := 0; i < len(r.Ctx); i += 2 { + if r.Ctx[i] == key { + if r.Ctx[i+1] == value { + if a != nil { + return a.Log(r) + } + return nil + } + } + } + if b != nil { + return b.Log(r) + } + return nil + }) +} + +// The nil handler is used if logging for a specific request needs to be turned off +func NilHandler() LogHandler { + return log15.FuncHandler(func(r *log15.Record) error { + return nil + }) +} + +// Match all values in map to log +func MatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler { + return matchMapHandler(matchMap, false, a) +} + +// Match !(Match all values in map to log) The inverse of MatchMapHandler +func NotMatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler { + return matchMapHandler(matchMap, true, a) +} + +// Rather then chaining multiple filter handlers, process all here +func matchMapHandler(matchMap map[string]interface{}, inverse bool, a LogHandler) LogHandler { + return log15.FuncHandler(func(r *log15.Record) error { + checkMap := map[string]bool{} + // Copy the map to a bool + for i := 0; i < len(r.Ctx); i += 2 { + if value, found := matchMap[r.Ctx[i].(string)]; found && value == r.Ctx[i+1] { + checkMap[r.Ctx[i].(string)] = true + } + } + if len(checkMap) == len(matchMap) { + if !inverse { + return a.Log(r) + } + } else if inverse { + return a.Log(r) + } + return nil + }) +} + +// Filters out records which do not match the key value pair +// Uses the `log15.FilterHandler` to perform this task +func NotMatchHandler(key string, value interface{}, h LogHandler) LogHandler { + return log15.FilterHandler(func(r *log15.Record) (pass bool) { + switch key { + case r.KeyNames.Lvl: + return r.Lvl != value + case r.KeyNames.Time: + return r.Time != value + case r.KeyNames.Msg: + return r.Msg != value + } + + for i := 0; i < len(r.Ctx); i += 2 { + if r.Ctx[i] == key { + return r.Ctx[i+1] == value + } + } + return true + }, h) +} + +func MultiHandler(hs ...LogHandler) LogHandler { + // Convert the log handlers to log15.Handlers + handlers := []log15.Handler{} + for _, h := range hs { + if h != nil { + handlers = append(handlers, h) + } + } + + return log15.MultiHandler(handlers...) +} + +// Outputs the records to the passed in stream +// Uses the `log15.StreamHandler` to perform this task +func StreamHandler(wr io.Writer, fmtr LogFormat) LogHandler { + return log15.StreamHandler(wr, fmtr) +} + +// Filter handler, this is the only +// Uses the `log15.FilterHandler` to perform this task +func FilterHandler(fn func(r *log15.Record) bool, h LogHandler) LogHandler { + return log15.FilterHandler(fn, h) +} + +type ListLogHandler struct { + handlers []LogHandler +} + +func NewListLogHandler(h1, h2 LogHandler) *ListLogHandler { + ll := &ListLogHandler{handlers: []LogHandler{h1, h2}} + return ll +} +func (ll *ListLogHandler) Log(r *log15.Record) (err error) { + for _, handler := range ll.handlers { + if err == nil { + err = handler.Log(r) + } else { + handler.Log(r) + } + } + return +} +func (ll *ListLogHandler) Add(h LogHandler) { + if h != nil { + ll.handlers = append(ll.handlers, h) + } +} +func (ll *ListLogHandler) Del(h LogHandler) { + if h != nil { + for i, handler := range ll.handlers { + if handler == h { + ll.handlers = append(ll.handlers[:i], ll.handlers[i+1:]...) + } + } + } +} + +type CompositeMultiHandler struct { + DebugHandler LogHandler + InfoHandler LogHandler + WarnHandler LogHandler + ErrorHandler LogHandler + CriticalHandler LogHandler +} + +func NewCompositeMultiHandler() (*CompositeMultiHandler, LogHandler) { + cw := &CompositeMultiHandler{} + return cw, cw +} +func (h *CompositeMultiHandler) Log(r *log15.Record) (err error) { + + var handler LogHandler + switch r.Lvl { + case log15.LvlInfo: + handler = h.InfoHandler + case log15.LvlDebug: + handler = h.DebugHandler + case log15.LvlWarn: + handler = h.WarnHandler + case log15.LvlError: + handler = h.ErrorHandler + case log15.LvlCrit: + handler = h.CriticalHandler + } + + // Embed the caller function in the context + if handler != nil { + handler.Log(r) + } + return +} +func (h *CompositeMultiHandler) SetHandler(handler LogHandler, replace bool, level LogLevel) { + if handler == nil { + // Ignore empty handler + return + } + source := &h.DebugHandler + switch level { + case LvlDebug: + source = &h.DebugHandler + case LvlInfo: + source = &h.InfoHandler + case LvlWarn: + source = &h.WarnHandler + case LvlError: + source = &h.ErrorHandler + case LvlCrit: + source = &h.CriticalHandler + } + + if !replace && *source != nil { + // If this already was a list add a new logger to it + if ll, found := (*source).(*ListLogHandler); found { + ll.Add(handler) + } else { + *source = NewListLogHandler(*source, handler) + } + } else { + *source = handler + } +} + +func (h *CompositeMultiHandler) SetHandlers(handler LogHandler, options *LogOptions) { + if len(options.Levels) == 0 { + options.Levels = LvlAllList + } + // Set all levels + for _, lvl := range options.Levels { + h.SetHandler(handler, options.ReplaceExistingHandler, lvl) + } + +} +func (h *CompositeMultiHandler) SetJson(writer io.Writer, options *LogOptions) { + handler := CallerFileHandler(StreamHandler(writer, log15.JsonFormatEx( + options.GetBoolDefault("pretty", false), + options.GetBoolDefault("lineSeparated", true), + ))) + if options.HandlerWrap != nil { + handler = options.HandlerWrap.SetChild(handler) + } + h.SetHandlers(handler, options) +} + +// Use built in rolling function +func (h *CompositeMultiHandler) SetJsonFile(filePath string, options *LogOptions) { + writer := &lumberjack.Logger{ + Filename: filePath, + MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes + MaxAge: options.GetIntDefault("maxAgeDays", 7), //days + MaxBackups: options.GetIntDefault("maxBackups", 7), + Compress: options.GetBoolDefault("compress", true), + } + h.SetJson(writer, options) +} + +func (h *CompositeMultiHandler) SetTerminal(writer io.Writer, options *LogOptions) { + streamHandler := StreamHandler( + writer, + TerminalFormatHandler( + options.GetBoolDefault("noColor", false), + options.GetBoolDefault("smallDate", true))) + + if os.Stdout == writer { + streamHandler = StreamHandler( + colorable.NewColorableStdout(), + TerminalFormatHandler( + options.GetBoolDefault("noColor", false), + options.GetBoolDefault("smallDate", true))) + } else if os.Stderr == writer { + streamHandler = StreamHandler( + colorable.NewColorableStderr(), + TerminalFormatHandler( + options.GetBoolDefault("noColor", false), + options.GetBoolDefault("smallDate", true))) + } + + handler := CallerFileHandler(streamHandler) + if options.HandlerWrap != nil { + handler = options.HandlerWrap.SetChild(handler) + } + h.SetHandlers(handler, options) +} + +// Use built in rolling function +func (h *CompositeMultiHandler) SetTerminalFile(filePath string, options *LogOptions) { + writer := &lumberjack.Logger{ + Filename: filePath, + MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes + MaxAge: options.GetIntDefault("maxAgeDays", 7), //days + MaxBackups: options.GetIntDefault("maxBackups", 7), + Compress: options.GetBoolDefault("compress", true), + } + h.SetTerminal(writer, options) +} + +func (h *CompositeMultiHandler) Disable(levels ...LogLevel) { + if len(levels) == 0 { + levels = LvlAllList + } + for _, level := range levels { + switch level { + case LvlDebug: + h.DebugHandler = nil + case LvlInfo: + h.InfoHandler = nil + case LvlWarn: + h.WarnHandler = nil + case LvlError: + h.ErrorHandler = nil + case LvlCrit: + h.CriticalHandler = nil + } + } +} diff --git a/logger/log.go.old b/logger/log.go.old new file mode 100644 index 0000000..3a65852 --- /dev/null +++ b/logger/log.go.old @@ -0,0 +1,656 @@ +package logger + + +// LoggedError is wrapper to differentiate logged panics from unexpected ones. +import ( + "os" + "fmt" + "strings" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "sync" + "go.uber.org/zap/buffer" + "time" + "encoding/base64" + "unicode/utf8" + "encoding/json" + "math" + + "io" +) + +type ( + MultiLogger interface { + //log15.Logger + //// New returns a new Logger that has this logger's context plus the given context + New(ctx ...interface{}) MultiLogger + // + // The encoders job is to encode the + SetHandler(h LogHandler) + SetStackDepth(int) MultiLogger + // + //// Log a message at the given level with context key/value pairs + Debug(msg string, ctx ...interface{}) + Debugf(msg string, params ...interface{}) + Info(msg string, ctx ...interface{}) + Infof(msg string, params ...interface{}) + Warn(msg string, ctx ...interface{}) + Warnf(msg string, params ...interface{}) + Error(msg string, ctx ...interface{}) + Errorf(msg string, params ...interface{}) + Crit(msg string, ctx ...interface{}) + Critf(msg string, params ...interface{}) + + //// Logs a message as an Crit and exits + Fatal(msg string, ctx ...interface{}) + Fatalf(msg string, params ...interface{}) + //// Logs a message as an Crit and panics + Panic(msg string, ctx ...interface{}) + Panicf(msg string, params ...interface{}) + } + + // The log han + LogHandler interface { + Encode(Record) ([]byte, error) + GetLevel() Level + GetWriter() io.Writer + } + + // The Record + Record struct { + Level Level + Time time.Time + LoggerName string + Message string + Caller EntryCaller + Stack string + Context []Field +} + + // The fields passed in + Field interface { + GetKey() string + GetValueAsString() string + GetValue() interface{} + } + + EntryCaller interface { + IsDefined() bool + GetPC() uintptr + GetFile() string + GetLine() int + } + + // Called only if the logger needs + ResolveLaterLogger func() interface{} + + FieldType int + Level int +) + +type ( + zapLogger struct { + logger *zap.SugaredLogger + coreList []*zapcore.Core + } + zapField struct { + Key string + Type FieldType + Integer int64 + String string + Interface interface{} + } + zapEntryCaller struct { + Defined bool + PC uintptr + File string + Line int + } + zapEncoder struct { + lh LogHandler + } +) + +func newLogger(addCaller bool) MultiLogger { + logger := zap.New(nil).WithOptions(zap.AddCaller()) + l := &zapLogger{logger:logger.Sugar()} + + return l +} + +// It is up to the handler to determine the synchronization to the output +// streams +func (z *zapLogger) SetHandler(lh LogHandler) { + // Swap out the logger when a new handler is attached + encoder := &zapEncoder{lh} + levelHandler := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= zapcore.Level(lh.GetLevel()) + }) + logger := zap.New(zapcore.NewCore(encoder, nil, levelHandler)).WithOptions(zap.AddCaller()) + Logger.With("foo","bar").Desugar().Core() +} + +var Logger *zap.SugaredLogger + +func InitLogger(logLevel zapcore.Level) { + config :=zap.NewDevelopmentEncoderConfig() + config.EncodeLevel = zapcore.CapitalColorLevelEncoder + + consoleEncoder := NewConsoleEncoder(config) + lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= logLevel + }) + + consoleDebugging := zapcore.Lock(os.Stdout) + core := zapcore.NewTee( + zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), + ) + logger := zap.New(core).WithOptions(zap.AddCaller()) + Logger = logger.Sugar() +} +type LoggedError struct{ error } + +func NewLoggedError(err error) *LoggedError { + return &LoggedError{err} +} + +func Errorf(format string, args ...interface{}) { + // Ensure the user's command prompt starts on the next line. + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + fmt.Fprintf(os.Stderr, format, args...) + panic(format) // Panic instead of os.Exit so that deferred will run. +} + + + +// This is all for the Console logger - a little wordy but it works + +var _sliceEncoderPool = sync.Pool{ + New: func() interface{} { + return &sliceArrayEncoder{elems: make([]interface{}, 0, 2)} + }, +} + +func getSliceEncoder() *sliceArrayEncoder { + return _sliceEncoderPool.Get().(*sliceArrayEncoder) +} + +func putSliceEncoder(e *sliceArrayEncoder) { + e.elems = e.elems[:0] + _sliceEncoderPool.Put(e) +} + +type consoleEncoder struct { + *zapcore.EncoderConfig + openNamespaces int + buf *buffer.Buffer + reflectBuf *buffer.Buffer + reflectEnc *json.Encoder +} + +var ( + _pool = buffer.NewPool() + // Get retrieves a buffer from the pool, creating one if necessary. + Get = _pool.Get +) + + +// NewConsoleEncoder creates an encoder whose output is designed for human - +// rather than machine - consumption. It serializes the core log entry data +// (message, level, timestamp, etc.) in a plain-text format and leaves the +// structured context as JSON. +// +// Note that although the console encoder doesn't use the keys specified in the +// encoder configuration, it will omit any element whose key is set to the empty +// string. +func NewConsoleEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder { + ec := &consoleEncoder{buf : Get(), reflectBuf: Get()} + ec.EncoderConfig = &cfg + return ec +} + +func (c consoleEncoder) Clone() zapcore.Encoder { + return &consoleEncoder{buf : Get(), reflectBuf: Get()} +} + +func (c consoleEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + line := Get() + + var color = 0 + switch ent.Level { + case zap.PanicLevel: + // Magenta + color = 35 + case zap.ErrorLevel: + // Red + color = 31 + case zap.WarnLevel: + // Yellow + color = 33 + case zap.InfoLevel: + // Green + color = 32 + case zap.DebugLevel: + // Cyan + color = 36 + } + + // We don't want the entry's metadata to be quoted and escaped (if it's + // encoded as strings), which means that we can't use the JSON encoder. The + // simplest option is to use the memory encoder and fmt.Fprint. + // + // If this ever becomes a performance bottleneck, we can implement + // ArrayEncoder for our plain-text format. + arr := getSliceEncoder() + if c.LevelKey != "" && c.EncodeLevel != nil { + arr.AppendString(fmt.Sprintf("\x1b[%dm%-5s\x1b[0m",color,ent.Level.CapitalString())) + } + if ent.LoggerName != "" && c.NameKey != "" { + nameEncoder := c.EncodeName + + if nameEncoder == nil { + // Fall back to FullNameEncoder for backward compatibility. + nameEncoder = zapcore.FullNameEncoder + } + + nameEncoder(ent.LoggerName, arr) + } + if c.TimeKey != "" && c.EncodeTime != nil { + arr.AppendString(ent.Time.Format("15:04:05")) + } + + if ent.Caller.Defined && c.CallerKey != "" && c.EncodeCaller != nil { + c.EncodeCaller(ent.Caller, arr) + } + for i := range arr.elems { + if i > 0 { + line.AppendByte(' ') + } + fmt.Fprint(line, arr.elems[i]) + } + putSliceEncoder(arr) + + // Add the message itself. + if c.MessageKey != "" { + c.addTabIfNecessary(line) + line.AppendString(ent.Message) + } + + // Add any structured context. + c.writeContext(line, fields) + + // If there's no stacktrace key, honor that; this allows users to force + // single-line output. + if ent.Stack != "" && c.StacktraceKey != "" { + line.AppendByte('\n') + line.AppendString(ent.Stack) + } + + if c.LineEnding != "" { + line.AppendString(c.LineEnding) + } else { + line.AppendString(zapcore.DefaultLineEnding) + } + return line, nil +} + +func (c consoleEncoder) writeContext(line *buffer.Buffer, extra []zapcore.Field) { + context := c.Clone().(*consoleEncoder) + defer context.buf.Free() + // + addFields(context, extra) + context.closeOpenNamespaces() + if context.buf.Len() == 0 { + return + } + // + line.Write(context.buf.Bytes()) +} +func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) { + for i := range fields { + fields[i].AddTo(enc) + } +} + + +func (c consoleEncoder) addTabIfNecessary(line *buffer.Buffer) { + if line.Len() > 0 { + line.AppendByte('\t') + } +} + + +func (enc *consoleEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error { + enc.addKey(key) + return enc.AppendArray(arr) +} + +func (enc *consoleEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error { + enc.addKey(key) + return enc.AppendObject(obj) +} + +func (enc *consoleEncoder) AddBinary(key string, val []byte) { + enc.AddString(key, base64.StdEncoding.EncodeToString(val)) +} + +func (enc *consoleEncoder) AddByteString(key string, val []byte) { + enc.addKey(key) + enc.AppendByteString(val) +} + +func (enc *consoleEncoder) AddBool(key string, val bool) { + enc.addKey(key) + enc.AppendBool(val) +} + +func (enc *consoleEncoder) AddComplex128(key string, val complex128) { + enc.addKey(key) + enc.AppendComplex128(val) +} + +func (enc *consoleEncoder) AddDuration(key string, val time.Duration) { + enc.addKey(key) + enc.AppendDuration(val) +} + +func (enc *consoleEncoder) AddFloat64(key string, val float64) { + enc.addKey(key) + enc.AppendFloat64(val) +} + +func (enc *consoleEncoder) AddInt64(key string, val int64) { + enc.addKey(key) + enc.AppendInt64(val) +} + +func (enc *consoleEncoder) AddReflected(key string, obj interface{}) error { + enc.resetReflectBuf() + err := enc.reflectEnc.Encode(obj) + if err != nil { + return err + } + enc.reflectBuf.TrimNewline() + enc.addKey(key) + _, err = enc.buf.Write(enc.reflectBuf.Bytes()) + return err +} + +func (enc *consoleEncoder) OpenNamespace(key string) { + enc.addKey(key) + enc.buf.AppendByte('{') + enc.openNamespaces++ +} + +func (enc *consoleEncoder) AddString(key, val string) { + enc.addKey(key) + enc.AppendString(val) +} + +func (enc *consoleEncoder) AddTime(key string, val time.Time) { + enc.addKey(key) + enc.AppendTime(val) +} + +func (enc *consoleEncoder) AddUint64(key string, val uint64) { + enc.addKey(key) + enc.AppendUint64(val) +} + +func (enc *consoleEncoder) addKey(key string) { + // Print key in different color + enc.buf.AppendString(fmt.Sprintf(" \x1b[%dm%s\x1b[0m",36,key)) + + enc.buf.AppendByte('=') +} + +func (enc *consoleEncoder) AppendArray(arr zapcore.ArrayMarshaler) error { + enc.buf.AppendByte('[') + err := arr.MarshalLogArray(enc) + enc.buf.AppendByte(']') + return err +} + +func (enc *consoleEncoder) AppendObject(obj zapcore.ObjectMarshaler) error { + enc.buf.AppendByte('{') + err := obj.MarshalLogObject(enc) + enc.buf.AppendByte('}') + return err +} + +func (enc *consoleEncoder) AppendBool(val bool) { + enc.buf.AppendBool(val) +} + +func (enc *consoleEncoder) AppendByteString(val []byte) { + enc.buf.AppendByte('"') + enc.safeAddByteString(val) + enc.buf.AppendByte('"') +} + +func (enc *consoleEncoder) AppendComplex128(val complex128) { + // Cast to a platform-independent, fixed-size type. + r, i := float64(real(val)), float64(imag(val)) + enc.buf.AppendByte('"') + // Because we're always in a quoted string, we can use strconv without + // special-casing NaN and +/-Inf. + enc.buf.AppendFloat(r, 64) + enc.buf.AppendByte('+') + enc.buf.AppendFloat(i, 64) + enc.buf.AppendByte('i') + enc.buf.AppendByte('"') +} + +func (enc *consoleEncoder) AppendDuration(val time.Duration) { + cur := enc.buf.Len() + enc.EncodeDuration(val, enc) + if cur == enc.buf.Len() { + // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep + // JSON valid. + enc.AppendInt64(int64(val)) + } +} + +func (enc *consoleEncoder) AppendInt64(val int64) { + enc.buf.AppendInt(val) +} +func (enc *consoleEncoder) resetReflectBuf() { + if enc.reflectBuf == nil { + enc.reflectBuf = Get() + enc.reflectEnc = json.NewEncoder(enc.reflectBuf) + } else { + enc.reflectBuf.Reset() + } +} +func (enc *consoleEncoder) AppendReflected(val interface{}) error { + enc.resetReflectBuf() + err := enc.reflectEnc.Encode(val) + if err != nil { + return err + } + enc.reflectBuf.TrimNewline() + _, err = enc.buf.Write(enc.reflectBuf.Bytes()) + return err +} + +func (enc *consoleEncoder) AppendString(val string) { + enc.safeAddString(val) +} + +func (enc *consoleEncoder) AppendTime(val time.Time) { + cur := enc.buf.Len() + enc.EncodeTime(val, enc) + if cur == enc.buf.Len() { + // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep + // output JSON valid. + enc.AppendInt64(val.UnixNano()) + } +} + +func (enc *consoleEncoder) AppendUint64(val uint64) { + enc.buf.AppendUint(val) +} +func (enc *consoleEncoder) appendFloat(val float64, bitSize int) { + switch { + case math.IsNaN(val): + enc.buf.AppendString(`"NaN"`) + case math.IsInf(val, 1): + enc.buf.AppendString(`"+Inf"`) + case math.IsInf(val, -1): + enc.buf.AppendString(`"-Inf"`) + default: + enc.buf.AppendFloat(val, bitSize) + } +} + +// safeAddString JSON-escapes a string and appends it to the internal buffer. +// Unlike the standard library's encoder, it doesn't attempt to protect the +// user from browser vulnerabilities or JSONP-related problems. +func (enc *consoleEncoder) safeAddString(s string) { + for i := 0; i < len(s); { + if enc.tryAddRuneSelf(s[i]) { + i++ + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if enc.tryAddRuneError(r, size) { + i++ + continue + } + enc.buf.AppendString(s[i : i+size]) + i += size + } +} + +// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte. +func (enc *consoleEncoder) safeAddByteString(s []byte) { + for i := 0; i < len(s); { + if enc.tryAddRuneSelf(s[i]) { + i++ + continue + } + r, size := utf8.DecodeRune(s[i:]) + if enc.tryAddRuneError(r, size) { + i++ + continue + } + enc.buf.Write(s[i : i+size]) + i += size + } +} + +// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte. +func (enc *consoleEncoder) tryAddRuneSelf(b byte) bool { + if b >= utf8.RuneSelf { + return false + } + if 0x20 <= b && b != '\\' && b != '"' { + enc.buf.AppendByte(b) + return true + } + switch b { + case '\\', '"': + enc.buf.AppendByte('\\') + enc.buf.AppendByte(b) + case '\n': + enc.buf.AppendByte('\\') + enc.buf.AppendByte('n') + case '\r': + enc.buf.AppendByte('\\') + enc.buf.AppendByte('r') + case '\t': + enc.buf.AppendByte('\\') + enc.buf.AppendByte('t') + default: + // Encode bytes < 0x20, except for the escape sequences above. + enc.buf.AppendString(`\u00`) + enc.buf.AppendByte(_hex[b>>4]) + enc.buf.AppendByte(_hex[b&0xF]) + } + return true +} +func (enc *consoleEncoder) closeOpenNamespaces() { + for i := 0; i < enc.openNamespaces; i++ { + enc.buf.AppendByte('}') + } +} + + +func (enc *consoleEncoder) tryAddRuneError(r rune, size int) bool { + if r == utf8.RuneError && size == 1 { + enc.buf.AppendString(`\ufffd`) + return true + } + return false +} +func (enc *consoleEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) } +func (enc *consoleEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) } +func (enc *consoleEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) } +func (enc *consoleEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) } +func (enc *consoleEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) } +func (enc *consoleEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) } +func (enc *consoleEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) } +func (enc *consoleEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) } +func (enc *consoleEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) } +func (enc *consoleEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) } +func (enc *consoleEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) } +func (enc *consoleEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) } +func (enc *consoleEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) } +func (enc *consoleEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) } +func (enc *consoleEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) } +func (enc *consoleEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) } +func (enc *consoleEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) } +func (enc *consoleEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) } +func (enc *consoleEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) } +func (enc *consoleEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) } +func (enc *consoleEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) } +func (enc *consoleEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) } +func (enc *consoleEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) } + +const _hex = "0123456789abcdef" + +type sliceArrayEncoder struct { + elems []interface{} +} + +func (s *sliceArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error { + enc := &sliceArrayEncoder{} + err := v.MarshalLogArray(enc) + s.elems = append(s.elems, enc.elems) + return err +} + +func (s *sliceArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error { + m := zapcore.NewMapObjectEncoder() + err := v.MarshalLogObject(m) + s.elems = append(s.elems, m.Fields) + return err +} + +func (s *sliceArrayEncoder) AppendReflected(v interface{}) error { + s.elems = append(s.elems, v) + return nil +} + +func (s *sliceArrayEncoder) AppendBool(v bool) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendByteString(v []byte) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendComplex128(v complex128) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendComplex64(v complex64) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendDuration(v time.Duration) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendFloat64(v float64) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendFloat32(v float32) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendInt(v int) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendInt64(v int64) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendInt32(v int32) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendInt16(v int16) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendInt8(v int8) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendString(v string) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendTime(v time.Time) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUint(v uint) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUint64(v uint64) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUint32(v uint32) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUint16(v uint16) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUint8(v uint8) { s.elems = append(s.elems, v) } +func (s *sliceArrayEncoder) AppendUintptr(v uintptr) { s.elems = append(s.elems, v) } diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..a0417f1 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,227 @@ +package logger + +import ( + "fmt" + "github.com/revel/config" + "github.com/revel/log15" + "log" + "os" +) + +// The LogHandler defines the interface to handle the log records +type ( + // The Multilogger reduces the number of exposed defined logging variables, + // and allows the output to be easily refined + MultiLogger interface { + //log15.Logger + //// New returns a new Logger that has this logger's context plus the given context + New(ctx ...interface{}) MultiLogger + // + //// SetHandler updates the logger to write records to the specified handler. + SetHandler(h LogHandler) + SetStackDepth(int) MultiLogger + // + //// Log a message at the given level with context key/value pairs + Debug(msg string, ctx ...interface{}) + Debugf(msg string, params ...interface{}) + Info(msg string, ctx ...interface{}) + Infof(msg string, params ...interface{}) + Warn(msg string, ctx ...interface{}) + Warnf(msg string, params ...interface{}) + Error(msg string, ctx ...interface{}) + Errorf(msg string, params ...interface{}) + Crit(msg string, ctx ...interface{}) + Critf(msg string, params ...interface{}) + + //// Logs a message as an Crit and exits + Fatal(msg string, ctx ...interface{}) + Fatalf(msg string, params ...interface{}) + //// Logs a message as an Crit and panics + Panic(msg string, ctx ...interface{}) + Panicf(msg string, params ...interface{}) + } + LogHandler interface { + log15.Handler + } + LogStackHandler interface { + LogHandler + GetStack() int + } + ParentLogHandler interface { + SetChild(handler LogHandler) LogHandler + } + LogFormat interface { + log15.Format + } + + LogLevel log15.Lvl + RevelLogger struct { + log15.Logger + } + + // Used for the callback to LogFunctionMap + LogOptions struct { + Ctx *config.Context + ReplaceExistingHandler bool + HandlerWrap ParentLogHandler + Levels []LogLevel + ExtendedOptions map[string]interface{} + } +) + +const ( + LvlDebug = LogLevel(log15.LvlDebug) + LvlInfo = LogLevel(log15.LvlInfo) + LvlWarn = LogLevel(log15.LvlWarn) + LvlError = LogLevel(log15.LvlError) + LvlCrit = LogLevel(log15.LvlCrit) +) + +// A list of all the log levels +var LvlAllList = []LogLevel{LvlDebug, LvlInfo, LvlWarn, LvlError, LvlCrit} + +// The log function map can be added to, so that you can specify your own logging mechanism +var LogFunctionMap = map[string]func(*CompositeMultiHandler, *LogOptions){ + // Do nothing - set the logger off + "off": func(c *CompositeMultiHandler, logOptions *LogOptions) { + // Only drop the results if there is a parent handler defined + if logOptions.HandlerWrap != nil { + for _, l := range logOptions.Levels { + c.SetHandler(logOptions.HandlerWrap.SetChild(NilHandler()), logOptions.ReplaceExistingHandler, l) + } + } + }, + // Do nothing - set the logger off + "": func(*CompositeMultiHandler, *LogOptions) {}, + // Set the levels to stdout, replace existing + "stdout": func(c *CompositeMultiHandler, logOptions *LogOptions) { + if logOptions.Ctx != nil { + logOptions.SetExtendedOptions( + "noColor", !logOptions.Ctx.BoolDefault("log.colorize", true), + "smallDate", logOptions.Ctx.BoolDefault("log.smallDate", true)) + } + + c.SetTerminal(os.Stdout, logOptions) + }, + // Set the levels to stderr output to terminal + "stderr": func(c *CompositeMultiHandler, logOptions *LogOptions) { + c.SetTerminal(os.Stderr, logOptions) + }, +} + +// Set the systems default logger +// Default logs will be captured and handled by revel at level info +func SetDefaultLog(fromLog MultiLogger) { + log.SetOutput(loggerRewrite{Logger: fromLog, Level: log15.LvlInfo, hideDeprecated: true}) + // No need to show date and time, that will be logged with revel + log.SetFlags(0) +} + +// Formatted debug call +func (rl *RevelLogger) Debugf(msg string, param ...interface{}) { + rl.Debug(fmt.Sprintf(msg, param...)) +} +// Formatted info call +func (rl *RevelLogger) Infof(msg string, param ...interface{}) { + rl.Info(fmt.Sprintf(msg, param...)) +} +func (rl *RevelLogger) Warnf(msg string, param ...interface{}) { + rl.Warn(fmt.Sprintf(msg, param...)) +} +func (rl *RevelLogger) Errorf(msg string, param ...interface{}) { + rl.Error(fmt.Sprintf(msg, param...)) +} +func (rl *RevelLogger) Critf(msg string, param ...interface{}) { + rl.Crit(fmt.Sprintf(msg, param...)) +} +func (rl *RevelLogger) Fatalf(msg string, param ...interface{}) { + rl.Crit(fmt.Sprintf(msg, param...)) + os.Exit(1) +} +func (rl *RevelLogger) Panicf(msg string, param ...interface{}) { + rl.Crit(fmt.Sprintf(msg, param...)) + panic(msg) +} + +func (rl *RevelLogger) Fatal(msg string, ctx ...interface{}) { + rl.Crit(msg, ctx...) + os.Exit(1) +} + +func (rl *RevelLogger) Panic(msg string, ctx ...interface{}) { + rl.Crit(msg, ctx...) + panic(msg) +} + +// Override log15 method +func (rl *RevelLogger) New(ctx ...interface{}) MultiLogger { + old := &RevelLogger{Logger: rl.Logger.New(ctx...)} + return old +} + +// Set the stack level to check for the caller +func (rl *RevelLogger) SetStackDepth(amount int) MultiLogger { + rl.Logger.SetStackDepth(amount) // Ignore the logger returned + return rl +} + +// Create a new logger +func New(ctx ...interface{}) MultiLogger { + r := &RevelLogger{Logger: log15.New(ctx...)} + r.SetStackDepth(1) + return r +} + +// Set the handler in the Logger +func (rl *RevelLogger) SetHandler(h LogHandler) { + rl.Logger.SetHandler(h) +} + +type parentLogHandler struct { + setChild func(handler LogHandler) LogHandler +} + +func NewParentLogHandler(callBack func(child LogHandler) LogHandler) ParentLogHandler { + return &parentLogHandler{callBack} +} + +func (p *parentLogHandler) SetChild(child LogHandler) LogHandler { + return p.setChild(child) +} + +// Create a new log options +func NewLogOptions(cfg *config.Context, replaceHandler bool, phandler ParentLogHandler, lvl ...LogLevel) (logOptions *LogOptions) { + logOptions = &LogOptions{ + Ctx: cfg, + ReplaceExistingHandler: replaceHandler, + HandlerWrap: phandler, + Levels: lvl, + ExtendedOptions: map[string]interface{}{}, + } + return +} + +// Assumes options will be an even number and have a string, value syntax +func (l *LogOptions) SetExtendedOptions(options ...interface{}) { + for x := 0; x < len(options); x += 2 { + l.ExtendedOptions[options[x].(string)] = options[x+1] + } +} +func (l *LogOptions) GetStringDefault(option, value string) string { + if v, found := l.ExtendedOptions[option]; found { + return v.(string) + } + return value +} +func (l *LogOptions) GetIntDefault(option string, value int) int { + if v, found := l.ExtendedOptions[option]; found { + return v.(int) + } + return value +} +func (l *LogOptions) GetBoolDefault(option string, value bool) bool { + if v, found := l.ExtendedOptions[option]; found { + return v.(bool) + } + return value +} diff --git a/logger/utils.go b/logger/utils.go new file mode 100644 index 0000000..3a98b8d --- /dev/null +++ b/logger/utils.go @@ -0,0 +1,277 @@ +package logger + +import ( + "gopkg.in/stack.v0" + "github.com/revel/config" + "github.com/revel/log15" + "log" + "os" + "path/filepath" + "strings" +) + +// Utility package to make existing logging backwards compatible +var ( + // Convert the string to LogLevel + toLevel = map[string]LogLevel{"debug": LogLevel(log15.LvlDebug), + "info": LogLevel(log15.LvlInfo), "request": LogLevel(log15.LvlInfo), "warn": LogLevel(log15.LvlWarn), + "error": LogLevel(log15.LvlError), "crit": LogLevel(log15.LvlCrit), + "trace": LogLevel(log15.LvlDebug), // TODO trace is deprecated, replaced by debug + } +) + +func GetLogger(name string, logger MultiLogger) (l *log.Logger) { + switch name { + case "trace": // TODO trace is deprecated, replaced by debug + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0) + case "debug": + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0) + case "info": + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0) + case "warn": + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlWarn}, "", 0) + case "error": + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlError}, "", 0) + case "request": + l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0) + } + + return l + +} + +// Get all handlers based on the Config (if available) +func InitializeFromConfig(basePath string, config *config.Context) (c *CompositeMultiHandler) { + // If running in test mode suppress anything that is not an error + if config!=nil && config.BoolDefault("testModeFlag",false) { + config.SetOption("log.info.output","none") + config.SetOption("log.debug.output","none") + config.SetOption("log.warn.output","none") + config.SetOption("log.error.output","stderr") + config.SetOption("log.crit.output","stderr") + } + + + // If the configuration has an all option we can skip some + c, _ = NewCompositeMultiHandler() + + // Filters are assigned first, non filtered items override filters + initAllLog(c, basePath, config) + initLogLevels(c, basePath, config) + if c.CriticalHandler == nil && c.ErrorHandler != nil { + c.CriticalHandler = c.ErrorHandler + } + initFilterLog(c, basePath, config) + if c.CriticalHandler == nil && c.ErrorHandler != nil { + c.CriticalHandler = c.ErrorHandler + } + initRequestLog(c, basePath, config) + + return c +} + +// Init the log.all configuration options +func initAllLog(c *CompositeMultiHandler, basePath string, config *config.Context) { + if config != nil { + extraLogFlag := config.BoolDefault("specialUseFlag", false) + if output, found := config.String("log.all.output"); found { + // Set all output for the specified handler + if extraLogFlag { + log.Printf("Adding standard handler for levels to >%s< ", output) + } + initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, LvlAllList...)) + } + } +} + +// Init the filter options +// log.all.filter .... +// log.error.filter .... +func initFilterLog(c *CompositeMultiHandler, basePath string, config *config.Context) { + if config != nil { + extraLogFlag := config.BoolDefault("specialUseFlag", false) + + // The commands to use + logFilterList := []struct { + LogPrefix, LogSuffix string + parentHandler func(map[string]interface{}) ParentLogHandler + }{{ + "log.", ".filter", + func(keyMap map[string]interface{}) ParentLogHandler { + return NewParentLogHandler(func(child LogHandler) LogHandler { + return MatchMapHandler(keyMap, child) + }) + + }, + }, { + "log.", ".nfilter", + func(keyMap map[string]interface{}) ParentLogHandler { + return NewParentLogHandler(func(child LogHandler) LogHandler { + return NotMatchMapHandler(keyMap, child) + }) + }, + }} + + for _, logFilter := range logFilterList { + // Init for all filters + for _, name := range []string{"all", "debug", "info", "warn", "error", "crit", + "trace", // TODO trace is deprecated + } { + optionList := config.Options(logFilter.LogPrefix + name + logFilter.LogSuffix) + for _, option := range optionList { + splitOptions := strings.Split(option, ".") + keyMap := map[string]interface{}{} + for x := 3; x < len(splitOptions); x += 2 { + keyMap[splitOptions[x]] = splitOptions[x+1] + } + phandler := logFilter.parentHandler(keyMap) + if extraLogFlag { + log.Printf("Adding key map handler %s %s output %s", option, name, config.StringDefault(option, "")) + } + + if name == "all" { + initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler)) + } else { + initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler, toLevel[name])) + } + } + } + } + } +} + +// Init the log.error, log.warn etc configuration options +func initLogLevels(c *CompositeMultiHandler, basePath string, config *config.Context) { + for _, name := range []string{"debug", "info", "warn", "error", "crit", + "trace", // TODO trace is deprecated + } { + if config != nil { + extraLogFlag := config.BoolDefault("specialUseFlag", false) + output, found := config.String("log." + name + ".output") + if found { + if extraLogFlag { + log.Printf("Adding standard handler %s output %s", name, output) + } + initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, toLevel[name])) + } + // Gets the list of options with said prefix + } else { + initHandlerFor(c, "stderr", basePath, NewLogOptions(config, true, nil, toLevel[name])) + } + } +} + +// Init the request log options +func initRequestLog(c *CompositeMultiHandler, basePath string, config *config.Context) { + // Request logging to a separate output handler + // This takes the InfoHandlers and adds a MatchAbHandler handler to it to direct + // context with the word "section=requestlog" to that handler. + // Note if request logging is not enabled the MatchAbHandler will not be added and the + // request log messages will be sent out the INFO handler + outputRequest := "stdout" + if config != nil { + outputRequest = config.StringDefault("log.request.output", "") + } + oldInfo := c.InfoHandler + c.InfoHandler = nil + if outputRequest != "" { + initHandlerFor(c, outputRequest, basePath, NewLogOptions(config, false, nil, LvlInfo)) + } + if c.InfoHandler != nil || oldInfo != nil { + if c.InfoHandler == nil { + c.InfoHandler = oldInfo + } else { + c.InfoHandler = MatchAbHandler("section", "requestlog", c.InfoHandler, oldInfo) + } + } +} + +// Returns a handler for the level using the output string +// Accept formats for output string are +// LogFunctionMap[value] callback function +// `stdout` `stderr` `full/file/path/to/location/app.log` `full/file/path/to/location/app.json` +func initHandlerFor(c *CompositeMultiHandler, output, basePath string, options *LogOptions) { + if options.Ctx != nil { + options.SetExtendedOptions( + "noColor", !options.Ctx.BoolDefault("log.colorize", true), + "smallDate", options.Ctx.BoolDefault("log.smallDate", true), + "maxSize", options.Ctx.IntDefault("log.maxsize", 1024*10), + "maxAge", options.Ctx.IntDefault("log.maxage", 14), + "maxBackups", options.Ctx.IntDefault("log.maxbackups", 14), + "compressBackups", !options.Ctx.BoolDefault("log.compressBackups", true), + ) + } + + output = strings.TrimSpace(output) + if funcHandler, found := LogFunctionMap[output]; found { + funcHandler(c, options) + } else { + switch output { + case "": + fallthrough + case "off": + // No handler, discard data + default: + // Write to file specified + if !filepath.IsAbs(output) { + output = filepath.Join(basePath, output) + } + + if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil { + log.Panic(err) + } + + if strings.HasSuffix(output, "json") { + c.SetJsonFile(output, options) + } else { + // Override defaults for a terminal file + options.SetExtendedOptions("noColor", true) + options.SetExtendedOptions("smallDate", false) + c.SetTerminalFile(output, options) + } + } + } + return +} + +// This structure and method will handle the old output format and log it to the new format +type loggerRewrite struct { + Logger MultiLogger + Level log15.Lvl + hideDeprecated bool +} + +var log_deprecated = []byte("* LOG DEPRECATED * ") + +func (lr loggerRewrite) Write(p []byte) (n int, err error) { + if !lr.hideDeprecated { + p = append(log_deprecated, p...) + } + n = len(p) + if len(p) > 0 && p[n-1] == '\n' { + p = p[:n-1] + n-- + } + + switch lr.Level { + case log15.LvlInfo: + lr.Logger.Info(string(p)) + case log15.LvlDebug: + lr.Logger.Debug(string(p)) + case log15.LvlWarn: + lr.Logger.Warn(string(p)) + case log15.LvlError: + lr.Logger.Error(string(p)) + case log15.LvlCrit: + lr.Logger.Crit(string(p)) + } + + return +} + +// For logging purposes the call stack can be used to record the stack trace of a bad error +// simply pass it as a context field in your log statement like +// `controller.Log.Critc("This should not occur","stack",revel.NewCallStack())` +func NewCallStack() interface{} { + return stack.Trace() +} diff --git a/model/command_config.go b/model/command_config.go new file mode 100644 index 0000000..fbba7de --- /dev/null +++ b/model/command_config.go @@ -0,0 +1,61 @@ +package model + +type ( + // The Revel command type + COMMAND int + + // The Command config for the line input + CommandConfig struct { + Index COMMAND // The index + Verbose bool `short:"v" long:"debug" description:"If set the logger is set to verbose"` // True if debug is active + HistoricMode bool `long:"historic-run-mode" description:"If set the runmode is passed a string not json"` // True if debug is active + ImportPath string // The import path (converted from various commands) + GoPath string // The GoPath + GoCmd string // The full path to the go executable + SrcRoot string // The source root + AppPath string // The application path + AppName string // The applicaiton name + BasePath string // The base path + SkeletonPath string // The skeleton path + BuildFlags []string `short:"X" long:"build-flags" description:"These flags will be used when building the application. May be specified multiple times, only applicable for Build, Run, Package, Test commands"` + // The new command + New struct { + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + Skeleton string `short:"s" long:"skeleton" description:"Path to skeleton folder (Must exist on GO PATH)" required:"false"` + Vendored bool `short:"V" long:"vendor" description:"True if project should contain a vendor folder to be initialized. Creates the vendor folder and the 'Gopkg.toml' file in the root"` + Run bool `short:"r" long:"run" description:"True if you want to run the application right away"` + } `command:"new"` + // The build command + Build struct { + TargetPath string `short:"t" long:"target-path" description:"Path to target folder. Folder will be completely deleted if it exists" required:"true"` + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` + } `command:"build"` + // The run command + Run struct { + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` + Port string `short:"p" long:"port" description:"The port to listen"` + NoProxy bool `short:"n" long:"no-proxy" description:"True if proxy server should not be started. This will only update the main and routes files on change"` + } `command:"run"` + // The package command + Package struct { + Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + } `command:"package"` + // The clean command + Clean struct { + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + } `command:"clean"` + // The test command + Test struct { + Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"` + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"true"` + Function string `short:"f" long:"suite-function" description:"The suite.function"` + } `command:"test"` + // The version command + Version struct { + ImportPath string `short:"a" long:"application-path" description:"Path to applicaiton folder" required:"false"` + } `command:"version"` + } +) diff --git a/model/embedded_type_name.go b/model/embedded_type_name.go new file mode 100644 index 0000000..6a67056 --- /dev/null +++ b/model/embedded_type_name.go @@ -0,0 +1,11 @@ +package model + +// The embedded type name takes the import path and structure name +type EmbeddedTypeName struct { + ImportPath, StructName string +} + +// Convert the type to a properly formatted import line +func (s *EmbeddedTypeName) String() string { + return s.ImportPath + "." + s.StructName +} \ No newline at end of file diff --git a/model/method.go b/model/method.go new file mode 100644 index 0000000..5652a8c --- /dev/null +++ b/model/method.go @@ -0,0 +1,25 @@ +package model + + +// 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. +} + diff --git a/model/revel_container.go b/model/revel_container.go new file mode 100644 index 0000000..d013e99 --- /dev/null +++ b/model/revel_container.go @@ -0,0 +1,294 @@ +// This package will be shared between Revel and Revel CLI eventually +package model + +import ( + "github.com/revel/cmd/utils" + "github.com/revel/config" + "go/build" + + "os" + "path/filepath" + "sort" + "strings" +) +const ( + // Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option) + TEMPLATE_REFRESH_REQUESTED = iota + // Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option) + TEMPLATE_REFRESH_COMPLETED + // Event type before all module loads, events thrown to handlers added to AddInitEventHandler + + // Event type before all module loads, events thrown to handlers added to AddInitEventHandler + REVEL_BEFORE_MODULES_LOADED + // Event type called when a new module is found + REVEL_BEFORE_MODULE_LOADED + // Event type called when after a new module is found + REVEL_AFTER_MODULE_LOADED + // Event type after all module loads, events thrown to handlers added to AddInitEventHandler + REVEL_AFTER_MODULES_LOADED + + // Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_BEFORE_INITIALIZED + // Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_STARTED + // Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler + ENGINE_SHUTDOWN + + // Called before routes are refreshed + ROUTE_REFRESH_REQUESTED + // Called after routes have been refreshed + ROUTE_REFRESH_COMPLETED +) +type ( + // The container object for describing all Revels variables + RevelContainer struct { + ImportPath string // The import path + SourcePath string // The full source path + RunMode string // The current run mode + RevelPath string // The path to the Revel source code + BasePath string // The base path to the application + AppPath string // The application path (BasePath + "/app" + ViewsPath string // The application views path + CodePaths []string // All the code paths + TemplatePaths []string // All the template paths + ConfPaths []string // All the configuration paths + Config *config.Context // The global config object + Packaged bool // True if packaged + DevMode bool // True if running in dev mode + HTTPPort int // The http port + HTTPAddr string // The http address + HTTPSsl bool // True if running https + HTTPSslCert string // The SSL certificate + HTTPSslKey string // The SSL key + AppName string // The application name + AppRoot string // The application root from the config `app.root` + CookiePrefix string // The cookie prefix + CookieDomain string // The cookie domain + CookieSecure bool // True if cookie is secure + SecretStr string // The secret string + MimeConfig *config.Context // The mime configuration + ModulePathMap map[string]string // The module path map + } + + RevelCallback interface { + FireEvent(key int, value interface{}) (response int) + } + doNothingRevelCallback struct { + + } + +) + +// Simple callback to pass to the RevelCallback that does nothing +var DoNothingRevelCallback = RevelCallback(&doNothingRevelCallback{}) + +func (_ *doNothingRevelCallback) FireEvent(key int, value interface{}) (response int) { + return +} + +// RevelImportPath Revel framework import path +var RevelImportPath = "github.com/revel/revel" + +// This function returns a container object describing the revel application +// eventually this type of function will replace the global variables. +func NewRevelPaths(mode, importPath, srcPath string, callback RevelCallback) (rp *RevelContainer) { + rp = &RevelContainer{ModulePathMap: map[string]string{}} + // Ignore trailing slashes. + rp.ImportPath = strings.TrimRight(importPath, "/") + rp.SourcePath = srcPath + rp.RunMode = mode + + // If the SourcePath is not specified, find it using build.Import. + var revelSourcePath string // may be different from the app source path + if rp.SourcePath == "" { + revelSourcePath, rp.SourcePath = findSrcPaths(importPath) + } else { + // If the SourcePath was specified, assume both Revel and the app are within it. + rp.SourcePath = filepath.Clean(rp.SourcePath) + revelSourcePath = rp.SourcePath + + } + + // Setup paths for application + rp.RevelPath = filepath.Join(revelSourcePath, filepath.FromSlash(RevelImportPath)) + rp.BasePath = filepath.Join(rp.SourcePath, filepath.FromSlash(importPath)) + rp.AppPath = filepath.Join(rp.BasePath, "app") + rp.ViewsPath = filepath.Join(rp.AppPath, "views") + + rp.CodePaths = []string{rp.AppPath} + rp.TemplatePaths = []string{} + + if rp.ConfPaths == nil { + rp.ConfPaths = []string{} + } + + // Config load order + // 1. framework (revel/conf/*) + // 2. application (conf/*) + // 3. user supplied configs (...) - User configs can override/add any from above + rp.ConfPaths = append( + []string{ + filepath.Join(rp.RevelPath, "conf"), + filepath.Join(rp.BasePath, "conf"), + }, + rp.ConfPaths...) + + var err error + rp.Config, err = config.LoadContext("app.conf", rp.ConfPaths) + if err != nil { + utils.Logger.Fatal("Unable to load configuartion file ","error", err) + os.Exit(1) + } + + // Ensure that the selected runmode appears in app.conf. + // If empty string is passed as the mode, treat it as "DEFAULT" + if mode == "" { + mode = config.DefaultSection + } + if !rp.Config.HasSection(mode) { + utils.Logger.Fatal("app.conf: No mode found:", mode) + } + rp.Config.SetSection(mode) + + // Configure properties from app.conf + rp.DevMode = rp.Config.BoolDefault("mode.dev", false) + rp.HTTPPort = rp.Config.IntDefault("http.port", 9000) + rp.HTTPAddr = rp.Config.StringDefault("http.addr", "") + rp.HTTPSsl = rp.Config.BoolDefault("http.ssl", false) + rp.HTTPSslCert = rp.Config.StringDefault("http.sslcert", "") + rp.HTTPSslKey = rp.Config.StringDefault("http.sslkey", "") + if rp.HTTPSsl { + if rp.HTTPSslCert == "" { + utils.Logger.Fatal("No http.sslcert provided.") + } + if rp.HTTPSslKey == "" { + utils.Logger.Fatal("No http.sslkey provided.") + } + } + // + rp.AppName = rp.Config.StringDefault("app.name", "(not set)") + rp.AppRoot = rp.Config.StringDefault("app.root", "") + rp.CookiePrefix = rp.Config.StringDefault("cookie.prefix", "REVEL") + rp.CookieDomain = rp.Config.StringDefault("cookie.domain", "") + rp.CookieSecure = rp.Config.BoolDefault("cookie.secure", rp.HTTPSsl) + rp.SecretStr = rp.Config.StringDefault("app.secret", "") + + + callback.FireEvent(REVEL_BEFORE_MODULES_LOADED, nil) + rp.loadModules(callback) + callback.FireEvent(REVEL_AFTER_MODULES_LOADED, nil) + + return +} + +// LoadMimeConfig load mime-types.conf on init. +func (rp *RevelContainer) LoadMimeConfig() { + var err error + rp.MimeConfig, err = config.LoadContext("mime-types.conf", rp.ConfPaths) + if err != nil { + utils.Logger.Fatal("Failed to load mime type config:", "error", err) + } +} + +// Loads modules based on the configuration setup. +// This will fire the REVEL_BEFORE_MODULE_LOADED, REVEL_AFTER_MODULE_LOADED +// for each module loaded. The callback will receive the RevelContainer, name, moduleImportPath and modulePath +// It will automatically add in the code paths for the module to the +// container object +func (rp *RevelContainer) loadModules(callback RevelCallback) { + keys := []string{} + for _, key := range rp.Config.Options("module.") { + keys = append(keys, key) + } + + // Reorder module order by key name, a poor mans sort but at least it is consistent + sort.Strings(keys) + for _, key := range keys { + moduleImportPath := rp.Config.StringDefault(key, "") + if moduleImportPath == "" { + continue + } + + modulePath, err := rp.ResolveImportPath(moduleImportPath) + if err != nil { + utils.Logger.Error("Failed to load module. Import of path failed", "modulePath", moduleImportPath, "error", err) + } + // Drop anything between module.???. + name := key[len("module."):] + if index := strings.Index(name, "."); index > -1 { + name = name[index+1:] + } + callback.FireEvent(REVEL_BEFORE_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath}) + rp.addModulePaths(name, moduleImportPath, modulePath) + callback.FireEvent(REVEL_AFTER_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath}) + } +} + +// Adds a module paths to the container object +func (rp *RevelContainer) addModulePaths(name, importPath, modulePath string) { + if codePath := filepath.Join(modulePath, "app"); utils.DirExists(codePath) { + rp.CodePaths = append(rp.CodePaths, codePath) + rp.ModulePathMap[name] = modulePath + if viewsPath := filepath.Join(modulePath, "app", "views"); utils.DirExists(viewsPath) { + rp.TemplatePaths = append(rp.TemplatePaths, viewsPath) + } + } + + // Hack: There is presently no way for the testrunner module to add the + // "test" subdirectory to the CodePaths. So this does it instead. + if importPath == rp.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") { + joinedPath := filepath.Join(rp.BasePath, "tests") + rp.CodePaths = append(rp.CodePaths, joinedPath) + } + if testsPath := filepath.Join(modulePath, "tests"); utils.DirExists(testsPath) { + rp.CodePaths = append(rp.CodePaths, testsPath) + } +} + +// ResolveImportPath returns the filesystem path for the given import path. +// Returns an error if the import path could not be found. +func (rp *RevelContainer) ResolveImportPath(importPath string) (string, error) { + if rp.Packaged { + return filepath.Join(rp.SourcePath, importPath), nil + } + + modPkg, err := build.Import(importPath, rp.RevelPath, build.FindOnly) + if err != nil { + return "", err + } + return modPkg.Dir, nil +} + +// Find the full source dir for the import path, uses the build.Default.GOPATH to search for the directory +func findSrcPaths(importPath string) (revelSourcePath, appSourcePath string) { + var ( + gopaths = filepath.SplitList(build.Default.GOPATH) + goroot = build.Default.GOROOT + ) + + if len(gopaths) == 0 { + utils.Logger.Fatalf("GOPATH environment variable is not set. " + + "Please refer to http://golang.org/doc/code.html to configure your Go environment.") + } + + if utils.ContainsString(gopaths, goroot) { + utils.Logger.Fatalf("GOPATH (%s) must not include your GOROOT (%s). "+ + "Please refer to http://golang.org/doc/code.html to configure your Go environment.", + gopaths, goroot) + + } + + appPkg, err := build.Import(importPath, "", build.FindOnly) + if err != nil { + utils.Logger.Fatal("Failed to import "+importPath+" with error:", "error", err) + } + + revelPkg, err := build.Import(RevelImportPath, appPkg.Dir, build.FindOnly) + if err != nil { + utils.Logger.Fatal("Failed to find Revel with error:", "error", err) + } + + revelSourcePath, appSourcePath = revelPkg.Dir[:len(revelPkg.Dir)-len(RevelImportPath)], appPkg.SrcRoot + return +} + diff --git a/model/source_info.go b/model/source_info.go new file mode 100644 index 0000000..8b47043 --- /dev/null +++ b/model/source_info.go @@ -0,0 +1,121 @@ +package model + +// SourceInfo is the top-level struct containing all extracted information +// about the app source code, used to generate main.go. +import ( + "github.com/revel/cmd/utils" + "path/filepath" + "unicode" + "strings" +) + +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 +} + +// 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 utils.ContainsString(processed, spec.String()) || + utils.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]) { + utils.Logger.Info("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 + } + } + + // Report non controller structures in controller folder. + if !found && !strings.HasPrefix(spec.StructName, "Test") { + utils.Logger.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(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(RevelImportPath+"/testing.TestSuite", "testsuite") + } + return s.testSuites +} diff --git a/model/type_expr.go b/model/type_expr.go new file mode 100644 index 0000000..6387b7c --- /dev/null +++ b/model/type_expr.go @@ -0,0 +1,100 @@ +package model + + + +// TypeExpr provides a type name that may be rewritten to use a package name. +import ( + "go/ast" + "fmt" +) + +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 := 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 { + error := "" + 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} + } + error = fmt.Sprintf("Failed to generate name for Map field :%v. Make sure the field name is valid.", t.Key) + case *ast.Ellipsis: + e := NewTypeExpr(pkgName, t.Elt) + return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid} + default: + error = fmt.Sprintf("Failed to generate name for field: %v Package: %v. Make sure the field name is valid.", expr, pkgName) + + } + return TypeExpr{Valid: false, Expr:error} +} + +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 +} + +// Returns the first non empty string from a list of arguements +func FirstNonEmpty(strs ...string) string { + for _, str := range strs { + if len(str) > 0 { + return str + } + } + return "" +} + diff --git a/model/type_info.go b/model/type_info.go new file mode 100644 index 0000000..5489992 --- /dev/null +++ b/model/type_info.go @@ -0,0 +1,16 @@ +package model + +// 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 // Method specifications, the action functions + EmbeddedTypes []*EmbeddedTypeName // Used internally to identify controllers that indirectly embed *revel.Controller. +} + +// Return the type information as a properly formatted import string +func (s *TypeInfo) String() string { + return s.ImportPath + "." + s.StructName +} + diff --git a/harness/reflect.go b/parser/reflect.go similarity index 57% rename from harness/reflect.go rename to parser/reflect.go index c1bb8fc..a896edd 100644 --- a/harness/reflect.go +++ b/parser/reflect.go @@ -2,7 +2,7 @@ // Revel Framework source code and usage is governed by a MIT style // license that can be found in the LICENSE file. -package harness +package parser // This file handles the app code introspection. // It catalogs the controllers, their methods, and their arguments. @@ -17,97 +17,34 @@ import ( "path/filepath" "strings" - "unicode" - - "github.com/revel/revel" - "log" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" ) -// 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 +type methodMap map[string][]*model.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) { +func ProcessSource(paths *model.RevelContainer) (*model.SourceInfo, *utils.Error) { var ( - srcInfo *SourceInfo - compileError *revel.Error + srcInfo *model.SourceInfo + compileError *utils.Error ) - for _, root := range roots { + for _, root := range paths.CodePaths { rootImportPath := importPathFromPath(root) if rootImportPath == "" { - revel.RevelLog.Warn("Skipping empty code path", "path", root) + utils.Logger.Info("Skipping empty code path", "path", root) continue } // Start walking the directory tree. - _ = revel.Walk(root, func(path string, info os.FileInfo, err error) error { + _ = utils.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { - revel.RevelLog.Error("Error scanning app source:", "error", err) + utils.Logger.Error("Error scanning app source:", "error", err) return nil } @@ -130,17 +67,17 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { if err != nil { if errList, ok := err.(scanner.ErrorList); ok { var pos = errList[0].Pos - compileError = &revel.Error{ + compileError = &utils.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), + SourceLines: utils.MustReadLines(pos.Filename), } - errorLink := revel.Config.StringDefault("error.link", "") + errorLink := paths.Config.StringDefault("error.link", "") if errorLink != "" { compileError.SetLink(errorLink) @@ -149,9 +86,9 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { return compileError } - // This is exception, err alredy checked above. Here just a print + // This is exception, err already checked above. Here just a print ast.Print(nil, err) - revel.RevelLog.Fatal("Failed to parse dir", "error", err) + utils.Logger.Fatal("Failed to parse dir", "error", err) } // Skip "main" packages. @@ -176,7 +113,7 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { for i := range pkgs { println("Found package ", i) } - revel.RevelLog.Error("Most unexpected! Multiple packages in a single directory:", "packages", pkgs) + utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs) } var pkg *ast.Package @@ -192,7 +129,7 @@ func ProcessSource(roots []string) (*SourceInfo, *revel.Error) { return srcInfo, compileError } -func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo { +func appendSourceInfo(srcInfo1, srcInfo2 *model.SourceInfo) *model.SourceInfo { if srcInfo1 == nil { return srcInfo2 } @@ -201,7 +138,7 @@ func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo { 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) + utils.Logger.Warn("Warn: Key conflict when scanning validation calls:", "key", k) continue } srcInfo1.ValidationKeys[k] = v @@ -209,9 +146,9 @@ func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo { return srcInfo1 } -func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *SourceInfo { +func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *model.SourceInfo { var ( - structSpecs []*TypeInfo + structSpecs []*model.TypeInfo initImportPaths []string methodSpecs = make(methodMap) @@ -223,8 +160,8 @@ func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast ) // For each source file in the package... - log.Println("Exaiming files in path", pkgPath) - for _, file := range pkg.Files { + utils.Logger.Info("Exaiming files in path", "package", pkgPath) + for fname, 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{} @@ -235,16 +172,16 @@ func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast if scanControllers { // Match and add both structs and methods - structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset) + structSpecs = appendStruct(fname, 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) + structSpecs = appendStruct(fname, 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) + lineKeys := getValidationKeys(fname, fset, funcDecl, imports) if len(lineKeys) > 0 { validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys } @@ -262,7 +199,7 @@ func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast spec.MethodSpecs = methodSpecs[spec.StructName] } - return &SourceInfo{ + return &model.SourceInfo{ StructSpecs: structSpecs, ValidationKeys: validationKeys, InitImportPaths: initImportPaths, @@ -319,7 +256,7 @@ func addImports(imports map[string]string, decl ast.Decl, srcDir string) { // 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) + utils.Logger.Info("Debug: Could not find import:", "path", fullPath) } continue } @@ -332,7 +269,7 @@ func addImports(imports map[string]string, decl ast.Decl, srcDir string) { // 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 { +func appendStruct(fileName string, specs []*model.TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*model.TypeInfo { // Filter out non-Struct type declarations. spec, found := getStructTypeDecl(decl, fset) if !found { @@ -344,7 +281,7 @@ func appendStruct(specs []*TypeInfo, pkgImportPath string, pkg *ast.Package, dec // 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{ + controllerSpec := &model.TypeInfo{ StructName: spec.Name.Name, ImportPath: pkgImportPath, PackageName: pkg.Name, @@ -398,12 +335,12 @@ func appendStruct(specs []*TypeInfo, pkgImportPath string, pkg *ast.Package, dec } else { var ok bool if importPath, ok = imports[pkgName]; !ok { - revel.RevelLog.Error("Failed to find import path for ", "package", pkgName, "type", typeName) + utils.Logger.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName) continue } } - controllerSpec.embeddedTypes = append(controllerSpec.embeddedTypes, &embeddedTypeName{ + controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{ ImportPath: importPath, StructName: typeName, }) @@ -443,11 +380,11 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat if selExpr.Sel.Name != "Result" { return } - if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != revel.RevelImportPath { + if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != model.RevelImportPath { return } - method := &MethodSpec{ + method := &model.MethodSpec{ Name: funcDecl.Name.Name, } @@ -455,9 +392,9 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat for _, field := range funcDecl.Type.Params.List { for _, name := range field.Names { var importPath string - typeExpr := NewTypeExpr(pkgName, field.Type) + typeExpr := model.NewTypeExpr(pkgName, field.Type) if !typeExpr.Valid { - revel.RevelLog.Warnf("Didn't understand argument '%s' of action %s. Ignoring.", name, getFuncName(funcDecl)) + utils.Logger.Warn("Warn: 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 @@ -466,10 +403,10 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat } 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("")) + utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName("")) } } - method.Args = append(method.Args, &MethodArg{ + method.Args = append(method.Args, &model.MethodArg{ Name: name.Name, TypeExpr: typeExpr, ImportPath: importPath, @@ -479,7 +416,7 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat // Add a description of the calls to Render from the method. // Inspect every node (e.g. always return true). - method.RenderCalls = []*methodCall{} + method.RenderCalls = []*model.MethodCall{} ast.Inspect(funcDecl.Body, func(node ast.Node) bool { // Is it a function call? callExpr, ok := node.(*ast.CallExpr) @@ -501,7 +438,7 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat // Add this call's args to the renderArgs. pos := fset.Position(callExpr.Lparen) - methodCall := &methodCall{ + methodCall := &model.MethodCall{ Line: pos.Line, Names: []string{}, } @@ -541,7 +478,7 @@ func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPat // // 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 { +func getValidationKeys(fname string, fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string { var ( lineKeys = make(map[int]string) @@ -597,8 +534,11 @@ func getValidationKeys(fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[ return true } - if typeExpr := NewTypeExpr("", key); typeExpr.Valid { - lineKeys[fset.Position(callExpr.Pos()).Line] = typeExpr.TypeName("") + if typeExpr := model.NewTypeExpr("", key); typeExpr.Valid { + lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("") + } else { + utils.Logger.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", fname, + "line", fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String()) } return true }) @@ -624,21 +564,13 @@ func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) * continue } - if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == revel.RevelImportPath { + if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == model.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) { @@ -652,7 +584,7 @@ func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, } 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) + utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line) return } @@ -662,173 +594,6 @@ func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, 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:]) @@ -842,10 +607,10 @@ func importPathFromPath(root string) string { 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) + utils.Logger.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) + utils.Logger.Error("Unexpected! Code path is not in GOPATH:", "path", root) return "" } diff --git a/harness/reflect_test.go b/parser/reflect_test.go similarity index 94% rename from harness/reflect_test.go rename to parser/reflect_test.go index 74132b8..fed932a 100644 --- a/harness/reflect_test.go +++ b/parser/reflect_test.go @@ -2,7 +2,7 @@ // Revel Framework source code and usage is governed by a MIT style // license that can be found in the LICENSE file. -package harness +package parser import ( "go/ast" @@ -13,6 +13,7 @@ import ( "testing" "github.com/revel/revel" + "github.com/revel/cmd/model" ) const validationKeysSource = ` @@ -80,7 +81,7 @@ func TestGetValidationKeys(t *testing.T) { } for i, decl := range file.Decls { - lineKeys := getValidationKeys(fset, decl.(*ast.FuncDecl), map[string]string{"revel": revel.RevelImportPath}) + lineKeys := getValidationKeys("test", 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) @@ -93,7 +94,7 @@ func TestGetValidationKeys(t *testing.T) { } } -var TypeExprs = map[string]TypeExpr{ +var TypeExprs = map[string]model.TypeExpr{ "int": {"int", "", 0, true}, "*int": {"*int", "", 1, true}, "[]int": {"[]int", "", 2, true}, @@ -136,7 +137,7 @@ func TestTypeExpr(t *testing.T) { expr = &ast.Ellipsis{Ellipsis: expr.Pos(), Elt: expr} } - actual := NewTypeExpr("pkg", expr) + actual := model.NewTypeExpr("pkg", expr) if !reflect.DeepEqual(expected, actual) { t.Error("Fail, expected", expected, ", was", actual) } @@ -151,7 +152,7 @@ func TestProcessBookingSource(t *testing.T) { } controllerPackage := "github.com/revel/examples/booking/app/controllers" - expectedControllerSpecs := []*TypeInfo{ + expectedControllerSpecs := []*model.TypeInfo{ {"GorpController", controllerPackage, "controllers", nil, nil}, {"Application", controllerPackage, "controllers", nil, nil}, {"Hotels", controllerPackage, "controllers", nil, nil}, diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..278aa77 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,13 @@ +// 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 proxy for a Revel Framework. +// +// It has a following responsibilities: +// 1. Build and run the users program in a proxy +// 2. Monitor the user source and restart the program when necessary. +// +// Source files are generated in the app/tmp directory. + +package proxy diff --git a/revel/build.go b/revel/build.go index 3e0d56b..096c1e5 100644 --- a/revel/build.go +++ b/revel/build.go @@ -5,70 +5,86 @@ package main import ( - "fmt" "os" "path/filepath" "strings" "github.com/revel/cmd/harness" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "go/build" + "github.com/revel/cmd/utils" + "fmt" ) var cmdBuild = &Command{ - UsageLine: "build [import path] [target path] [run mode]", + UsageLine: "build -i [import path] -t [target path] -r [run mode]", Short: "build a Revel application (e.g. for deployment)", Long: ` Build the Revel web application named by the given import path. This allows it to be deployed and run on a machine that lacks a Go installation. -The run mode is used to select which set of app.conf configuration should -apply and may be used to determine logic in the application itself. - -Run mode defaults to "dev". - -WARNING: The target path will be completely deleted, if it already exists! - For example: - revel build github.com/revel/examples/chat /tmp/chat + revel build -a github.com/revel/examples/chat -t /tmp/chat + `, } func init() { - cmdBuild.Run = buildApp + cmdBuild.RunWith = buildApp + cmdBuild.UpdateConfig = updateBuildConfig } -func buildApp(args []string) { +// The update config updates the configuration command so that it can run +func updateBuildConfig(c *model.CommandConfig, args []string) (bool) { + c.Index = BUILD if len(args) < 2 { fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long) - return + return false + } + c.Build.ImportPath = args[0] + c.Build.TargetPath = args[1] + if len(args)>2 { + c.Build.Mode = args[1] + } + return true +} + +// The main entry point to build application from command line +func buildApp(c *model.CommandConfig) { + c.ImportPath = c.Build.ImportPath + appImportPath, destPath, mode := c.Build.ImportPath , c.Build.TargetPath, DefaultRunMode + if len(c.Build.Mode) > 0 { + mode = c.Build.Mode } - appImportPath, destPath, mode := args[0], args[1], DefaultRunMode - if len(args) >= 3 { - mode = args[2] - } + // Convert target to absolute path + destPath, _ = filepath.Abs(destPath) - if !revel.Initialized { - revel.Init(mode, appImportPath, "") - } + revel_paths := model.NewRevelPaths(mode, appImportPath, "", model.DoNothingRevelCallback) // First, verify that it is either already empty or looks like a previous // build (to avoid clobbering anything) - if exists(destPath) && !empty(destPath) && !exists(filepath.Join(destPath, "run.sh")) { - errorf("Abort: %s exists and does not look like a build directory.", destPath) + if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) { + utils.Logger.Errorf("Abort: %s exists and does not look like a build directory.", destPath) + return } if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) { - revel.RevelLog.Fatal("Remove all error","error", err) + utils.Logger.Error("Remove all error","error", err) + return } if err := os.MkdirAll(destPath, 0777); err != nil { - revel.RevelLog.Fatal("makedir error","error",err) + utils.Logger.Error("makedir error","error",err) + return } - app, reverr := harness.Build() - panicOnError(reverr, "Failed to build") + app, reverr := harness.Build(c,revel_paths) + if reverr!=nil { + utils.Logger.Error("Failed to build application","error",reverr) + return + } // Included are: // - run scripts @@ -79,15 +95,15 @@ func buildApp(args []string) { // Revel and the app are in a directory structure mirroring import path srcPath := filepath.Join(destPath, "src") destBinaryPath := filepath.Join(destPath, filepath.Base(app.BinaryPath)) - tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(revel.RevelImportPath)) - mustCopyFile(destBinaryPath, app.BinaryPath) - mustChmod(destBinaryPath, 0755) - _ = mustCopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel.RevelPath, "conf"), nil) - _ = mustCopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel.RevelPath, "templates"), nil) - _ = mustCopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel.BasePath, nil) + tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(model.RevelImportPath)) + utils.MustCopyFile(destBinaryPath, app.BinaryPath) + utils.MustChmod(destBinaryPath, 0755) + _ = utils.MustCopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil) + _ = utils.MustCopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil) + _ = utils.MustCopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil) // Find all the modules used and copy them over. - config := revel.Config.Raw() + config := revel_paths.Config.Raw() modulePaths := make(map[string]string) // import path => filesystem path for _, section := range config.Sections() { options, _ := config.SectionOptions(section) @@ -99,32 +115,46 @@ func buildApp(args []string) { if moduleImportPath == "" { continue } - modulePath, err := revel.ResolveImportPath(moduleImportPath) + + modPkg, err := build.Import(c.ImportPath, revel_paths.RevelPath, build.FindOnly) if err != nil { - revel.RevelLog.Fatalf("Failed to load module %s: %s", key[len("module."):], err) + utils.Logger.Fatalf("Failed to load module %s (%s): %s", key[len("module."):], c.ImportPath, err) } - modulePaths[moduleImportPath] = modulePath + modulePaths[moduleImportPath] = modPkg.Dir } } for importPath, fsPath := range modulePaths { - _ = mustCopyDir(filepath.Join(srcPath, importPath), fsPath, nil) + _ = utils.MustCopyDir(filepath.Join(srcPath, importPath), fsPath, nil) } - tmplData, runShPath := map[string]interface{}{ + tmplData := map[string]interface{}{ "BinName": filepath.Base(app.BinaryPath), "ImportPath": appImportPath, "Mode": mode, - }, filepath.Join(destPath, "run.sh") + } - mustRenderTemplate( - runShPath, - filepath.Join(revel.RevelPath, "..", "cmd", "revel", "package_run.sh.template"), - tmplData) - - mustChmod(runShPath, 0755) - - mustRenderTemplate( + utils.MustGenerateTemplate( + filepath.Join(destPath, "run.sh"), + PACKAGE_RUN_SH, + tmplData, + ) + utils.MustChmod(filepath.Join(destPath, "run.sh"), 0755) + utils.MustGenerateTemplate( filepath.Join(destPath, "run.bat"), - filepath.Join(revel.RevelPath, "..", "cmd", "revel", "package_run.bat.template"), - tmplData) + PACKAGE_RUN_BAT, + tmplData, + ) + + fmt.Println("Your application has been built in:", destPath) + } + +const PACKAGE_RUN_SH = `#!/bin/sh + +SCRIPTPATH=$(cd "$(dirname "$0")"; pwd) +"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode {{.Mode}} +` +const PACKAGE_RUN_BAT = `@echo off + +{{.BinName}} -importPath {{.ImportPath}} -srcPath "%CD%\src" -runMode {{.Mode}} +` diff --git a/revel/clean.go b/revel/clean.go index a05c58b..5eec24b 100644 --- a/revel/clean.go +++ b/revel/clean.go @@ -6,39 +6,52 @@ package main import ( "fmt" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" "go/build" "os" "path/filepath" ) var cmdClean = &Command{ - UsageLine: "clean [import path]", + UsageLine: "clean -i [import path]", Short: "clean a Revel application's temp files", Long: ` Clean the Revel web application named by the given import path. For example: - revel clean github.com/revel/examples/chat + revel clean -a github.com/revel/examples/chat It removes the app/tmp and app/routes directory. + + `, } func init() { - cmdClean.Run = cleanApp + cmdClean.UpdateConfig = updateCleanConfig + cmdClean.RunWith = cleanApp } -func cleanApp(args []string) { +// Update the clean command configuration, using old method +func updateCleanConfig(c *model.CommandConfig, args []string) bool { + c.Index = CLEAN if len(args) == 0 { fmt.Fprintf(os.Stderr, cmdClean.Long) - return + return false } + c.Clean.ImportPath = args[0] + return true +} - appPkg, err := build.Import(args[0], "", build.FindOnly) +// Clean the source directory of generated files +func cleanApp(c *model.CommandConfig) { + c.ImportPath = c.Clean.ImportPath + + appPkg, err := build.Import(c.ImportPath, "", build.FindOnly) if err != nil { - fmt.Fprintln(os.Stderr, "Abort: Failed to find import path:", err) - return + utils.Logger.Fatal("Abort: Failed to find import path:", "error", err) } purgeDirs := []string{ @@ -50,7 +63,7 @@ func cleanApp(args []string) { fmt.Println("Removing:", dir) err = os.RemoveAll(dir) if err != nil { - fmt.Fprintln(os.Stderr, "Abort:", err) + utils.Logger.Error("Failed to clean dir", "error", err) return } } diff --git a/revel/new.go b/revel/new.go index 2875e05..69c2dc7 100644 --- a/revel/new.go +++ b/revel/new.go @@ -14,11 +14,12 @@ import ( "path/filepath" "strings" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" ) var cmdNew = &Command{ - UsageLine: "new [path] [skeleton]", + UsageLine: "new -i [path] -s [skeleton]", Short: "create a skeleton Revel application", Long: ` New creates a few files to get a new Revel application running quickly. @@ -30,61 +31,113 @@ Skeleton is an optional argument, provided as an import path For example: - revel new import/path/helloworld + revel new -a import/path/helloworld + + revel new -a import/path/helloworld -s import/path/skeleton - revel new import/path/helloworld import/path/skeleton `, } func init() { - cmdNew.Run = newApp + cmdNew.RunWith = newApp + cmdNew.UpdateConfig = updateNewConfig } -var ( - - // go related paths - gopath string - gocmd string - srcRoot string - - // revel related paths - revelPkg *build.Package - revelCmdPkg *build.Package - appPath string - appName string - basePath string - importPath string - skeletonPath string -) - -func newApp(args []string) { - // check for proper args by count +// Called when unable to parse the command line automatically and assumes an old launch +func updateNewConfig(c *model.CommandConfig, args []string) bool { + c.Index = NEW if len(args) == 0 { - errorf("No import path given.\nRun 'revel help new' for usage.\n") + fmt.Fprintf(os.Stderr, cmdNew.Long) + return false } - if len(args) > 2 { - errorf("Too many arguments provided.\nRun 'revel help new' for usage.\n") + c.New.ImportPath = args[0] + if len(args)>1 { + c.New.Skeleton = args[1] + } + return true + +} + +// Call to create a new application +func newApp(c *model.CommandConfig) { + // check for proper args by count + c.ImportPath = c.New.ImportPath + c.SkeletonPath = c.New.Skeleton + + // Check for an existing folder so we dont clober it + c.AppPath = filepath.Join(c.SrcRoot, filepath.FromSlash(c.ImportPath)) + _, err := build.Import(c.ImportPath, "", build.FindOnly) + if err==nil || !utils.Empty(c.AppPath) { + utils.Logger.Fatal("Abort: Import path already exists.","path", c.ImportPath) + } + + if c.New.Vendored { + depPath, err := exec.LookPath("dep") + if err != nil { + // Do not halt build unless a new package needs to be imported + utils.Logger.Fatal("New: `dep` executable not found in PATH, but vendor folder requested." + + "You must install the dep tool before creating a vendored project. " + + "You can install the `dep` tool by doing a `go get -u github.com/golang/dep/cmd/dep`") + } + vendorPath := filepath.Join(c.ImportPath,"vendor") + if !utils.DirExists(vendorPath) { + err := os.MkdirAll(vendorPath,os.ModePerm) + utils.PanicOnError(err, "Failed to create " + vendorPath) + } + // In order for dep to run there needs to be a source file in the folder + tempPath := filepath.Join(c.ImportPath,"tmp") + if !utils.DirExists(tempPath) { + err := os.MkdirAll(tempPath,os.ModePerm) + utils.PanicOnError(err, "Failed to create " + vendorPath) + err = utils.MustGenerateTemplate(filepath.Join(tempPath,"main.go"), NEW_MAIN_FILE, nil) + utils.PanicOnError(err, "Failed to create main file " + vendorPath) + + } + packageFile := filepath.Join(c.ImportPath,"Gopkg.toml") + if !utils.Exists(packageFile) { + utils.MustGenerateTemplate(packageFile,VENDOR_GOPKG,nil) + } else { + utils.Logger.Info("Package file exists in skeleto, skipping adding") + } + + getCmd := exec.Command(depPath, "ensure", "-v") + getCmd.Dir = c.ImportPath + utils.Logger.Info("Exec:", "args", getCmd.Args) + getCmd.Dir = c.ImportPath + getOutput, err := getCmd.CombinedOutput() + if err != nil { + utils.Logger.Fatal(string(getOutput)) + } + + // TODO build.Default.GOPATH = build.Default.GOPATH + string(os.PathListSeparator) + c.ImportPath } - // checking and setting go paths - initGoPaths() // checking and setting application - setApplicationPath(args) + setApplicationPath(c) // checking and setting skeleton - setSkeletonPath(args) + setSkeletonPath(c) // copy files to new app directory - copyNewAppFiles() + copyNewAppFiles(c) + // goodbye world - fmt.Fprintln(os.Stdout, "Your application is ready:\n ", appPath) - fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run", importPath) + fmt.Fprintln(os.Stdout, "Your application is ready:\n ", c.AppPath) + // Check to see if it should be run right off + if c.New.Run { + c.Run.ImportPath = c.ImportPath + runApp(c) + } else { + fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run -a ", c.ImportPath) + } } +// Used to generate a new secret key const alphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +// Generate a secret key func generateSecret() string { chars := make([]byte, 64) for i := 0; i < 64; i++ { @@ -93,129 +146,145 @@ func generateSecret() string { return string(chars) } -// lookup and set Go related variables -func initGoPaths() { - // lookup go path - gopath = build.Default.GOPATH - if gopath == "" { - errorf("Abort: GOPATH environment variable is not set. " + - "Please refer to http://golang.org/doc/code.html to configure your Go environment.") - } - - // check for go executable - var err error - gocmd, err = exec.LookPath("go") - if err != nil { - errorf("Go executable not found in PATH.") - } - - // revel/revel#1004 choose go path relative to current working directory - workingDir, _ := os.Getwd() - goPathList := filepath.SplitList(gopath) - for _, path := range goPathList { - if strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { - srcRoot = path - break - } - - path, _ = filepath.EvalSymlinks(path) - if len(path) > 0 && strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { - srcRoot = path - break - } - } - - if len(srcRoot) == 0 { - revel.RevelLog.Fatal("Abort: could not create a Revel application outside of GOPATH.") - } - - // set go src path - srcRoot = filepath.Join(srcRoot, "src") -} - -func setApplicationPath(args []string) { - var err error - importPath = args[0] +// Sets the applicaiton path +func setApplicationPath(c *model.CommandConfig) { // revel/revel#1014 validate relative path, we cannot use built-in functions // since Go import path is valid relative path too. // so check basic part of the path, which is "." - if filepath.IsAbs(importPath) || strings.HasPrefix(importPath, ".") { - errorf("Abort: '%s' looks like a directory. Please provide a Go import path instead.", - importPath) + if filepath.IsAbs(c.ImportPath) || strings.HasPrefix(c.ImportPath, ".") { + utils.Logger.Fatalf("Abort: '%s' looks like a directory. Please provide a Go import path instead.", + c.ImportPath) } - appPath = filepath.Join(srcRoot, filepath.FromSlash(importPath)) - _, err = build.Import(importPath, "", build.FindOnly) - if err == nil && !empty(appPath) { - errorf("Abort: Import path %s already exists.\n", importPath) + // If we are running a vendored version of Revel we do not need to check for it. + if !c.New.Vendored { + var err error + _, err = build.Import(model.RevelImportPath, "", build.FindOnly) + if err != nil { + // Go get the revel project + + utils.Logger.Fatal("Abort: Could not find Revel source code:", "error", err) + } } - revelPkg, err = build.Import(revel.RevelImportPath, "", build.FindOnly) - if err != nil { - errorf("Abort: Could not find Revel source code: %s\n", err) - } + c.AppName = filepath.Base(c.AppPath) + c.BasePath = filepath.ToSlash(filepath.Dir(c.ImportPath)) - appName = filepath.Base(appPath) - basePath = filepath.ToSlash(filepath.Dir(importPath)) - - if basePath == "." { + if c.BasePath == "." { // we need to remove the a single '.' when // the app is in the $GOROOT/src directory - basePath = "" + c.BasePath = "" } else { // we need to append a '/' when the app is // is a subdirectory such as $GOROOT/src/path/to/revelapp - basePath += "/" + c.BasePath += "/" } } -func setSkeletonPath(args []string) { +// Set the skeleton path +func setSkeletonPath(c *model.CommandConfig) { var err error - if len(args) == 2 { // user specified - skeletonName := args[1] - _, err = build.Import(skeletonName, "", build.FindOnly) + if len(c.SkeletonPath) > 0 { // user specified + + _, err = build.Import(c.SkeletonPath, "", build.FindOnly) if err != nil { // Execute "go get " - getCmd := exec.Command(gocmd, "get", "-d", skeletonName) + getCmd := exec.Command(c.GoCmd, "get", "-d", c.SkeletonPath) fmt.Println("Exec:", getCmd.Args) getOutput, err := getCmd.CombinedOutput() // check getOutput for no buildible string bpos := bytes.Index(getOutput, []byte("no buildable Go source files in")) if err != nil && bpos == -1 { - errorf("Abort: Could not find or 'go get' Skeleton source code: %s\n%s\n", getOutput, skeletonName) + utils.Logger.Fatalf("Abort: Could not find or 'go get' Skeleton source code: %s\n%s\n", getOutput, c.SkeletonPath) } } // use the - skeletonPath = filepath.Join(srcRoot, skeletonName) + c.SkeletonPath = filepath.Join(c.SrcRoot, c.SkeletonPath) } else { // use the revel default - revelCmdPkg, err = build.Import(RevelCmdImportPath, "", build.FindOnly) + revelCmdPkg, err := build.Import(RevelCmdImportPath, "", build.FindOnly) if err != nil { - errorf("Abort: Could not find Revel Cmd source code: %s\n", err) + utils.Logger.Fatalf("Abort: Could not find Revel Cmd source code: %s\n", err) } - skeletonPath = filepath.Join(revelCmdPkg.Dir, "revel", "skeleton") + c.SkeletonPath = filepath.Join(revelCmdPkg.Dir, "revel", "skeleton") } } -func copyNewAppFiles() { +func copyNewAppFiles(c *model.CommandConfig) { var err error - err = os.MkdirAll(appPath, 0777) - panicOnError(err, "Failed to create directory "+appPath) + err = os.MkdirAll(c.AppPath, 0777) + utils.PanicOnError(err, "Failed to create directory "+c.AppPath) - _ = mustCopyDir(appPath, skeletonPath, map[string]interface{}{ + _ = utils.MustCopyDir(c.AppPath, c.SkeletonPath, map[string]interface{}{ // app.conf - "AppName": appName, - "BasePath": basePath, + "AppName": c.AppName, + "BasePath": c.BasePath, "Secret": generateSecret(), }) // Dotfiles are skipped by mustCopyDir, so we have to explicitly copy the .gitignore. gitignore := ".gitignore" - mustCopyFile(filepath.Join(appPath, gitignore), filepath.Join(skeletonPath, gitignore)) + utils.MustCopyFile(filepath.Join(c.AppPath, gitignore), filepath.Join(c.SkeletonPath, gitignore)) } + +const ( + VENDOR_GOPKG = `# +# Revel Gopkg.toml +# +# If you want to use a specific version of Revel change the branches below +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +required = ["github.com/revel/cmd/revel"] + +[[override]] + branch = "master" + name = "github.com/revel/modules" + +[[override]] + branch = "master" + name = "github.com/revel/revel" + +[[override]] + branch = "master" + name = "github.com/revel/cmd" + +[[override]] + branch = "master" + name = "github.com/revel/log15" + +[[override]] + branch = "master" + name = "github.com/revel/cron" + +[[override]] + branch = "master" + name = "github.com/xeonx/timeago" + +` + NEW_MAIN_FILE = `package main + + ` +) \ No newline at end of file diff --git a/revel/package.go b/revel/package.go index 33db250..fafc731 100644 --- a/revel/package.go +++ b/revel/package.go @@ -10,11 +10,12 @@ import ( "os" "path/filepath" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" ) var cmdPackage = &Command{ - UsageLine: "package [import path] [run mode]", + UsageLine: "package -i [import path] -r [run mode]", Short: "package a Revel application (e.g. for deployment)", Long: ` Package the Revel web application named by the given import path. @@ -27,43 +28,60 @@ Run mode defaults to "dev". For example: - revel package github.com/revel/examples/chat + revel package -i github.com/revel/examples/chat `, } func init() { - cmdPackage.Run = packageApp + cmdPackage.RunWith = packageApp + cmdPackage.UpdateConfig = updatePackageConfig } -func packageApp(args []string) { +// Called when unable to parse the command line automatically and assumes an old launch +func updatePackageConfig(c *model.CommandConfig, args []string) bool { + c.Index = PACAKAGE if len(args) == 0 { - fmt.Fprint(os.Stderr, cmdPackage.Long) - return + fmt.Fprintf(os.Stderr, cmdPackage.Long) + return false } + c.New.ImportPath = args[0] + if len(args)>1 { + c.New.Skeleton = args[1] + } + return true + +} + +func packageApp(c *model.CommandConfig) { // Determine the run mode. mode := DefaultRunMode - if len(args) >= 2 { - mode = args[1] + if len(c.Package.Mode) >= 0 { + mode = c.Package.Mode } - appImportPath := args[0] - revel.Init(mode, appImportPath, "") + appImportPath := c.Package.ImportPath + revel_paths := model.NewRevelPaths(mode, appImportPath, "", model.DoNothingRevelCallback) // Remove the archive if it already exists. - destFile := filepath.Base(revel.BasePath) + ".tar.gz" + destFile := filepath.Base(revel_paths.BasePath) + ".tar.gz" if err := os.Remove(destFile); err != nil && !os.IsNotExist(err) { - revel.RevelLog.Fatal("Unable to remove target file","error",err,"file",destFile) + utils.Logger.Error("Unable to remove target file","error",err,"file",destFile) + os.Exit(1) } // Collect stuff in a temp directory. - tmpDir, err := ioutil.TempDir("", filepath.Base(revel.BasePath)) - panicOnError(err, "Failed to get temp dir") + tmpDir, err := ioutil.TempDir("", filepath.Base(revel_paths.BasePath)) + utils.PanicOnError(err, "Failed to get temp dir") - buildApp([]string{args[0], tmpDir, mode}) + // Build expects the command the build to contain the proper data + c.Build.ImportPath = appImportPath + c.Build.Mode = mode + c.Build.TargetPath = tmpDir + buildApp(c) // Create the zip file. - archiveName := mustTarGzDir(destFile, tmpDir) + archiveName := utils.MustTarGzDir(destFile, tmpDir) fmt.Println("Your archive is ready:", archiveName) } diff --git a/revel/package_run.bat.template b/revel/package_run.bat.template deleted file mode 100644 index cce164f..0000000 --- a/revel/package_run.bat.template +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -{{.BinName}} -importPath {{.ImportPath}} -srcPath "%CD%\src" -runMode {{.Mode}} diff --git a/revel/package_run.sh.template b/revel/package_run.sh.template deleted file mode 100644 index 69bcaa6..0000000 --- a/revel/package_run.sh.template +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -SCRIPTPATH=$(cd "$(dirname "$0")"; pwd) -"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode {{.Mode}} diff --git a/revel/rev.go b/revel/rev.go deleted file mode 100644 index 2f8b263..0000000 --- a/revel/rev.go +++ /dev/null @@ -1,144 +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. - -// The command line tool for running Revel apps. -package main - -import ( - "flag" - "fmt" - "io" - "math/rand" - "os" - "runtime" - "strings" - "text/template" - "time" - - "github.com/agtorre/gocolorize" -) - -const ( - // RevelCmdImportPath Revel framework cmd tool import path - RevelCmdImportPath = "github.com/revel/cmd" - - // DefaultRunMode for revel's application - DefaultRunMode = "dev" -) - -// Command structure cribbed from the genius organization of the "go" command. -type Command struct { - Run func(args []string) - UsageLine, Short, Long string -} - -// Name returns command name from usage line -func (cmd *Command) Name() string { - name := cmd.UsageLine - i := strings.Index(name, " ") - if i >= 0 { - name = name[:i] - } - return name -} - -var commands = []*Command{ - cmdNew, - cmdRun, - cmdBuild, - cmdPackage, - cmdClean, - cmdTest, - cmdVersion, -} - -func main() { - if runtime.GOOS == "windows" { - gocolorize.SetPlain(true) - } - fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header)) - flag.Usage = func() { usage(1) } - flag.Parse() - args := flag.Args() - - if len(args) < 1 || args[0] == "help" { - if len(args) == 1 { - usage(0) - } - if len(args) > 1 { - for _, cmd := range commands { - if cmd.Name() == args[1] { - tmpl(os.Stdout, helpTemplate, cmd) - return - } - } - } - usage(2) - } - - // Commands use panic to abort execution when something goes wrong. - // Panics are logged at the point of error. Ignore those. - defer func() { - if err := recover(); err != nil { - if _, ok := err.(LoggedError); !ok { - // This panic was not expected / logged. - panic(err) - } - os.Exit(1) - } - }() - - for _, cmd := range commands { - if cmd.Name() == args[0] { - cmd.Run(args[1:]) - return - } - } - - errorf("unknown command %q\nRun 'revel help' for usage.\n", args[0]) -} - -func errorf(format string, args ...interface{}) { - // Ensure the user's command prompt starts on the next line. - if !strings.HasSuffix(format, "\n") { - format += "\n" - } - fmt.Fprintf(os.Stderr, format, args...) - panic(LoggedError{}) // Panic instead of os.Exit so that deferred will run. -} - -const header = `~ -~ revel! http://revel.github.io -~ -` - -const usageTemplate = `usage: revel command [arguments] - -The commands are: -{{range .}} - {{.Name | printf "%-11s"}} {{.Short}}{{end}} - -Use "revel help [command]" for more information. -` - -var helpTemplate = `usage: revel {{.UsageLine}} -{{.Long}} -` - -func usage(exitCode int) { - tmpl(os.Stderr, usageTemplate, commands) - os.Exit(exitCode) -} - -func tmpl(w io.Writer, text string, data interface{}) { - t := template.New("top") - template.Must(t.Parse(text)) - if err := t.Execute(w, data); err != nil { - panic(err) - } -} - -func init() { - rand.Seed(time.Now().UnixNano()) -} diff --git a/revel/revel.go b/revel/revel.go new file mode 100644 index 0000000..ae967d5 --- /dev/null +++ b/revel/revel.go @@ -0,0 +1,300 @@ +// 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. + +// The command line tool for running Revel apps. +package main + +import ( + "flag" + "fmt" + "io" + "math/rand" + "os" + "runtime" + "strings" + "text/template" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/agtorre/gocolorize" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "github.com/revel/cmd/logger" + "os/exec" + "path/filepath" + "go/build" +) + +const ( + // RevelCmdImportPath Revel framework cmd tool import path + RevelCmdImportPath = "github.com/revel/cmd" + + // DefaultRunMode for revel's application + DefaultRunMode = "dev" +) + +// Command structure cribbed from the genius organization of the "go" command. +type Command struct { + UpdateConfig func(c *model.CommandConfig, args []string) bool + RunWith func(c *model.CommandConfig) + UsageLine, Short, Long string +} + +// Name returns command name from usage line +func (cmd *Command) Name() string { + name := cmd.UsageLine + i := strings.Index(name, " ") + if i >= 0 { + name = name[:i] + } + return name +} + +// The constants +const ( + NEW model.COMMAND = iota +1 + RUN + BUILD + PACAKAGE + CLEAN + TEST + VERSION +) +// The commands +var commands = []*Command{ + nil, // Safety net, prevent missing index from running + cmdNew, + cmdRun, + cmdBuild, + cmdPackage, + cmdClean, + cmdTest, + cmdVersion, +} +func main() { + if runtime.GOOS == "windows" { + gocolorize.SetPlain(true) + } + c := &model.CommandConfig{} + wd,_ := os.Getwd() + + utils.InitLogger(wd,logger.LvlError) + + parser := flags.NewParser(c, flags.HelpFlag | flags.PassDoubleDash) + if ini:=flag.String("ini","none","");*ini!="none" { + if err:=flags.NewIniParser(parser).ParseFile(*ini);err!=nil { + utils.Logger.Error("Unable to load ini", "error",err) + } + } else { + if _, err := parser.Parse(); err != nil { + utils.Logger.Info("Command line options failed", "error", err.Error()) + + // Decode nature of error + if perr,ok:=err.(*flags.Error); ok { + if perr.Type == flags.ErrRequired { + // Try the old way + if !main_parse_old(c) { + println("Command line error:", err.Error()) + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + } else { + println("Command line error:", err.Error()) + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + } else { + println("Command line error:", err.Error()) + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + } else { + switch parser.Active.Name { + case "new": + c.Index = NEW + case "run": + c.Index = RUN + case "build": + c.Index = BUILD + case "package": + c.Index = PACAKAGE + case "clean": + c.Index = CLEAN + case "test": + c.Index = TEST + case "version": + c.Index = VERSION + } + } + } + + // Switch based on the verbose flag + if c.Verbose { + utils.InitLogger(wd, logger.LvlDebug) + } else { + utils.InitLogger(wd, logger.LvlWarn) + } + println("Revel executing:", commands[c.Index].Short) + // checking and setting go paths + initGoPaths(c) + + commands[c.Index].RunWith(c) + + +} + +// Try to populate the CommandConfig using the old techniques +func main_parse_old(c *model.CommandConfig) bool { + // Take the old command format and try to parse them + flag.Usage = func() { usage(1) } + flag.Parse() + args := flag.Args() + + if len(args) < 1 || args[0] == "help" { + if len(args) == 1 { + usage(0) + } + + if len(args) > 1 { + for _, cmd := range commands { + if cmd!=nil && cmd.Name() == args[1] { + tmpl(os.Stdout, helpTemplate, cmd) + return false + } + } + } + usage(2) + } + + for _, cmd := range commands { + if cmd!=nil && cmd.Name() == args[0] { + println("Running", cmd.Name()) + return cmd.UpdateConfig(c, args[1:]) + } + } + + return false +} + +func main_old() { + if runtime.GOOS == "windows" { + gocolorize.SetPlain(true) + } + fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header)) + flag.Usage = func() { usage(1) } + flag.Parse() + args := flag.Args() + + if len(args) < 1 || args[0] == "help" { + if len(args) == 1 { + usage(0) + } + if len(args) > 1 { + for _, cmd := range commands { + if cmd.Name() == args[1] { + tmpl(os.Stdout, helpTemplate, cmd) + return + } + } + } + usage(2) + } + + // Commands use panic to abort execution when something goes wrong. + // Panics are logged at the point of error. Ignore those. + defer func() { + if err := recover(); err != nil { + if _, ok := err.(utils.LoggedError); !ok { + // This panic was not expected / logged. + panic(err) + } + os.Exit(1) + } + }() + + //for _, cmd := range commands { + // if cmd.Name() == args[0] { + // cmd.UpdateConfig(args[1:]) + // return + // } + //} + + utils.Logger.Fatalf("unknown command %q\nRun 'revel help' for usage.\n", args[0]) +} + +const header = `~ +~ revel! http://revel.github.io +~ +` + +const usageTemplate = `usage: revel command [arguments] + +The commands are: +{{range .}} + {{.Name | printf "%-11s"}} {{.Short}}{{end}} + +Use "revel help [command]" for more information. +` + +var helpTemplate = `usage: revel {{.UsageLine}} +{{.Long}} +` + +func usage(exitCode int) { + tmpl(os.Stderr, usageTemplate, commands) + os.Exit(exitCode) +} + +func tmpl(w io.Writer, text string, data interface{}) { + t := template.New("top") + template.Must(t.Parse(text)) + if err := t.Execute(w, data); err != nil { + panic(err) + } +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// lookup and set Go related variables +func initGoPaths(c *model.CommandConfig) { + // lookup go path + c.GoPath = build.Default.GOPATH + if c.GoPath == "" { + utils.Logger.Fatal("Abort: GOPATH environment variable is not set. " + + "Please refer to http://golang.org/doc/code.html to configure your Go environment.") + } + + // check for go executable + var err error + c.GoCmd, err = exec.LookPath("go") + if err != nil { + utils.Logger.Fatal("Go executable not found in PATH.") + } + + // revel/revel#1004 choose go path relative to current working directory + workingDir, _ := os.Getwd() + goPathList := filepath.SplitList(c.GoPath) + for _, path := range goPathList { + if strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { + c.SrcRoot = path + break + } + + path, _ = filepath.EvalSymlinks(path) + if len(path) > 0 && strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) { + c.SrcRoot = path + break + } + } + + if len(c.SrcRoot) == 0 { + utils.Logger.Fatal("Abort: could not create a Revel application outside of GOPATH.") + } + + // set go src path + c.SrcRoot = filepath.Join(c.SrcRoot, "src") +} \ No newline at end of file diff --git a/revel/run.go b/revel/run.go index a8cb8b5..72b54d2 100644 --- a/revel/run.go +++ b/revel/run.go @@ -5,11 +5,13 @@ package main import ( - "go/build" "strconv" + "fmt" "github.com/revel/cmd/harness" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "go/build" ) var cmdRun = &Command{ @@ -40,49 +42,41 @@ type RunArgs struct { } func init() { - cmdRun.Run = runApp + cmdRun.RunWith = runApp + cmdRun.UpdateConfig = updateRunConfig } -func parseRunArgs(args []string) *RunArgs { - inputArgs := RunArgs{ - ImportPath: importPathFromCurrentDir(), - Mode: DefaultRunMode, - Port: revel.HTTPPort, - } +func updateRunConfig(c *model.CommandConfig, args []string) bool { + switch len(args) { case 3: // Possible combinations // revel run [import-path] [run-mode] [port] - port, err := strconv.Atoi(args[2]) - if err != nil { - errorf("Failed to parse port as integer: %s", args[2]) - } - inputArgs.ImportPath = args[0] - inputArgs.Mode = args[1] - inputArgs.Port = port + c.Run.ImportPath = args[0] + c.Run.Mode = args[1] + c.Run.Port = args[2] case 2: // Possible combinations // 1. revel run [import-path] [run-mode] // 2. revel run [import-path] [port] // 3. revel run [run-mode] [port] + + // Check to see if the import path evaluates out to something that may be on a gopath if _, err := build.Import(args[0], "", build.FindOnly); err == nil { // 1st arg is the import path - inputArgs.ImportPath = args[0] - if port, err := strconv.Atoi(args[1]); err == nil { + c.Run.ImportPath = args[0] + + if _, err := strconv.Atoi(args[1]); err == nil { // 2nd arg is the port number - inputArgs.Port = port + c.Run.Port = args[1] } else { // 2nd arg is the run mode - inputArgs.Mode = args[1] + c.Run.Mode = args[1] } } else { // 1st arg is the run mode - port, err := strconv.Atoi(args[1]) - if err != nil { - errorf("Failed to parse port as integer: %s", args[1]) - } - inputArgs.Mode = args[0] - inputArgs.Port = port + c.Run.Mode = args[0] + c.Run.Port = args[1] } case 1: // Possible combinations @@ -91,52 +85,62 @@ func parseRunArgs(args []string) *RunArgs { // 3. revel run [run-mode] _, err := build.Import(args[0], "", build.FindOnly) if err != nil { - revel.RevelLog.Warn("Unable to run using an import path, assuming import path is working directory %s %s", "Argument", args[0], "error", err.Error()) + utils.Logger.Warn("Unable to run using an import path, assuming import path is working directory %s %s", "Argument", args[0], "error", err.Error()) } - println("Trying to build with", args[0], err) + utils.Logger.Info("Trying to build with", args[0], err) if err == nil { // 1st arg is the import path - inputArgs.ImportPath = args[0] - } else if port, err := strconv.Atoi(args[0]); err == nil { + c.Run.ImportPath = args[0] + } else if _, err := strconv.Atoi(args[0]); err == nil { // 1st arg is the port number - inputArgs.Port = port + c.Run.Port = args[0] } else { // 1st arg is the run mode - inputArgs.Mode = args[0] + c.Run.Mode = args[0] } } - - return &inputArgs + c.Index = RUN + return true } -func runApp(args []string) { - runArgs := parseRunArgs(args) - - // Find and parse app.conf - revel.Init(runArgs.Mode, runArgs.ImportPath, "") - revel.LoadMimeConfig() - - // fallback to default port - if runArgs.Port == 0 { - runArgs.Port = revel.HTTPPort +func runApp(c *model.CommandConfig) { + if c.Run.Mode == "" { + c.Run.Mode = "dev" } - revel.RevelLog.Infof("Running %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, runArgs.Mode) - revel.RevelLog.Debug("Base path:", "path", revel.BasePath) + revel_path := model.NewRevelPaths(c.Run.Mode, c.Run.ImportPath, "", model.DoNothingRevelCallback) + if c.Run.Port != "" { + port, err := strconv.Atoi(c.Run.Port) + if err != nil { + utils.Logger.Fatalf("Failed to parse port as integer: %s", c.Run.Port) + } + revel_path.HTTPPort = port + } + + utils.Logger.Infof("Running %s (%s) in %s mode\n", revel_path.AppName, revel_path.ImportPath, revel_path.RunMode) + utils.Logger.Debug("Base path:", "path", revel_path.BasePath) // If the app is run in "watched" mode, use the harness to run it. - if revel.Config.BoolDefault("watch", true) && revel.Config.BoolDefault("watch.code", true) { - revel.RevelLog.Debug("Running in watched mode.") - revel.HTTPPort = runArgs.Port - harness.NewHarness().Run() // Never returns. + if revel_path.Config.BoolDefault("watch", true) && revel_path.Config.BoolDefault("watch.code", true) { + utils.Logger.Info("Running in watched mode.") + runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v}`, revel_path.RunMode, c.Verbose) + if c.HistoricMode { + runMode = revel_path.RunMode + } + // **** Never returns. + harness.NewHarness(c, revel_path, runMode, c.Run.NoProxy).Run() } // Else, just build and run the app. - revel.RevelLog.Debug("Running in live build mode.") - app, err := harness.Build() + utils.Logger.Debug("Running in live build mode.") + app, err := harness.Build(c, revel_path) if err != nil { - errorf("Failed to build app: %s", err) + utils.Logger.Errorf("Failed to build app: %s", err) } - app.Port = runArgs.Port - app.Cmd().Run() + app.Port = revel_path.HTTPPort + runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v}`, app.Paths.RunMode, c.Verbose) + if c.HistoricMode { + runMode = revel_path.RunMode + } + app.Cmd(runMode).Run() } diff --git a/revel/skeleton/app/init.go b/revel/skeleton/app/init.go index 69540e8..117dd4e 100644 --- a/revel/skeleton/app/init.go +++ b/revel/skeleton/app/init.go @@ -26,6 +26,7 @@ func init() { HeaderFilter, // Add some security based headers revel.InterceptorFilter, // Run interceptors around the action. revel.CompressFilter, // Compress the result. + revel.BeforeAfterFilter, // Call the before and after filter functions revel.ActionInvoker, // Invoke the action. } diff --git a/revel/skeleton/conf/app.conf.template b/revel/skeleton/conf/app.conf.template index 2b57895..ef087ce 100644 --- a/revel/skeleton/conf/app.conf.template +++ b/revel/skeleton/conf/app.conf.template @@ -143,7 +143,7 @@ watch = true # Rebuild when a new request is received and changes have been detected. # "eager" # Rebuild as soon as changes are detected. -watch.mode = normal +watch.mode = eager # Watch the entire `$GOPATH` for changes. # Values: diff --git a/revel/test.go b/revel/test.go index e7135a2..c04d03f 100644 --- a/revel/test.go +++ b/revel/test.go @@ -16,8 +16,9 @@ import ( "time" "github.com/revel/cmd/harness" - "github.com/revel/modules/testrunner/app/controllers" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "github.com/revel/cmd/tests" + "github.com/revel/cmd/utils" ) var cmdTest = &Command{ @@ -47,80 +48,104 @@ or one of UserTest's methods: } func init() { - cmdTest.Run = testApp + cmdTest.RunWith = testApp + cmdTest.UpdateConfig = updateTestConfig } -func testApp(args []string) { - var err error - if len(args) == 0 { - errorf("No import path given.\nRun 'revel help test' for usage.\n") +// Called to update the config command with from the older stype +func updateTestConfig(c *model.CommandConfig, args []string) bool { + c.Index = TEST + // The full test runs + // revel test (run mode) (suite(.function)) + if len(args) < 1 { + return false } + c.Test.ImportPath = args[0] + if len(args) > 1 { + c.Test.Mode = args[1] + } + if len(args) > 2 { + c.Test.Function = args[2] + } + return true +} + +// Called to test the application +func testApp(c *model.CommandConfig) { + var err error mode := DefaultRunMode - if len(args) >= 2 { - mode = args[1] + if c.Test.Mode != "" { + mode = c.Test.Mode } // Find and parse app.conf - revel.Init(mode, args[0], "") + revel_path := model.NewRevelPaths(mode, c.Test.ImportPath, "", model.DoNothingRevelCallback) // Ensure that the testrunner is loaded in this mode. - checkTestRunner() + // todo checkTestRunner() // Create a directory to hold the test result files. - resultPath := filepath.Join(revel.BasePath, "test-results") + resultPath := filepath.Join(revel_path.BasePath, "test-results") if err = os.RemoveAll(resultPath); err != nil { - errorf("Failed to remove test result directory %s: %s", resultPath, err) + utils.Logger.Errorf("Failed to remove test result directory %s: %s", resultPath, err) } if err = os.Mkdir(resultPath, 0777); err != nil { - errorf("Failed to create test result directory %s: %s", resultPath, err) + utils.Logger.Errorf("Failed to create test result directory %s: %s", resultPath, err) } // Direct all the output into a file in the test-results directory. file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { - errorf("Failed to create test result log file: %s", err) + utils.Logger.Errorf("Failed to create test result log file: %s", err) } - app, reverr := harness.Build() + app, reverr := harness.Build(c, revel_path) if reverr != nil { - errorf("Error building: %s", reverr) + utils.Logger.Errorf("Error building: %s", reverr) } - cmd := app.Cmd() + runMode := fmt.Sprintf(`{"mode":"%s","testModeFlag":true, "specialUseFlag":%v}`, app.Paths.RunMode, c.Verbose) + if c.HistoricMode { + runMode = app.Paths.RunMode + } + cmd := app.Cmd(runMode) + cmd.Stderr = io.MultiWriter(cmd.Stderr, file) cmd.Stdout = io.MultiWriter(cmd.Stderr, file) // Start the app... - if err := cmd.Start(); err != nil { - errorf("%s", err) + if err := cmd.Start(c); err != nil { + utils.Logger.Errorf("%s", err) } defer cmd.Kill() - revel.INFO.Printf("Testing %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode) - var httpAddr = revel.HTTPAddr + var httpAddr = revel_path.HTTPAddr if httpAddr == "" { - httpAddr = "127.0.0.1" + httpAddr = "localhost" } var httpProto = "http" - if revel.HTTPSsl { + if revel_path.HTTPSsl { httpProto = "https" } // Get a list of tests - var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel.HTTPPort) + var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel_path.HTTPPort) + + utils.Logger.Infof("Testing %s (%s) in %s mode URL %s \n", revel_path.AppName, revel_path.ImportPath, mode, baseURL) testSuites, _ := getTestsList(baseURL) // If a specific TestSuite[.Method] is specified, only run that suite/test - if len(args) == 3 { - testSuites = filterTestSuites(testSuites, args[2]) + if c.Test.Function != "" { + testSuites = filterTestSuites(testSuites, c.Test.Function) } + testSuiteCount := len(*testSuites) fmt.Printf("\n%d test suite%s to run.\n", testSuiteCount, pluralize(testSuiteCount, "", "s")) fmt.Println() // Run each suite. - failedResults, overallSuccess := runTestSuites(baseURL, resultPath, testSuites) + failedResults, overallSuccess := runTestSuites(revel_path, baseURL, resultPath, testSuites) fmt.Println() if overallSuccess { @@ -137,16 +162,18 @@ func testApp(args []string) { } } writeResultFile(resultPath, "result.failed", "failed") - errorf("Some tests failed. See file://%s for results.", resultPath) + utils.Logger.Errorf("Some tests failed. See file://%s for results.", resultPath) } } +// Outputs the results to a file func writeResultFile(resultPath, name, content string) { if err := ioutil.WriteFile(filepath.Join(resultPath, name), []byte(content), 0666); err != nil { - errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err) + utils.Logger.Errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err) } } +// Determines if response should be plural func pluralize(num int, singular, plural string) string { if num == 1 { return singular @@ -156,7 +183,7 @@ func pluralize(num int, singular, plural string) string { // Filters test suites and individual tests to match // the parsed command line parameter -func filterTestSuites(suites *[]controllers.TestSuiteDesc, suiteArgument string) *[]controllers.TestSuiteDesc { +func filterTestSuites(suites *[]tests.TestSuiteDesc, suiteArgument string) *[]tests.TestSuiteDesc { var suiteName, testName string argArray := strings.Split(suiteArgument, ".") suiteName = argArray[0] @@ -171,54 +198,34 @@ func filterTestSuites(suites *[]controllers.TestSuiteDesc, suiteArgument string) continue } if testName == "" { - return &[]controllers.TestSuiteDesc{suite} + return &[]tests.TestSuiteDesc{suite} } // Only run a particular test in a suite for _, test := range suite.Tests { if test.Name != testName { continue } - return &[]controllers.TestSuiteDesc{ + return &[]tests.TestSuiteDesc{ { Name: suite.Name, - Tests: []controllers.TestDesc{test}, + Tests: []tests.TestDesc{test}, }, } } - errorf("Couldn't find test %s in suite %s", testName, suiteName) + utils.Logger.Errorf("Couldn't find test %s in suite %s", testName, suiteName) } - errorf("Couldn't find test suite %s", suiteName) + utils.Logger.Errorf("Couldn't find test suite %s", suiteName) return nil } -func checkTestRunner() { - testRunnerFound := false - for _, module := range revel.Modules { - if module.ImportPath == revel.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") { - testRunnerFound = true - break - } - } - - if !testRunnerFound { - errorf(`Error: The testrunner module is not running. - -You can add it to a run mode configuration with the following line: - - module.testrunner = github.com/revel/modules/testrunner - -`) - } -} - // Get a list of tests from server. // Since this is the first request to the server, retry/sleep a couple times // in case it hasn't finished starting up yet. -func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) { +func getTestsList(baseURL string) (*[]tests.TestSuiteDesc, error) { var ( err error resp *http.Response - testSuites []controllers.TestSuiteDesc + testSuites []tests.TestSuiteDesc ) for i := 0; ; i++ { if resp, err = http.Get(baseURL + "/@tests.list"); err == nil { @@ -231,9 +238,9 @@ func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) { continue } if err != nil { - errorf("Failed to request test list: %s", err) + utils.Logger.Fatalf("Failed to request test list: %s %s", baseURL, err) } else { - errorf("Failed to request test list: non-200 response") + utils.Logger.Fatalf("Failed to request test list: non-200 response %s", baseURL) } } defer func() { @@ -245,21 +252,15 @@ func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) { return &testSuites, err } -func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSuiteDesc) (*[]controllers.TestSuiteResult, bool) { - // Load the result template, which we execute for each suite. - module, _ := revel.ModuleByName("testrunner") - TemplateLoader := revel.NewTemplateLoader([]string{filepath.Join(module.Path, "app", "views")}) - if err := TemplateLoader.Refresh(); err != nil { - errorf("Failed to compile templates: %s", err) - } - resultTemplate, err := TemplateLoader.Template("TestRunner/SuiteResult.html") - if err != nil { - errorf("Failed to load suite result template: %s", err) - } +// Run the testsuites using the container +func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, testSuites *[]tests.TestSuiteDesc) (*[]tests.TestSuiteResult, bool) { + + // We can determine the testsuite location by finding the test module and extracting the data from it + resultFilePath := filepath.Join(paths.ModulePathMap["testrunner"], "app", "views", "TestRunner/SuiteResult.html") var ( overallSuccess = true - failedResults []controllers.TestSuiteResult + failedResults []tests.TestSuiteResult ) for _, suite := range *testSuites { // Print the name of the suite we're running. @@ -271,21 +272,24 @@ func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSui // Run every test. startTime := time.Now() - suiteResult := controllers.TestSuiteResult{Name: suite.Name, Passed: true} + suiteResult := tests.TestSuiteResult{Name: suite.Name, Passed: true} for _, test := range suite.Tests { testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name resp, err := http.Get(testURL) if err != nil { - errorf("Failed to fetch test result at url %s: %s", testURL, err) + utils.Logger.Errorf("Failed to fetch test result at url %s: %s", testURL, err) } defer func() { _ = resp.Body.Close() }() - var testResult controllers.TestResult + var testResult tests.TestResult err = json.NewDecoder(resp.Body).Decode(&testResult) if err == nil && !testResult.Passed { suiteResult.Passed = false + fmt.Printf(" %s.%s : FAILED\n", suite.Name, test.Name) + } else { + fmt.Printf(" %s.%s : PASSED\n", suite.Name, test.Name) } suiteResult.Results = append(suiteResult.Results, testResult) } @@ -301,13 +305,7 @@ func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSui // Create the result HTML file. suiteResultFilename := filepath.Join(resultPath, fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr))) - suiteResultFile, err := os.Create(suiteResultFilename) - if err != nil { - errorf("Failed to create result file %s: %s", suiteResultFilename, err) - } - if err = resultTemplate.Render(suiteResultFile, suiteResult); err != nil { - errorf("Failed to render result template: %s", err) - } + utils.MustRenderTemplate(suiteResultFilename, resultFilePath, suiteResult) } return &failedResults, overallSuccess diff --git a/revel/util.go b/revel/util.go deleted file mode 100644 index 940b020..0000000 --- a/revel/util.go +++ /dev/null @@ -1,176 +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 main - -import ( - "archive/tar" - "compress/gzip" - "fmt" - "go/build" - "io" - "os" - "path/filepath" - "strings" - "text/template" - - "github.com/revel/revel" -) - -// LoggedError is wrapper to differentiate logged panics from unexpected ones. -type LoggedError struct{ error } - -func panicOnError(err error, msg string) { - if revErr, ok := err.(*revel.Error); (ok && revErr != nil) || (!ok && err != nil) { - fmt.Fprintf(os.Stderr, "Abort: %s: %s\n", msg, err) - panic(LoggedError{err}) - } -} - -func mustCopyFile(destFilename, srcFilename string) { - destFile, err := os.Create(destFilename) - panicOnError(err, "Failed to create file "+destFilename) - - srcFile, err := os.Open(srcFilename) - panicOnError(err, "Failed to open file "+srcFilename) - - _, err = io.Copy(destFile, srcFile) - panicOnError(err, - fmt.Sprintf("Failed to copy data from %s to %s", srcFile.Name(), destFile.Name())) - - err = destFile.Close() - panicOnError(err, "Failed to close file "+destFile.Name()) - - err = srcFile.Close() - panicOnError(err, "Failed to close file "+srcFile.Name()) -} - -func mustRenderTemplate(destPath, srcPath string, data map[string]interface{}) { - tmpl, err := template.ParseFiles(srcPath) - panicOnError(err, "Failed to parse template "+srcPath) - - f, err := os.Create(destPath) - panicOnError(err, "Failed to create "+destPath) - - err = tmpl.Execute(f, data) - panicOnError(err, "Failed to render template "+srcPath) - - err = f.Close() - panicOnError(err, "Failed to close "+f.Name()) -} - -func mustChmod(filename string, mode os.FileMode) { - err := os.Chmod(filename, mode) - panicOnError(err, fmt.Sprintf("Failed to chmod %d %q", mode, filename)) -} - -// copyDir copies a directory tree over to a new directory. Any files ending in -// ".template" are treated as a Go template and rendered using the given data. -// Additionally, the trailing ".template" is stripped from the file name. -// Also, dot files and dot directories are skipped. -func mustCopyDir(destDir, srcDir string, data map[string]interface{}) error { - return revel.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error { - // Get the relative path from the source base, and the corresponding path in - // the dest directory. - relSrcPath := strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)) - destPath := filepath.Join(destDir, relSrcPath) - - // Skip dot files and dot directories. - if strings.HasPrefix(relSrcPath, ".") { - if info.IsDir() { - return filepath.SkipDir - } - return nil - } - - // Create a subdirectory if necessary. - if info.IsDir() { - err := os.MkdirAll(filepath.Join(destDir, relSrcPath), 0777) - if !os.IsExist(err) { - panicOnError(err, "Failed to create directory") - } - return nil - } - - // If this file ends in ".template", render it as a template. - if strings.HasSuffix(relSrcPath, ".template") { - mustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data) - return nil - } - - // Else, just copy it over. - mustCopyFile(destPath, srcPath) - return nil - }) -} - -func mustTarGzDir(destFilename, srcDir string) string { - zipFile, err := os.Create(destFilename) - panicOnError(err, "Failed to create archive") - defer func() { - _ = zipFile.Close() - }() - - gzipWriter := gzip.NewWriter(zipFile) - defer func() { - _ = gzipWriter.Close() - }() - - tarWriter := tar.NewWriter(gzipWriter) - defer func() { - _ = tarWriter.Close() - }() - - _ = revel.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - - srcFile, err := os.Open(srcPath) - panicOnError(err, "Failed to read source file") - defer func() { - _ = srcFile.Close() - }() - - err = tarWriter.WriteHeader(&tar.Header{ - Name: strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)), - Size: info.Size(), - Mode: int64(info.Mode()), - ModTime: info.ModTime(), - }) - panicOnError(err, "Failed to write tar entry header") - - _, err = io.Copy(tarWriter, srcFile) - panicOnError(err, "Failed to copy") - - return nil - }) - - return zipFile.Name() -} - -func exists(filename string) bool { - _, err := os.Stat(filename) - return err == nil -} - -// empty returns true if the given directory is empty. -// the directory must exist. -func empty(dirname string) bool { - dir, err := os.Open(dirname) - if err != nil { - errorf("error opening directory: %s", err) - } - defer func() { - _ = dir.Close() - }() - results, _ := dir.Readdir(1) - return len(results) == 0 -} - -func importPathFromCurrentDir() string { - pwd, _ := os.Getwd() - importPath, _ := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), pwd) - return filepath.ToSlash(importPath) -} diff --git a/revel/version.go b/revel/version.go index c6ee21a..a0cb4b0 100644 --- a/revel/version.go +++ b/revel/version.go @@ -12,7 +12,14 @@ import ( "fmt" "runtime" - "github.com/revel/revel" + "github.com/revel/cmd/model" + "go/build" + "go/token" + "go/parser" + "go/ast" + "io/ioutil" + "path/filepath" + "github.com/revel/cmd/utils" ) var cmdVersion = &Command{ @@ -28,11 +35,46 @@ For example: } func init() { - cmdVersion.Run = versionApp + cmdVersion.RunWith = versionApp } -func versionApp(args []string) { - fmt.Printf("Version(s):") - fmt.Printf("\n Revel v%v (%v)", revel.Version, revel.BuildDate) +// Displays the version of go and Revel +func versionApp(c *model.CommandConfig) { + revelPkg, err := build.Import(model.RevelImportPath, c.Version.ImportPath, build.FindOnly) + if err != nil { + utils.Logger.Errorf("Failed to find Revel with error:", "error", err) + } + + + fset := token.NewFileSet() // positions are relative to fset + + + version, err := ioutil.ReadFile(filepath.Join(revelPkg.Dir,"version.go")) + if err != nil { + utils.Logger.Errorf("Failed to find Revel version:", "error", err) + } + + // Parse src but stop after processing the imports. + f, err := parser.ParseFile(fset, "", version, parser.ParseComments) + if err != nil { + utils.Logger.Errorf("Failed to parse Revel version error:", "error", err) + } + + // Print the imports from the file's AST. + for _, s := range f.Decls { + genDecl, ok := s.(*ast.GenDecl) + if !ok { + continue + } + if genDecl.Tok != token.CONST { + continue + } + for _, a := range genDecl.Specs { + spec := a.(*ast.ValueSpec) + r := spec.Values[0].(*ast.BasicLit) + fmt.Printf("Revel %s = %s\n",spec.Names[0].Name,r.Value) + } + } + fmt.Printf("\n %s %s/%s\n\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) } diff --git a/tests/testrunner.go b/tests/testrunner.go new file mode 100644 index 0000000..d3f24d8 --- /dev/null +++ b/tests/testrunner.go @@ -0,0 +1,158 @@ +// 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 tests + +import ( + "fmt" + "html/template" + "reflect" + "strings" + + "github.com/revel/cmd/utils" +) + + +// TestSuiteDesc is used for storing information about a single test suite. +// This structure is required by revel test cmd. +type TestSuiteDesc struct { + Name string + Tests []TestDesc + + // Elem is reflect.Type which can be used for accessing methods + // of the test suite. + Elem reflect.Type +} + +// TestDesc is used for describing a single test of some test suite. +// This structure is required by revel test cmd. +type TestDesc struct { + Name string +} + +// TestSuiteResult stores the results the whole test suite. +// This structure is required by revel test cmd. +type TestSuiteResult struct { + Name string + Passed bool + Results []TestResult +} + +// TestResult represents the results of running a single test of some test suite. +// This structure is required by revel test cmd. +type TestResult struct { + Name string + Passed bool + ErrorHTML template.HTML + ErrorSummary string +} + +var ( + testSuites []TestSuiteDesc // A list of all available tests. + + none = []reflect.Value{} // It is used as input for reflect call in a few places. + + // registeredTests simplifies the search of test suites by their name. + // "TestSuite.TestName" is used as a key. Value represents index in testSuites. + registeredTests map[string]int +) + +/* + Below are helper functions. +*/ + +// describeSuite expects testsuite interface as input parameter +// and returns its description in a form of TestSuiteDesc structure. +func describeSuite(testSuite interface{}) TestSuiteDesc { + t := reflect.TypeOf(testSuite) + + // Get a list of methods of the embedded test type. + // It will be used to make sure the same tests are not included in multiple test suites. + super := t.Elem().Field(0).Type + superMethods := map[string]bool{} + for i := 0; i < super.NumMethod(); i++ { + // Save the current method's name. + superMethods[super.Method(i).Name] = true + } + + // Get a list of methods on the test suite that take no parameters, return + // no results, and were not part of the embedded type's method set. + var tests []TestDesc + for i := 0; i < t.NumMethod(); i++ { + m := t.Method(i) + mt := m.Type + + // Make sure the test method meets the criterias: + // - method of testSuite without input parameters; + // - nothing is returned; + // - has "Test" prefix; + // - doesn't belong to the embedded structure. + methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t) + nothingReturned := (mt.NumOut() == 0) + hasTestPrefix := (strings.HasPrefix(m.Name, "Test")) + if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] { + // Register the test suite's index so we can quickly find it by test's name later. + registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites) + + // Add test to the list of tests. + tests = append(tests, TestDesc{m.Name}) + } + } + + return TestSuiteDesc{ + Name: t.Elem().Name(), + Tests: tests, + Elem: t.Elem(), + } +} + +// errorSummary gets an error and returns its summary in human readable format. +func errorSummary(err *utils.Error) (message string) { + expectedPrefix := "(expected)" + actualPrefix := "(actual)" + errDesc := err.Description + //strip the actual/expected stuff to provide more condensed display. + if strings.Index(errDesc, expectedPrefix) == 0 { + errDesc = errDesc[len(expectedPrefix):] + } + if strings.LastIndex(errDesc, actualPrefix) > 0 { + errDesc = errDesc[0 : len(errDesc)-len(actualPrefix)] + } + + errFile := err.Path + slashIdx := strings.LastIndex(errFile, "/") + if slashIdx > 0 { + errFile = errFile[slashIdx+1:] + } + + message = fmt.Sprintf("%s %s#%d", errDesc, errFile, err.Line) + + /* + // If line of error isn't known return the message as is. + if err.Line == 0 { + return + } + + // Otherwise, include info about the line number and the relevant + // source code lines. + message += fmt.Sprintf(" (around line %d): ", err.Line) + for _, line := range err.ContextSource() { + if line.IsError { + message += line.Source + } + } + */ + + return +} + + +//sortbySuiteName sorts the testsuites by name. +type sortBySuiteName []interface{} + +func (a sortBySuiteName) Len() int { return len(a) } +func (a sortBySuiteName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortBySuiteName) Less(i, j int) bool { + return reflect.TypeOf(a[i]).Elem().Name() < reflect.TypeOf(a[j]).Elem().Name() +} diff --git a/utils/error.go b/utils/error.go new file mode 100644 index 0000000..08b3180 --- /dev/null +++ b/utils/error.go @@ -0,0 +1,81 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" +) + +// The error is a wrapper for the +type Error struct { + SourceType string // The type of source that failed to build. + Title, Path, Description string // Description of the error, as presented to the user. + Line, Column int // Where the error was encountered. + SourceLines []string // The entire source file, split into lines. + Stack string // The raw stack trace string from debug.Stack(). + MetaError string // Error that occurred producing the error page. + Link string // A configurable link to wrap the error source in +} + +// Creates a link based on the configuration setting "errors.link" +func (e *Error) SetLink(errorLink string) { + errorLink = strings.Replace(errorLink, "{{Path}}", e.Path, -1) + errorLink = strings.Replace(errorLink, "{{Line}}", strconv.Itoa(e.Line), -1) + + e.Link = "" + e.Path + ":" + strconv.Itoa(e.Line) + "" +} + +// Error method constructs a plaintext version of the error, taking +// account that fields are optionally set. Returns e.g. Compilation Error +// (in views/header.html:51): expected right delim in end; got "}" +func (e *Error) Error() string { + if e == nil { + panic("opps") + } + loc := "" + if e.Path != "" { + line := "" + if e.Line != 0 { + line = fmt.Sprintf(":%d", e.Line) + } + loc = fmt.Sprintf("(in %s%s)", e.Path, line) + } + header := loc + if e.Title != "" { + if loc != "" { + header = fmt.Sprintf("%s %s: ", e.Title, loc) + } else { + header = fmt.Sprintf("%s: ", e.Title) + } + } + return fmt.Sprintf("%s%s", header, e.Description) +} +// ContextSource method returns a snippet of the source around +// where the error occurred. +func (e *Error) ContextSource() []SourceLine { + if e.SourceLines == nil { + return nil + } + start := (e.Line - 1) - 5 + if start < 0 { + start = 0 + } + end := (e.Line - 1) + 5 + if end > len(e.SourceLines) { + end = len(e.SourceLines) + } + + lines := make([]SourceLine, end-start) + for i, src := range e.SourceLines[start:end] { + fileLine := start + i + 1 + lines[i] = SourceLine{src, fileLine, fileLine == e.Line} + } + return lines +} + +// SourceLine structure to hold the per-source-line details. +type SourceLine struct { + Source string + Line int + IsError bool +} diff --git a/utils/file.go b/utils/file.go new file mode 100644 index 0000000..e9c6fc6 --- /dev/null +++ b/utils/file.go @@ -0,0 +1,285 @@ +package utils + + +// DirExists returns true if the given path exists and is a directory. +import ( + "os" + "archive/tar" + "strings" + "io" + "path/filepath" + "fmt" + "html/template" + "compress/gzip" + "go/build" + "io/ioutil" + "bytes" +) + +func DirExists(filename string) bool { + fileInfo, err := os.Stat(filename) + return err == nil && fileInfo.IsDir() +} + +// MustReadLines reads the lines of the given file. Panics in the case of error. +func MustReadLines(filename string) []string { + r, err := ReadLines(filename) + if err != nil { + panic(err) + } + return r +} + +// ReadLines reads the lines of the given file. Panics in the case of error. +func ReadLines(filename string) ([]string, error) { + dataBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return strings.Split(string(dataBytes), "\n"), nil +} + +func MustCopyFile(destFilename, srcFilename string) { + destFile, err := os.Create(destFilename) + PanicOnError(err, "Failed to create file "+destFilename) + + srcFile, err := os.Open(srcFilename) + PanicOnError(err, "Failed to open file "+srcFilename) + + _, err = io.Copy(destFile, srcFile) + PanicOnError(err, + fmt.Sprintf("Failed to copy data from %s to %s", srcFile.Name(), destFile.Name())) + + err = destFile.Close() + PanicOnError(err, "Failed to close file "+destFile.Name()) + + err = srcFile.Close() + PanicOnError(err, "Failed to close file "+srcFile.Name()) +} + + +// GenerateTemplate renders the given template to produce source code, which it writes +// to the given file. +func MustGenerateTemplate(filename, templateSource string, args map[string]interface{}) (err error) { + tmpl := template.Must(template.New("").Parse(templateSource)) + + var b bytes.Buffer + if err = tmpl.Execute(&b, args); err != nil { + Logger.Fatal("ExecuteTemplate: Execute failed", "error", err) + return + } + sourceCode := b.String() + filePath := filepath.Dir(filename) + if !DirExists(filePath) { + err = os.Mkdir(filePath, 0777) + if err != nil && !os.IsExist(err) { + Logger.Fatal("Failed to make directory","dir", filePath, "error", err) + } + } + + + // Create the file + file, err := os.Create(filename) + if err != nil { + Logger.Fatal("Failed to create file","error", err) + return + } + defer func() { + _ = file.Close() + }() + + if _, err = file.WriteString(sourceCode); err != nil { + Logger.Fatal("Failed to write to file: ", "error", err) + } + + return +} + +// Given the target path and source path and data. A template +func MustRenderTemplate(destPath, srcPath string, data interface{}) { + tmpl, err := template.ParseFiles(srcPath) + PanicOnError(err, "Failed to parse template "+srcPath) + + f, err := os.Create(destPath) + PanicOnError(err, "Failed to create "+destPath) + + err = tmpl.Execute(f, data) + PanicOnError(err, "Failed to render template "+srcPath) + + err = f.Close() + PanicOnError(err, "Failed to close "+f.Name()) +} + +// Given the target path and source path and data. A template +func MustRenderTemplateToStream(output io.Writer, srcPath []string, data interface{}) { + tmpl, err := template.ParseFiles(srcPath...) + PanicOnError(err, "Failed to parse template "+srcPath[0]) + + err = tmpl.Execute(output, data) + PanicOnError(err, "Failed to render template "+srcPath[0]) +} + +func MustChmod(filename string, mode os.FileMode) { + err := os.Chmod(filename, mode) + PanicOnError(err, fmt.Sprintf("Failed to chmod %d %q", mode, filename)) +} + +// Called if panic +func PanicOnError(err error, msg string) { + if revErr, ok := err.(*Error); (ok && revErr != nil) || (!ok && err != nil) { + Logger.Fatalf("Abort: %s: %s %s\n", msg, revErr, err) + //panic(NewLoggedError(err)) + } +} + +// copyDir copies a directory tree over to a new directory. Any files ending in +// ".template" are treated as a Go template and rendered using the given data. +// Additionally, the trailing ".template" is stripped from the file name. +// Also, dot files and dot directories are skipped. +func MustCopyDir(destDir, srcDir string, data map[string]interface{}) error { + return fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error { + // Get the relative path from the source base, and the corresponding path in + // the dest directory. + relSrcPath := strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)) + destPath := filepath.Join(destDir, relSrcPath) + + // Skip dot files and dot directories. + if strings.HasPrefix(relSrcPath, ".") { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + // Create a subdirectory if necessary. + if info.IsDir() { + err := os.MkdirAll(filepath.Join(destDir, relSrcPath), 0777) + if !os.IsExist(err) { + PanicOnError(err, "Failed to create directory") + } + return nil + } + + // If this file ends in ".template", render it as a template. + if strings.HasSuffix(relSrcPath, ".template") { + MustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data) + return nil + } + + // Else, just copy it over. + MustCopyFile(destPath, srcPath) + return nil + }) +} + +func Walk(root string, walkFn filepath.WalkFunc) error { + return fsWalk(root,root,walkFn) +} +func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error { + fsWalkFunc := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + var name string + name, err = filepath.Rel(fname, path) + if err != nil { + return err + } + + path = filepath.Join(linkName, name) + + if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { + var symlinkPath string + symlinkPath, err = filepath.EvalSymlinks(path) + if err != nil { + return err + } + + // https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392 + info, err = os.Lstat(symlinkPath) + + if err != nil { + return walkFn(path, info, err) + } + + if info.IsDir() { + return fsWalk(symlinkPath, path, walkFn) + } + } + + return walkFn(path, info, err) + } + err := filepath.Walk(fname, fsWalkFunc) + return err +} + +func MustTarGzDir(destFilename, srcDir string) string { + zipFile, err := os.Create(destFilename) + PanicOnError(err, "Failed to create archive") + defer func() { + _ = zipFile.Close() + }() + + gzipWriter := gzip.NewWriter(zipFile) + defer func() { + _ = gzipWriter.Close() + }() + + tarWriter := tar.NewWriter(gzipWriter) + defer func() { + _ = tarWriter.Close() + }() + + _ = fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + srcFile, err := os.Open(srcPath) + PanicOnError(err, "Failed to read source file") + defer func() { + _ = srcFile.Close() + }() + + err = tarWriter.WriteHeader(&tar.Header{ + Name: strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)), + Size: info.Size(), + Mode: int64(info.Mode()), + ModTime: info.ModTime(), + }) + PanicOnError(err, "Failed to write tar entry header") + + _, err = io.Copy(tarWriter, srcFile) + PanicOnError(err, "Failed to copy") + + return nil + }) + + return zipFile.Name() +} + +func Exists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +// empty returns true if the given directory is empty. +// the directory must exist. +func Empty(dirname string) bool { + dir, err := os.Open(dirname) + if err != nil { + Logger.Infof("error opening directory: %s", err) + } + defer func() { + _ = dir.Close() + }() + results, _ := dir.Readdir(1) + return len(results) == 0 +} + +func ImportPathFromCurrentDir() string { + pwd, _ := os.Getwd() + importPath, _ := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), pwd) + return filepath.ToSlash(importPath) +} diff --git a/utils/log.go b/utils/log.go new file mode 100644 index 0000000..f3c4033 --- /dev/null +++ b/utils/log.go @@ -0,0 +1,43 @@ +package utils + +import ( + "github.com/revel/cmd/logger" + "github.com/revel/config" + "fmt" + "os" + "strings" +) + +var Logger = logger.New() + +func InitLogger(basePath string, logLevel logger.LogLevel) { + newContext := config.NewContext() + if logLevel