From d58fd6434f9dc1884d91eb5eb94a2083f9eb3ad3 Mon Sep 17 00:00:00 2001 From: lealife Date: Sun, 15 Aug 2021 10:35:34 +0800 Subject: [PATCH] gen tmp tool without revel --- .gitignore | 3 +- app/cmd/README.md | 9 + app/cmd/build.go | 270 +++++++++++ app/cmd/gen_tmp.sh | 4 + app/cmd/harness/app.go | 219 +++++++++ app/cmd/harness/build.go | 566 +++++++++++++++++++++++ app/cmd/harness/harness.go | 411 ++++++++++++++++ app/cmd/parser2/source_info_processor.go | 427 +++++++++++++++++ app/cmd/parser2/source_processor.go | 310 +++++++++++++ app/cmd/revel.go | 151 ++++++ go.mod | 6 +- go.sum | 23 + 12 files changed, 2397 insertions(+), 2 deletions(-) create mode 100644 app/cmd/README.md create mode 100644 app/cmd/build.go create mode 100644 app/cmd/gen_tmp.sh create mode 100644 app/cmd/harness/app.go create mode 100644 app/cmd/harness/build.go create mode 100644 app/cmd/harness/harness.go create mode 100644 app/cmd/parser2/source_info_processor.go create mode 100644 app/cmd/parser2/source_processor.go create mode 100644 app/cmd/revel.go diff --git a/.gitignore b/.gitignore index 3c11945..ca02f1b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ files/ *.iml target/ package/leanote.tar.gz -package/leanote/ \ No newline at end of file +package/leanote/ +leanote.log \ No newline at end of file diff --git a/app/cmd/README.md b/app/cmd/README.md new file mode 100644 index 0000000..43483fa --- /dev/null +++ b/app/cmd/README.md @@ -0,0 +1,9 @@ +全部代码来自https://github.com/revel/cmd + +因为要改parse2, 所以改只要一点点代码 +harness/ + build.go 只要gensource, 其它的先return +main.go 改动很小 +build.go 改动很小 +parser2/ + source_processors.go 改了 fsWalk 过滤掉 public, files, build 等文件夹 \ No newline at end of file diff --git a/app/cmd/build.go b/app/cmd/build.go new file mode 100644 index 0000000..45a7510 --- /dev/null +++ b/app/cmd/build.go @@ -0,0 +1,270 @@ +// 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 ( + "os" + "path/filepath" + "strings" + + "fmt" + "github.com/leanote/leanote/app/cmd/harness" // 只改了这个 + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" +) + +var cmdBuild = &Command{ + UsageLine: "revel build [-r [run mode]] [import path] [target path] ", + 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. + +For example: + + revel build github.com/revel/examples/chat /tmp/chat + +`, +} + +func init() { + cmdBuild.RunWith = buildApp + cmdBuild.UpdateConfig = updateBuildConfig +} + +// The update config updates the configuration command so that it can run +func updateBuildConfig(c *model.CommandConfig, args []string) bool { + c.Index = model.BUILD + if c.Build.TargetPath == "" { + c.Build.TargetPath = "target" + } + if len(args) == 0 && c.Build.ImportPath != "" { + return true + } + // If arguments were passed in then there must be two + if len(args) < 2 { + fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long) + return false + } + + c.Build.ImportPath = args[0] + c.Build.TargetPath = args[1] + if len(args) > 2 { + c.Build.Mode = args[2] + } + return true +} + +// The main entry point to build application from command line +func buildApp(c *model.CommandConfig) (err error) { + + appImportPath, destPath, mode := c.ImportPath, c.Build.TargetPath, DefaultRunMode + if len(c.Build.Mode) > 0 { + mode = c.Build.Mode + } + + // Convert target to absolute path + c.Build.TargetPath, _ = filepath.Abs(destPath) + c.Build.Mode = mode + c.Build.ImportPath = appImportPath + + revel_paths, err := model.NewRevelPaths(mode, appImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver)) + if err != nil { + return + } + + if err = buildSafetyCheck(destPath); err != nil { + return + } + + // Ensure the application can be built, this generates the main file + app, err := harness.Build(c, revel_paths) + if err != nil { + return err + } + // Copy files + // Included are: + // - run scripts + // - binary + // - revel + // - app + + return // 改了这里 + + packageFolders, err := buildCopyFiles(c, app, revel_paths) + if err != nil { + return + } + err = buildCopyModules(c, revel_paths, packageFolders, app) + if err != nil { + return + } + err = buildWriteScripts(c, app) + if err != nil { + return + } + return +} + +// Copy the files to the target +func buildCopyFiles(c *model.CommandConfig, app *harness.App, revel_paths *model.RevelContainer) (packageFolders []string, err error) { + appImportPath, destPath := c.ImportPath, c.Build.TargetPath + + // 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(model.RevelImportPath)) + if err = utils.CopyFile(destBinaryPath, filepath.Join(revel_paths.BasePath, app.BinaryPath)); err != nil { + return + } + utils.MustChmod(destBinaryPath, 0755) + + // Copy the templates from the revel + if err = utils.CopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil); err != nil { + return + } + if err = utils.CopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil); err != nil { + return + } + + // Get the folders to be packaged + packageFolders = strings.Split(revel_paths.Config.StringDefault("package.folders", "conf,public,app/views"), ",") + for i, p := range packageFolders { + // Clean spaces, reformat slash to filesystem + packageFolders[i] = filepath.FromSlash(strings.TrimSpace(p)) + } + + if c.Build.CopySource { + err = utils.CopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil) + if err != nil { + return + } + } else { + for _, folder := range packageFolders { + err = utils.CopyDir( + filepath.Join(srcPath, filepath.FromSlash(appImportPath), folder), + filepath.Join(revel_paths.BasePath, folder), + nil) + if err != nil { + return + } + } + } + + return +} + +// Based on the section copy over the build modules +func buildCopyModules(c *model.CommandConfig, revel_paths *model.RevelContainer, packageFolders []string, app *harness.App) (err error) { + destPath := filepath.Join(c.Build.TargetPath, "src") + // Find all the modules used and copy them over. + config := revel_paths.Config.Raw() + + // We should only copy over the section of options what the build is targeted for + // We will default to prod + moduleImportList := []string{} + for _, section := range config.Sections() { + // If the runmode is defined we will only import modules defined for that run mode + if c.Build.Mode != "" && c.Build.Mode != section { + continue + } + options, _ := config.SectionOptions(section) + for _, key := range options { + if !strings.HasPrefix(key, "module.") { + continue + } + moduleImportPath, _ := config.String(section, key) + if moduleImportPath == "" { + continue + } + moduleImportList = append(moduleImportList, moduleImportPath) + + } + } + + // Copy the the paths for each of the modules + for _, importPath := range moduleImportList { + fsPath := app.PackagePathMap[importPath] + utils.Logger.Info("Copy files ", "to", filepath.Join(destPath, importPath), "from", fsPath) + if c.Build.CopySource { + err = utils.CopyDir(filepath.Join(destPath, importPath), fsPath, nil) + if err != nil { + return + } + } else { + for _, folder := range packageFolders { + err = utils.CopyDir( + filepath.Join(destPath, importPath, folder), + filepath.Join(fsPath, folder), + nil) + if err != nil { + return + } + } + } + } + + return +} + +// Write the run scripts for the build +func buildWriteScripts(c *model.CommandConfig, app *harness.App) (err error) { + tmplData := map[string]interface{}{ + "BinName": filepath.Base(app.BinaryPath), + "ImportPath": c.Build.ImportPath, + "Mode": c.Build.Mode, + } + + err = utils.GenerateTemplate( + filepath.Join(c.Build.TargetPath, "run.sh"), + PACKAGE_RUN_SH, + tmplData, + ) + if err != nil { + return + } + utils.MustChmod(filepath.Join(c.Build.TargetPath, "run.sh"), 0755) + err = utils.GenerateTemplate( + filepath.Join(c.Build.TargetPath, "run.bat"), + PACKAGE_RUN_BAT, + tmplData, + ) + if err != nil { + return + } + + fmt.Println("Your application has been built in:", c.Build.TargetPath) + + return +} + +// Checks to see if the target folder exists and can be created +func buildSafetyCheck(destPath string) error { + + // First, verify that it is either already empty or looks like a previous + // build (to avoid clobbering anything) + if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) { + return utils.NewBuildError("Abort: %s exists and does not look like a build directory.", "path", destPath) + } + + if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) { + return utils.NewBuildIfError(err, "Remove all error", "path", destPath) + } + + if err := os.MkdirAll(destPath, 0777); err != nil { + return utils.NewBuildIfError(err, "MkDir all error", "path", destPath) + } + return nil +} + +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/app/cmd/gen_tmp.sh b/app/cmd/gen_tmp.sh new file mode 100644 index 0000000..759183a --- /dev/null +++ b/app/cmd/gen_tmp.sh @@ -0,0 +1,4 @@ +SCRIPTPATH=$(cd "$(dirname "$0")"; pwd) +cd $SCRIPTPATH +go run . build -v ../../ ./tmptmp +rm -rf ./tmptmp \ No newline at end of file diff --git a/app/cmd/harness/app.go b/app/cmd/harness/app.go new file mode 100644 index 0000000..24ab4ee --- /dev/null +++ b/app/cmd/harness/app.go @@ -0,0 +1,219 @@ +// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. +// Revel Framework source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package harness + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "time" + "sync" + + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "runtime" +) + +// App contains the configuration for running a Revel app. (Not for the app itself) +// Its only purpose is constructing the command to execute. +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. + PackagePathMap map[string]string // Package to directory path map + Paths *model.RevelContainer +} + +// NewApp returns app instance with binary path in it +func NewApp(binPath string, paths *model.RevelContainer, packagePathMap map[string]string) *App { + return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort, PackagePathMap:packagePathMap} +} + +// Cmd returns a command to run the app server using the current configuration. +func (a *App) Cmd(runMode string) AppCmd { + a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths) + return a.cmd +} + +// Kill the last app command returned. +func (a *App) Kill() { + a.cmd.Kill() +} + +// AppCmd manages the running of a Revel app server. +// It requires revel.Init to have been called previously. +type AppCmd struct { + *exec.Cmd +} + +// NewAppCmd returns the AppCmd with parameters initialized for running app +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", 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(c *model.CommandConfig) error { + listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c, &bytes.Buffer{}} + cmd.Stdout = listeningWriter + utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath) + utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env) + if err := cmd.Cmd.Start(); err != nil { + utils.Logger.Fatal("Error running:", "error", err) + } + + select { + case exitState := <-cmd.waitChan(): + fmt.Println("Startup failure view previous messages, \n Proxy is listening :", c.Run.Port) + err := utils.NewError("", "Revel Run Error", "starting your application there was an exception. See terminal output, " + exitState, "") + // TODO pretiffy command line output + // err.MetaError = listeningWriter.getLastOutput() + return err + + case <-time.After(60 * time.Second): + println("Revel proxy is listening, point your browser to :", c.Run.Port) + utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid) + cmd.Kill() + return errors.New("revel/harness: app timed out") + + case <-listeningWriter.notifyReady: + println("Revel proxy is listening, point your browser to :", c.Run.Port) + return nil + } + +} + +// Run the app server inline. Never returns. +func (cmd AppCmd) Run(c *model.CommandConfig) { + utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath) + utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args) + if err := cmd.Cmd.Run(); err != nil { + 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()) { + // Windows appears to send the kill to all threads, shutting down the + // server before this can, this check will ensure the process is still running + if _, err := os.FindProcess(int(cmd.Process.Pid)); err != nil { + // Server has already exited + utils.Logger.Info("Server not running revel server pid", "pid", cmd.Process.Pid) + return + } + + // Wait for the shutdown channel + waitMutex := &sync.WaitGroup{} + waitMutex.Add(1) + ch := make(chan bool, 1) + go func() { + waitMutex.Done() + s, err := cmd.Process.Wait() + defer func() { + ch <- true + }() + if err != nil { + utils.Logger.Info("Wait failed for process ", "error", err) + } + if s != nil { + utils.Logger.Info("Revel App exited", "state", s.String()) + } + }() + // Wait for the channel to begin waiting + waitMutex.Wait() + + // Send an interrupt signal to allow for a graceful shutdown + utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid) + var err error + if runtime.GOOS != "windows" { + // os.Interrupt is not available on windows + err = cmd.Process.Signal(os.Interrupt) + } + + if err != nil { + utils.Logger.Info( + "Revel app already exited.", + "processid", cmd.Process.Pid, "error", err, + "killerror", cmd.Process.Kill()) + return + } + + + // Use a timer to ensure that the process exits + utils.Logger.Info("Waiting to exit") + select { + case <-ch: + return + case <-time.After(60 * time.Second): + // Kill the process + utils.Logger.Error( + "Revel app failed to exit in 60 seconds - killing.", + "processid", cmd.Process.Pid, + "killerror", cmd.Process.Kill()) + } + + utils.Logger.Info("Done Waiting to exit") + } +} + +// Return a channel that is notified when Wait() returns. +func (cmd AppCmd) waitChan() <-chan string { + ch := make(chan string, 1) + go func() { + _ = cmd.Wait() + state := cmd.ProcessState + exitStatus := " unknown " + if state != nil { + exitStatus = state.String() + } + + ch <- exitStatus + }() + return ch +} + +// 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 + buffer *bytes.Buffer +} + +// Writes to this output stream +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 + } + } + if w.notifyReady != nil { + w.buffer.Write(p) + } + return w.dest.Write(p) +} + +// Returns the cleaned output from the response +// TODO clean the response more +func (w *startupListeningWriter) getLastOutput() string { + return w.buffer.String() +} + diff --git a/app/cmd/harness/build.go b/app/cmd/harness/build.go new file mode 100644 index 0000000..3c9206a --- /dev/null +++ b/app/cmd/harness/build.go @@ -0,0 +1,566 @@ +// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. +// Revel Framework source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package harness + +import ( + "fmt" + "go/build" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "runtime" + "sort" + "strconv" + "strings" + "time" + + "github.com/leanote/leanote/app/cmd/parser2" + "github.com/revel/cmd/model" + "github.com/revel/cmd/parser" + _ "github.com/revel/cmd/parser" + "github.com/revel/cmd/utils" +) + +var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"") + +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] +} +func (c ByString) Less(i, j int) bool { + return c[i].String() < c[j].String() +} + +// Build the app: +// 1. Generate the the main.go file. +// 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(c *model.CommandConfig, paths *model.RevelContainer) (_ *App, err error) { + // First, clear the generated files (to avoid them messing with ProcessSource). + cleanSource(paths, "tmp", "routes") + + var sourceInfo *model.SourceInfo + + if c.HistoricBuildMode { + sourceInfo, err = parser.ProcessSource(paths) + } else { + sourceInfo, err = parser2.ProcessSource(paths) + } + if err != nil { + return + } + + // Add the db.import to the import paths. + if dbImportPath, found := paths.Config.String("db.import"); found { + sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, strings.Split(dbImportPath, ",")...) + } + + // Sort controllers so that file generation is reproducible + controllers := sourceInfo.ControllerSpecs() + sort.Stable(ByString(controllers)) + + // Generate two source files. + templateArgs := map[string]interface{}{ + "ImportPath": paths.ImportPath, + "Controllers": controllers, + "ValidationKeys": sourceInfo.ValidationKeys, + "ImportPaths": calcImportAliases(sourceInfo), + "TestSuites": sourceInfo.TestSuites(), + } + + // Generate code for the main, run and routes file. + // The run file allows external programs to launch and run the application + // without being the main thread + cleanSource(paths, "tmp", "routes") + + if err = genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs); err != nil { + return + } + if err = genSource(paths, filepath.Join("tmp", "run"), "run.go", RevelRunTemplate, templateArgs); err != nil { + return + } + if err = genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs); err != nil { + return + } + + utils.Logger.Warn("gen tmp/main.go, tmp/run/run.go, routes/routes.go success!!") + + return // 改了这里 + + // Read build config. + 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 { + utils.Logger.Fatal("Go executable not found in PATH.") + } + + // Binary path is a combination of target/app directory, app's import path and its name. + binName := filepath.Join("target", "app", paths.ImportPath, filepath.Base(paths.BasePath)) + + // Change binary path for Windows build + goos := runtime.GOOS + if goosEnv := os.Getenv("GOOS"); goosEnv != "" { + goos = goosEnv + } + if goos == "windows" { + binName += ".exe" + } + + gotten := make(map[string]struct{}) + contains := func(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false + } + + if len(c.GoModFlags) > 0 { + for _, gomod := range c.GoModFlags { + goModCmd := exec.Command(goPath, append([]string{"mod"}, strings.Split(gomod, " ")...)...) + utils.CmdInit(goModCmd, !c.Vendored, c.AppPath) + output, err := goModCmd.CombinedOutput() + utils.Logger.Info("Gomod applied ", "output", string(output)) + + // If the build succeeded, we're done. + if err != nil { + utils.Logger.Error("Gomod Failed continuing ", "error", err, "output", string(output)) + } + } + } + + for { + appVersion := getAppVersion(paths) + if appVersion == "" { + appVersion = "noVersionProvided" + } + + buildTime := time.Now().UTC().Format(time.RFC3339) + versionLinkerFlags := fmt.Sprintf("-X '%s/app.AppVersion=%s' -X '%s/app.BuildTime=%s'", + 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", + "-ldflags", versionLinkerFlags, + "-tags", buildTags, + "-o", binName} + } else { + if !contains(c.BuildFlags, "build") { + flags = []string{"build"} + } + if !contains(flags, "-ldflags") { + ldflags := "-ldflags= " + versionLinkerFlags + // Add user defined build flags + for i := range c.BuildFlags { + ldflags += " -X '" + c.BuildFlags[i] + "'" + } + flags = append(flags, ldflags) + } + if !contains(flags, "-tags") && buildTags != "" { + flags = append(flags, "-tags", buildTags) + } + if !contains(flags, "-o") { + flags = append(flags, "-o", binName) + } + } + + // Note: It's not applicable for filepath.* usage + flags = append(flags, path.Join(paths.ImportPath, "app", "tmp")) + + buildCmd := exec.Command(goPath, flags...) + if !c.Vendored { + // This is Go main path + gopath := c.GoPath + for _, o := range paths.ModulePathMap { + gopath += string(filepath.ListSeparator) + o.Path + } + + buildCmd.Env = append(os.Environ(), + "GOPATH=" + gopath, + ) + } + utils.CmdInit(buildCmd, !c.Vendored, c.AppPath) + + utils.Logger.Info("Exec:", "args", buildCmd.Args, "working dir", buildCmd.Dir) + output, err := buildCmd.CombinedOutput() + + // If the build succeeded, we're done. + if err == nil { + utils.Logger.Info("Build successful continuing") + return NewApp(binName, paths, sourceInfo.PackageMap), nil + } + + // Since there was an error, capture the output in case we need to report it + stOutput := string(output) + utils.Logger.Infof("Got error on build of app %s", stOutput) + + // See if it was an import error that we can go get. + matches := importErrorPattern.FindAllStringSubmatch(stOutput, -1) + utils.Logger.Info("Build failed checking for missing imports", "message", stOutput, "missing_imports", len(matches)) + if matches == nil { + 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 { + utils.Logger.Error("Failed to import ", "package", pkgName) + return nil, newCompileError(paths, output) + } + gotten[pkgName] = struct{}{} + if err := c.PackageResolver(pkgName); err != nil { + utils.Logger.Error("Unable to resolve package", "package", pkgName, "error", err) + return nil, newCompileError(paths, []byte(err.Error())) + } + } + + // Success getting the import, attempt to build again. + } + + // TODO remove this unreachable code and document it + utils.Logger.Fatal("Not reachable") + return nil, nil +} + +// Try to define a version string for the compiled app +// The following is tried (first match returns): +// - Read a version explicitly specified in the APP_VERSION environment +// 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(paths *model.RevelContainer) string { + if version := os.Getenv("APP_VERSION"); version != "" { + return version + } + + // Check for the git binary + if gitPath, err := exec.LookPath("git"); err == nil { + // Check for the .git directory + 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=" + paths.BasePath, "describe", "--always", "--dirty") + utils.Logger.Info("Exec:", "args", gitCmd.Args) + output, err := gitCmd.Output() + + if err != nil { + utils.Logger.Error("Cannot determine git repository version:", "error", err) + return "" + } + + return "git-" + strings.TrimSpace(string(output)) + } + + return "" +} + +func cleanSource(paths *model.RevelContainer, dirs ...string) { + for _, dir := range dirs { + cleanDir(paths, 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) { + utils.Logger.Error("Failed to clean dir:", "error", err) + } + } else { + defer func() { + _ = f.Close() + }() + + infos, err := f.Readdir(0) + if err != nil { + if !os.IsNotExist(err) { + utils.Logger.Fatal("Failed to clean dir:", "error", err) + } + } else { + for _, info := range infos { + pathName := filepath.Join(tmpPath, info.Name()) + if info.IsDir() { + err := os.RemoveAll(pathName) + if err != nil { + utils.Logger.Fatal("Failed to remove dir:", "error", err) + } + } else { + err := os.Remove(pathName) + if err != nil { + utils.Logger.Fatal("Failed to remove file:", "error", err) + } + } + } + } + } +} + +// genSource renders the given template to produce source code, which it writes +// to the given directory and file. +func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) error { + + return utils.GenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args) +} + +// 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 *model.SourceInfo) map[string]string { + aliases := make(map[string]string) + typeArrays := [][]*model.TypeInfo{src.ControllerSpecs(), src.TestSuites()} + for _, specs := range typeArrays { + for _, spec := range specs { + addAlias(aliases, spec.ImportPath, spec.PackageName) + + for _, methSpec := range spec.MethodSpecs { + for _, methArg := range methSpec.Args { + if methArg.ImportPath == "" { + continue + } + + addAlias(aliases, methArg.ImportPath, methArg.TypeExpr.PkgName) + } + } + } + } + + // Add the "InitImportPaths", with alias "_" + for _, importPath := range src.InitImportPaths { + if _, ok := aliases[importPath]; !ok { + aliases[importPath] = "_" + } + } + + 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 { + return + } + alias = makePackageAlias(aliases, pkgName) + aliases[importPath] = alias +} + +// Generates a package alias +func makePackageAlias(aliases map[string]string, pkgName string) string { + i := 0 + alias := pkgName + for containsValue(aliases, alias) || alias == "revel" { + alias = fmt.Sprintf("%s%d", pkgName, i) + i++ + } + 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 { + return true + } + } + return false +} + +// Parse the output of the "go build" command. +// Return a detailed Error. +func newCompileError(paths *model.RevelContainer, output []byte) *utils.SourceError { + errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`). + FindSubmatch(output) + if errorMatch == nil { + errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch(output) + + if errorMatch == nil { + utils.Logger.Error("Failed to parse build errors", "error", string(output)) + return &utils.SourceError{ + SourceType: "Go code", + Title: "Go Compilation Error", + Description: "See console for build error.", + } + } + + errorMatch = append(errorMatch, errorMatch[3]) + + 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, "src", paths.ImportPath, relFilename) + println(newPath) + 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 = findInPaths(relFilename) + line, _ = strconv.Atoi(string(errorMatch[2])) + description = string(errorMatch[4]) + compileError = &utils.SourceError{ + SourceType: "Go code", + Title: "Go Compilation Error", + Path: relFilename, + Description: description, + Line: line, + } + ) + + errorLink := paths.Config.StringDefault("error.link", "") + + if errorLink != "" { + compileError.SetLink(errorLink) + } + + fileStr, err := utils.ReadLines(absFilename) + if err != nil { + compileError.MetaError = absFilename + ": " + err.Error() + utils.Logger.Info("Unable to readlines " + compileError.MetaError, "error", err) + return compileError + } + + compileError.SourceLines = fileStr + return compileError +} + +// RevelMainTemplate template for app/tmp/run/run.go +const RevelRunTemplate = `// GENERATED CODE - DO NOT EDIT +// This file is the run 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 run + +import ( + "reflect" + "github.com/revel/revel"{{range $k, $v := $.ImportPaths}} + {{$v}} "{{$k}}"{{end}} + "github.com/revel/revel/testing" +) + +var ( + // So compiler won't complain if the generated code doesn't reference reflect package... + _ = reflect.Invalid +) + +// Register and run the application +func Run(port int) { + Register() + revel.Run(port) +} + +// Register all the controllers +func Register() { + revel.AppLog.Info("Running revel server") + {{range $i, $c := .Controllers}} + revel.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil), + []*revel.MethodType{ + {{range .MethodSpecs}}&revel.MethodType{ + Name: "{{.Name}}", + Args: []*revel.MethodArg{ {{range .Args}} + &revel.MethodArg{Name: "{{.Name}}", Type: reflect.TypeOf((*{{index $.ImportPaths .ImportPath | .TypeExpr.TypeName}})(nil)) },{{end}} + }, + RenderArgNames: map[int][]string{ {{range .RenderCalls}} + {{.Line}}: []string{ {{range .Names}} + "{{.}}",{{end}} + },{{end}} + }, + }, + {{end}} + }) + {{end}} + revel.DefaultValidationKeys = map[string]map[int]string{ {{range $path, $lines := .ValidationKeys}} + "{{$path}}": { {{range $line, $key := $lines}} + {{$line}}: "{{$key}}",{{end}} + },{{end}} + } + testing.TestSuites = []interface{}{ {{range .TestSuites}} + (*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}} + } +} +` +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 ( + "flag" + "{{.ImportPath}}/app/tmp/run" + "github.com/revel/revel" +) + +var ( + runMode *string = flag.String("runMode", "", "Run mode.") + port *int = flag.Int("port", 0, "By default, read from app.conf") + importPath *string = flag.String("importPath", "", "Go Import Path for the app.") + srcPath *string = flag.String("srcPath", "", "Path to the source root.") + +) + +func main() { + flag.Parse() + revel.Init(*runMode, *importPath, *srcPath) + run.Run(*port) +} +` + +// 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" + +{{range $i, $c := .Controllers}} +type t{{.StructName}} struct {} +var {{.StructName}} t{{.StructName}} + +{{range .MethodSpecs}} +func (_ t{{$c.StructName}}) {{.Name}}({{range .Args}} + {{.Name}} {{if .ImportPath}}interface{}{{else}}{{.TypeExpr.TypeName ""}}{{end}},{{end}} + ) string { + args := make(map[string]string) + {{range .Args}} + revel.Unbind(args, "{{.Name}}", {{.Name}}){{end}} + return revel.MainRouter.Reverse("{{$c.StructName}}.{{.Name}}", args).URL +} +{{end}} +{{end}} +` diff --git a/app/cmd/harness/harness.go b/app/cmd/harness/harness.go new file mode 100644 index 0000000..87ad05b --- /dev/null +++ b/app/cmd/harness/harness.go @@ -0,0 +1,411 @@ +// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved. +// Revel Framework source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +// Package harness for a Revel Framework. +// +// It has a following responsibilities: +// 1. Parse the user program, generating a main.go file that registers +// controller classes and starts the user's server. +// 2. Build and run the user program. Show compile errors. +// 3. Monitor the user source and re-build / restart the program when necessary. +// +// Source files are generated in the app/tmp directory. +package harness + +import ( + "crypto/tls" + "fmt" + "time" + "go/build" + "io" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/signal" + "path/filepath" + "strings" + "sync/atomic" + + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "github.com/revel/cmd/watcher" + "html/template" + "io/ioutil" + "sync" + "encoding/json" +) + +var ( + doNotWatch = []string{"tmp", "views", "routes"} + + lastRequestHadError int32 +) + +// Harness reverse proxies requests to the application server. +// It builds / runs / rebuilds / restarts the server when code is changed. +type Harness struct { + 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 + isError bool // True if harness is in error state + ranOnce bool // True app compiled once +} + +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 + if err == nil { + utils.Logger.Panic("Caller passed in a nil error") + } + 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) + } + + 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.Fprintf(iw, "Target template not found not found %s
\n", target[0]) + fmt.Fprintf(iw, "An error ocurred %s", err.Error()) + return + } + var revelError *utils.SourceError + switch e := err.(type) { + case *utils.SourceError: + revelError = e + case error: + revelError = &utils.SourceError{ + 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) + } +} + +// ServeHTTP handles all requests. +// It checks for changes to app, rebuilds if necessary, and forwards the request. +func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Don't rebuild the app for favicon requests. + if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" { + return + } + + // Flush any change events and rebuild app if necessary. + // Render an error page if the rebuild / restart failed. + err := h.watcher.Notify() + 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) + h.renderError(w, r, err) + return + } + + // In a thread safe manner update the flag so that a request for + // /favicon.ico is allowed + atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0) + + // Reverse proxy the request. + // (Need special code for websockets, courtesy of bradfitz) + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + h.proxyWebsocket(w, r, h.serverHost) + } else { + h.proxy.ServeHTTP(w, r) + } +} + +// NewHarness method returns a reverse proxy that forwards requests +// to the given port. +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) + //} + + addr := paths.HTTPAddr + port := paths.Config.IntDefault("harness.port", 0) + scheme := "http" + + 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" + } + + if port == 0 { + port = getFreePort() + } + + serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port)) + + serverHarness := &Harness{ + port: port, + serverHost: serverURL.String()[len(scheme+"://"):], + proxy: httputil.NewSingleHostReverseProxy(serverURL), + mutex: &sync.Mutex{}, + paths: paths, + useProxy: !noProxy, + config: c, + runMode: runMode, + } + + if paths.HTTPSsl { + serverHarness.proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + return serverHarness +} + +// Refresh method rebuilds the Revel application and run it on the given port. +// called by the watcher +func (h *Harness) Refresh() (err *utils.SourceError) { + t := time.Now(); + fmt.Println("Changed detected, recompiling") + err = h.refresh() + if err!=nil && !h.ranOnce && h.useProxy { + addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort) + + fmt.Printf("\nError compiling code, to view error details see proxy running on http://%s\n\n",addr) + } + + h.ranOnce = true + fmt.Printf("\nTime to recompile %s\n",time.Now().Sub(t).String()) + return +} + +func (h *Harness) refresh() (err *utils.SourceError) { + // 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() + + if h.app != nil { + h.app.Kill() + } + + utils.Logger.Info("Rebuild Called") + var newErr error + h.app, newErr = Build(h.config, h.paths) + if newErr != nil { + utils.Logger.Error("Build detected an error", "error", newErr) + if castErr, ok := newErr.(*utils.SourceError); ok { + return castErr + } + err = &utils.SourceError{ + Title: "App failed to start up", + Description: err.Error(), + } + return + } + + if h.useProxy { + h.app.Port = h.port + runMode := h.runMode + if !h.config.HistoricMode { + // Recalulate run mode based on the config + var paths []byte + if len(h.app.PackagePathMap)>0 { + paths, _ = json.Marshal(h.app.PackagePathMap) + } + runMode = fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, h.app.Paths.RunMode, h.config.Verbose, string(paths)) + + } + if err2 := h.app.Cmd(runMode).Start(h.config); err2 != nil { + utils.Logger.Error("Could not start application", "error", err2) + if err,k :=err2.(*utils.SourceError);k { + return err + } + return &utils.SourceError{ + Title: "App failed to start up", + Description: err2.Error(), + } + } + } else { + h.app = nil + } + + return +} + +// WatchDir method returns false to file matches with doNotWatch +// otheriwse true +func (h *Harness) WatchDir(info os.FileInfo) bool { + return !utils.ContainsString(doNotWatch, info.Name()) +} + +// WatchFile method returns true given filename HasSuffix of ".go" +// otheriwse false - implements revel.DiscerningListener +func (h *Harness) WatchFile(filename string) bool { + return strings.HasSuffix(filename, ".go") +} + +// Run the harness, which listens for requests and proxies them to the app +// server, which it runs and rebuilds as necessary. +func (h *Harness) Run() { + var paths []string + if h.paths.Config.BoolDefault("watch.gopath", false) { + gopaths := filepath.SplitList(build.Default.GOPATH) + paths = append(paths, gopaths...) + } + paths = append(paths, h.paths.CodePaths...) + h.watcher = watcher.NewWatcher(h.paths, false) + h.watcher.Listen(h, paths...) + go h.Refresh() + // h.watcher.Notify() + + if h.useProxy { + go func() { + // Check the port to start on a random port + if h.paths.HTTPPort == 0 { + h.paths.HTTPPort = getFreePort() + } + addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort) + utils.Logger.Infof("Proxy server is listening on %s", addr) + + + 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) + } + }() + + } + + // Make a new channel to listen for the interrupt event + ch := make(chan os.Signal) + signal.Notify(ch, os.Interrupt, os.Kill) + <-ch + // Kill the app and exit + if h.app != nil { + h.app.Kill() + } + os.Exit(1) +} + +// Find an unused port +func getFreePort() (port int) { + conn, err := net.Listen("tcp", ":0") + if err != nil { + utils.Logger.Fatal("Unable to fetch a freee port address", "error", err) + } + + port = conn.Addr().(*net.TCPAddr).Port + err = conn.Close() + if err != nil { + 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 (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) { + var ( + d net.Conn + err error + ) + 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. + d, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) + } else { + d, err = net.Dial("tcp", host) + } + if err != nil { + http.Error(w, "Error contacting backend server.", 500) + utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err) + return + } + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Not a hijacker?", 500) + return + } + nc, _, err := hj.Hijack() + if err != nil { + utils.Logger.Error("Hijack error", "error", err) + return + } + defer func() { + if err = nc.Close(); err != nil { + utils.Logger.Error("Connection close error", "error", err) + } + if err = d.Close(); err != nil { + utils.Logger.Error("Dial close error", "error", err) + } + }() + + err = r.Write(d) + if err != nil { + utils.Logger.Error("Error copying request to target", "error", err) + return + } + + errc := make(chan error, 2) + cp := func(dst io.Writer, src io.Reader) { + _, err := io.Copy(dst, src) + errc <- err + } + go cp(d, nc) + go cp(nc, d) + <-errc +} diff --git a/app/cmd/parser2/source_info_processor.go b/app/cmd/parser2/source_info_processor.go new file mode 100644 index 0000000..a18ad0f --- /dev/null +++ b/app/cmd/parser2/source_info_processor.go @@ -0,0 +1,427 @@ +package parser2 + +import ( + "github.com/revel/cmd/utils" + "golang.org/x/tools/go/packages" + "github.com/revel/cmd/model" + "go/ast" + "go/token" + "strings" + "path/filepath" + "github.com/revel/cmd/logger" +) + +type ( + SourceInfoProcessor struct { + sourceProcessor *SourceProcessor + } +) + +func NewSourceInfoProcessor(sourceProcessor *SourceProcessor) *SourceInfoProcessor { + return &SourceInfoProcessor{sourceProcessor:sourceProcessor} +} + +func (s *SourceInfoProcessor) processPackage(p *packages.Package) (sourceInfo *model.SourceInfo) { + sourceInfo = &model.SourceInfo{ + ValidationKeys: map[string]map[int]string{}, + } + var ( + isController = strings.HasSuffix(p.PkgPath, "/controllers") || + strings.Contains(p.PkgPath, "/controllers/") + isTest = strings.HasSuffix(p.PkgPath, "/tests") || + strings.Contains(p.PkgPath, "/tests/") + methodMap = map[string][]*model.MethodSpec{} + ) + localImportMap := map[string]string{} + log := s.sourceProcessor.log.New("package", p.PkgPath) + log.Info("Processing package") + for _, tree := range p.Syntax { + for _, decl := range tree.Decls { + + s.sourceProcessor.packageMap[p.PkgPath] = filepath.Dir(p.Fset.Position(decl.Pos()).Filename) + if !s.addImport(decl, p, localImportMap, log) { + continue + } + spec, found := s.getStructTypeDecl(decl, p.Fset) + //log.Info("Checking file","filename", p.Fset.Position(decl.Pos()).Filename,"found",found) + if found { + if isController || isTest { + controllerSpec := s.getControllerSpec(spec, p, localImportMap) + sourceInfo.StructSpecs = append(sourceInfo.StructSpecs, controllerSpec) + } + } else { + // Not a type definition, this could be a method for a controller try to extract that + // Func declaration? + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + // This could be a controller action endpoint, check and add if needed + if isController && + funcDecl.Recv != nil && // Must have a receiver + funcDecl.Name.IsExported() && // be public + funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) == 1 { + // return one result + if m, receiver := s.getControllerFunc(funcDecl, p, localImportMap); m != nil { + methodMap[receiver] = append(methodMap[receiver], m) + log.Info("Added method map to ", "receiver", receiver, "method", m.Name) + } + } + // Check for validation + if lineKeyMap := s.getValidation(funcDecl, p); len(lineKeyMap) > 1 { + sourceInfo.ValidationKeys[p.PkgPath + "." + s.getFuncName(funcDecl)] = lineKeyMap + } + if funcDecl.Name.Name == "init" { + sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, p.PkgPath) + } + } + } + } + + // Add the method specs to the struct specs. + for _, spec := range sourceInfo.StructSpecs { + spec.MethodSpecs = methodMap[spec.StructName] + } + + return +} +// Scan app source code for calls to X.Y(), where X is of type *Validation. +// +// Recognize these scenarios: +// - "Y" = "Validation" and is a member of the receiver. +// (The common case for inline validation) +// - "X" is passed in to the func as a parameter. +// (For structs implementing Validated) +// +// The line number to which a validation call is attributed is that of the +// surrounding ExprStmt. This is so that it matches what runtime.Callers() +// reports. +// +// The end result is that we can set the default validation key for each call to +// be the same as the local variable. +func (s *SourceInfoProcessor) getValidation(funcDecl *ast.FuncDecl, p *packages.Package) (map[int]string) { + var ( + lineKeys = make(map[int]string) + + // Check the func parameters and the receiver's members for the *revel.Validation type. + validationParam = s.getValidationParameter(funcDecl) + ) + + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // e.g. c.Validation.Required(arg) or v.Required(arg) + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // e.g. c.Validation.Required or v.Required + funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + switch x := funcSelector.X.(type) { + case *ast.SelectorExpr: // e.g. c.Validation + if x.Sel.Name != "Validation" { + return true + } + + case *ast.Ident: // e.g. v + if validationParam == nil || x.Obj != validationParam { + return true + } + + default: + return true + } + + if len(callExpr.Args) == 0 { + return true + } + + // Given the validation expression, extract the key. + key := callExpr.Args[0] + switch expr := key.(type) { + case *ast.BinaryExpr: + // If the argument is a binary expression, take the first expression. + // (e.g. c.Validation.Required(myName != "")) + key = expr.X + case *ast.UnaryExpr: + // If the argument is a unary expression, drill in. + // (e.g. c.Validation.Required(!myBool) + key = expr.X + case *ast.BasicLit: + // If it's a literal, skip it. + return true + } + + if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid { + lineKeys[p.Fset.Position(callExpr.End()).Line] = typeExpr.TypeName("") + } else { + s.sourceProcessor.log.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", p.PkgPath, + "line", p.Fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String()) + } + return true + }) + + return lineKeys + +} +// Check to see if there is a *revel.Validation as an argument. +func (s *SourceInfoProcessor) getValidationParameter(funcDecl *ast.FuncDecl) *ast.Object { + for _, field := range funcDecl.Type.Params.List { + starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation + if !ok { + continue + } + + selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation + if !ok { + continue + } + + xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev + if !ok { + continue + } + + if selExpr.Sel.Name == "Validation" && s.sourceProcessor.importMap[xIdent.Name] == model.RevelImportPath { + return field.Names[0].Obj + } + } + return nil +} +func (s *SourceInfoProcessor) getControllerFunc(funcDecl *ast.FuncDecl, p *packages.Package, localImportMap map[string]string) (method *model.MethodSpec, recvTypeName string) { + selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr) + if !ok { + return + } + if selExpr.Sel.Name != "Result" { + return + } + if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || s.sourceProcessor.importMap[pkgIdent.Name] != model.RevelImportPath { + return + } + method = &model.MethodSpec{ + Name: funcDecl.Name.Name, + } + + // Add a description of the arguments to the method. + for _, field := range funcDecl.Type.Params.List { + for _, name := range field.Names { + var importPath string + typeExpr := model.NewTypeExprFromAst(p.Name, field.Type) + if !typeExpr.Valid { + utils.Logger.Warn("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, s.getFuncName(funcDecl)) + return // We didn't understand one of the args. Ignore this action. + } + // Local object + if typeExpr.PkgName == p.Name { + importPath = p.PkgPath + } else if typeExpr.PkgName != "" { + var ok bool + if importPath, ok = localImportMap[typeExpr.PkgName]; !ok { + if importPath, ok = s.sourceProcessor.importMap[typeExpr.PkgName]; !ok { + utils.Logger.Error("Unable to find import", "importMap", s.sourceProcessor.importMap, "localimport", localImportMap) + utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName("")) + } + } + } + method.Args = append(method.Args, &model.MethodArg{ + Name: name.Name, + TypeExpr: typeExpr, + ImportPath: importPath, + }) + } + } + + // Add a description of the calls to Render from the method. + // Inspect every node (e.g. always return true). + method.RenderCalls = []*model.MethodCall{} + ast.Inspect(funcDecl.Body, func(node ast.Node) bool { + // Is it a function call? + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return true + } + + // Is it calling (*Controller).Render? + selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + // The type of the receiver is not easily available, so just store every + // call to any method called Render. + if selExpr.Sel.Name != "Render" { + return true + } + + // Add this call's args to the renderArgs. + pos := p.Fset.Position(callExpr.Lparen) + methodCall := &model.MethodCall{ + Line: pos.Line, + Names: []string{}, + } + for _, arg := range callExpr.Args { + argIdent, ok := arg.(*ast.Ident) + if !ok { + continue + } + methodCall.Names = append(methodCall.Names, argIdent.Name) + } + method.RenderCalls = append(method.RenderCalls, methodCall) + return true + }) + + var recvType = funcDecl.Recv.List[0].Type + if recvStarType, ok := recvType.(*ast.StarExpr); ok { + recvTypeName = recvStarType.X.(*ast.Ident).Name + } else { + recvTypeName = recvType.(*ast.Ident).Name + } + return +} +func (s *SourceInfoProcessor) getControllerSpec(spec *ast.TypeSpec, p *packages.Package, localImportMap map[string]string) (controllerSpec *model.TypeInfo) { + structType := spec.Type.(*ast.StructType) + + // At this point we know it's a type declaration for a struct. + // Fill in the rest of the info by diving into the fields. + // Add it provisionally to the Controller list -- it's later filtered using field info. + controllerSpec = &model.TypeInfo{ + StructName: spec.Name.Name, + ImportPath: p.PkgPath, + PackageName: p.Name, + } + log := s.sourceProcessor.log.New("file", p.Fset.Position(spec.Pos()).Filename, "position", p.Fset.Position(spec.Pos()).Line) + for _, field := range structType.Fields.List { + // If field.Names is set, it's not an embedded type. + if field.Names != nil { + continue + } + + // A direct "sub-type" has an ast.Field as either: + // Ident { "AppController" } + // SelectorExpr { "rev", "Controller" } + // Additionally, that can be wrapped by StarExprs. + fieldType := field.Type + pkgName, typeName := func() (string, string) { + // Drill through any StarExprs. + for { + if starExpr, ok := fieldType.(*ast.StarExpr); ok { + fieldType = starExpr.X + continue + } + break + } + + // If the embedded type is in the same package, it's an Ident. + if ident, ok := fieldType.(*ast.Ident); ok { + return "", ident.Name + } + + if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok { + if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok { + return pkgIdent.Name, selectorExpr.Sel.Name + } + } + return "", "" + }() + + // If a typename wasn't found, skip it. + if typeName == "" { + continue + } + + // Find the import path for this type. + // If it was referenced without a package name, use the current package import path. + // Else, look up the package's import path by name. + var importPath string + if pkgName == "" { + importPath = p.PkgPath + } else { + var ok bool + if importPath, ok = localImportMap[pkgName]; !ok { + log.Debug("Debug: Unusual, failed to find package locally ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", ) + if importPath, ok = s.sourceProcessor.importMap[pkgName]; !ok { + log.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", ) + continue + } + } + } + + controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{ + ImportPath: importPath, + StructName: typeName, + }) + } + s.sourceProcessor.log.Info("Added controller spec", "name", controllerSpec.StructName, "package", controllerSpec.ImportPath) + return +} +func (s *SourceInfoProcessor) getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) { + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + return + } + + if genDecl.Tok != token.TYPE { + return + } + + if len(genDecl.Specs) == 0 { + utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line) + return + } + + spec = genDecl.Specs[0].(*ast.TypeSpec) + _, found = spec.Type.(*ast.StructType) + + return + +} +func (s *SourceInfoProcessor) getFuncName(funcDecl *ast.FuncDecl) string { + prefix := "" + if funcDecl.Recv != nil { + recvType := funcDecl.Recv.List[0].Type + if recvStarType, ok := recvType.(*ast.StarExpr); ok { + prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")" + } else { + prefix = recvType.(*ast.Ident).Name + } + prefix += "." + } + return prefix + funcDecl.Name.Name +} +func (s *SourceInfoProcessor) addImport(decl ast.Decl, p *packages.Package, localImportMap map[string]string, log logger.MultiLogger) (shouldContinue bool) { + shouldContinue = true + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + return + } + + if genDecl.Tok == token.IMPORT { + shouldContinue = false + for _, spec := range genDecl.Specs { + importSpec := spec.(*ast.ImportSpec) + //fmt.Printf("*** import specification %#v\n", importSpec) + var pkgAlias string + if importSpec.Name != nil { + pkgAlias = importSpec.Name.Name + if pkgAlias == "_" { + continue + } + } + quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\"" + fullPath := quotedPath[1 : len(quotedPath) - 1] // Remove the quotes + if pkgAlias == "" { + pkgAlias = fullPath + if index := strings.LastIndex(pkgAlias, "/"); index > 0 { + pkgAlias = pkgAlias[index + 1:] + } + } + localImportMap[pkgAlias] = fullPath + } + + } + return +} \ No newline at end of file diff --git a/app/cmd/parser2/source_processor.go b/app/cmd/parser2/source_processor.go new file mode 100644 index 0000000..d0b20c1 --- /dev/null +++ b/app/cmd/parser2/source_processor.go @@ -0,0 +1,310 @@ +package parser2 + +import ( + "github.com/revel/cmd/logger" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "go/ast" + "go/parser" + "go/scanner" + "go/token" + "golang.org/x/tools/go/packages" + "os" + "path/filepath" + "strings" +) + +type ( + SourceProcessor struct { + revelContainer *model.RevelContainer + log logger.MultiLogger + packageList []*packages.Package + importMap map[string]string + packageMap map[string]string + sourceInfoProcessor *SourceInfoProcessor + sourceInfo *model.SourceInfo + } +) + +func ProcessSource(revelContainer *model.RevelContainer) (sourceInfo *model.SourceInfo, compileError error) { + utils.Logger.Info("ProcessSource") + processor := NewSourceProcessor(revelContainer) + compileError = processor.parse() + sourceInfo = processor.sourceInfo + if compileError == nil { + processor.log.Infof("From parsers : Structures:%d InitImports:%d ValidationKeys:%d %v", len(sourceInfo.StructSpecs), len(sourceInfo.InitImportPaths), len(sourceInfo.ValidationKeys), sourceInfo.PackageMap) + } + + return +} + +func NewSourceProcessor(revelContainer *model.RevelContainer) *SourceProcessor { + s := &SourceProcessor{revelContainer:revelContainer, log:utils.Logger.New("parser", "SourceProcessor")} + s.sourceInfoProcessor = NewSourceInfoProcessor(s) + return s +} + +func (s *SourceProcessor) parse() (compileError error) { + print("Parsing packages, (may require download if not cached)...") + if compileError = s.addPackages(); compileError != nil { + return + } + println(" Completed") + if compileError = s.addImportMap(); compileError != nil { + return + } + if compileError = s.addSourceInfo(); compileError != nil { + return + } + + s.sourceInfo.PackageMap = map[string]string{} + getImportFromMap := func(packagePath string) string { + for path := range s.packageMap { + if strings.Index(path, packagePath) == 0 { + fullPath := s.packageMap[path] + return fullPath[:(len(fullPath) - len(path) + len(packagePath))] + } + } + return "" + } + s.sourceInfo.PackageMap[model.RevelImportPath] = getImportFromMap(model.RevelImportPath) + s.sourceInfo.PackageMap[s.revelContainer.ImportPath] = getImportFromMap(s.revelContainer.ImportPath) + for _, module := range s.revelContainer.ModulePathMap { + s.sourceInfo.PackageMap[module.ImportPath] = getImportFromMap(module.ImportPath) + } + + return +} + +// 这两个方法来自util + +// Shortcut to fsWalk +func (s *SourceProcessor) Walk(root string, walkFn filepath.WalkFunc) error { + return s.fsWalk(root, root, walkFn) +} + +// Walk the path tree using the function +// Every file found will call the function +func (s *SourceProcessor) 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 strings.Contains(path, "/leanote/public") || + strings.Contains(path, "/leanote/files") || + strings.Contains(path, "/leanote/doc") || + strings.Contains(path, "/leanote/logs") || + strings.Contains(path, "/leanote/build") || + strings.Contains(path, "/leanote/target") { + s.log.Warn("public 或 files 不要处理", "path", path) + return filepath.SkipDir + } + + 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 s.fsWalk(symlinkPath, path, walkFn) + } + } + + return walkFn(path, info, err) + } + err := filepath.Walk(fname, fsWalkFunc) + return err +} + +// Using the packages.Load function load all the packages and type specifications (forces compile). +// this sets the SourceProcessor.packageList []*packages.Package +func (s *SourceProcessor) addPackages() (err error) { + allPackages := []string{model.RevelImportPath + "/..."} + for _, module := range s.revelContainer.ModulePathMap { + allPackages = append(allPackages, module.ImportPath + "/...") // +"/app/controllers/...") + } + s.log.Info("Reading packages", "packageList", allPackages) + //allPackages = []string{s.revelContainer.ImportPath + "/..."} //+"/app/controllers/..."} + + config := &packages.Config{ + // ode: packages.NeedSyntax | packages.NeedCompiledGoFiles, + Mode: + packages.NeedTypes | // For compile error + packages.NeedDeps | // To load dependent files + packages.NeedName | // Loads the full package name + packages.NeedSyntax, // To load ast tree (for end points) + //Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | + // packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | + // packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | + // packages.NeedTypesSizes, + + //Mode: packages.NeedName | packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | packages.NeedFiles | + // packages.NeedCompiledGoFiles | packages.NeedTypesSizes | + // packages.NeedSyntax | packages.NeedCompiledGoFiles , + //Mode: packages.NeedSyntax | packages.NeedCompiledGoFiles | packages.NeedName | packages.NeedFiles | + // packages.LoadTypes | packages.NeedTypes | packages.NeedDeps, //, // | + // packages.NeedTypes, // packages.LoadTypes | packages.NeedSyntax | packages.NeedTypesInfo, + //packages.LoadSyntax | packages.NeedDeps, + Dir:s.revelContainer.AppPath, + } + s.packageList, err = packages.Load(config, allPackages...) + s.log.Info("Loaded modules ", "len results", len(s.packageList), "error", err) + + // Now process the files in the aap source folder s.revelContainer.ImportPath + "/...", + err = s.Walk(s.revelContainer.BasePath, s.processPath) + s.log.Info("Loaded apps and modules ", "len results", len(s.packageList), "error", err) + return +} + +// This callback is used to build the packages for the "app" package. This allows us to +// parse the source files without doing a full compile on them +// This callback only processes folders, so any files passed to this will return a nil +func (s *SourceProcessor) processPath(path string, info os.FileInfo, err error) error { + if err != nil { + s.log.Error("Error scanning app source:", "error", err) + return nil + } + + // Ignore files and folders not marked tmp (since those are generated) + if !info.IsDir() || info.Name() == "tmp" { + return nil + } + + // Real work for processing the folder + pkgImportPath := s.revelContainer.ImportPath + appPath := s.revelContainer.BasePath + if appPath != path { + pkgImportPath = s.revelContainer.ImportPath + "/" + filepath.ToSlash(path[len(appPath) + 1:]) + } + s.log.Info("Processing source package folder", "package", pkgImportPath, "path", path) + + // Parse files within the path. + var pkgMap map[string]*ast.Package + fset := token.NewFileSet() + pkgMap, err = parser.ParseDir( + fset, + path, + func(f os.FileInfo) bool { + return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go") + }, + 0) + + if err != nil { + if errList, ok := err.(scanner.ErrorList); ok { + var pos = errList[0].Pos + newError := &utils.SourceError{ + SourceType: ".go source", + Title: "Go Compilation Error", + Path: pos.Filename, + Description: errList[0].Msg, + Line: pos.Line, + Column: pos.Column, + SourceLines: utils.MustReadLines(pos.Filename), + } + + errorLink := s.revelContainer.Config.StringDefault("error.link", "") + if errorLink != "" { + newError.SetLink(errorLink) + } + return newError + } + + // This is exception, err already checked above. Here just a print + ast.Print(nil, err) + s.log.Fatal("Failed to parse dir", "error", err) + } + + // Skip "main" packages. + delete(pkgMap, "main") + + // Ignore packages that end with _test + // These cannot be included in source code that is not generated specifically as a test + for i := range pkgMap { + if len(i) > 6 { + if string(i[len(i) - 5:]) == "_test" { + delete(pkgMap, i) + } + } + } + + // If there is no code in this directory, skip it. + if len(pkgMap) == 0 { + return nil + } + + // There should be only one package in this directory. + if len(pkgMap) > 1 { + for i := range pkgMap { + println("Found duplicate packages in single directory ", i) + } + utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgMap) + } + + // At this point there is only one package in the pkgs map, + p := &packages.Package{} + p.PkgPath = pkgImportPath + p.Fset = fset + for _, pkg := range pkgMap { + p.Name = pkg.Name + s.log.Info("Found package", "pkg.Name", pkg.Name, "p.Name", p.PkgPath) + for filename, astFile := range pkg.Files { + p.Syntax = append(p.Syntax, astFile) + p.GoFiles = append(p.GoFiles, filename) + } + } + s.packageList = append(s.packageList, p) + + return nil +} + +// This function is used to populate a map so that we can lookup controller embedded types in order to determine +// if a Struct inherits from from revel.Controller +func (s *SourceProcessor) addImportMap() (err error) { + s.importMap = map[string]string{} + s.packageMap = map[string]string{} + for _, p := range s.packageList { + + if len(p.Errors) > 0 { + // Generate a compile error + for _, e := range p.Errors { + s.log.Info("While reading packages encountered import error ignoring ", "PkgPath", p.PkgPath, "error", e) + } + } + for _, tree := range p.Syntax { + s.importMap[tree.Name.Name] = p.PkgPath + } + } + return +} + +func (s *SourceProcessor) addSourceInfo() (err error) { + for _, p := range s.packageList { + if sourceInfo := s.sourceInfoProcessor.processPackage(p); sourceInfo != nil { + if s.sourceInfo != nil { + s.sourceInfo.Merge(sourceInfo) + } else { + s.sourceInfo = sourceInfo + } + } + } + return +} diff --git a/app/cmd/revel.go b/app/cmd/revel.go new file mode 100644 index 0000000..3507977 --- /dev/null +++ b/app/cmd/revel.go @@ -0,0 +1,151 @@ +// 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" + "math/rand" + "os" + "runtime" + "strings" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/agtorre/gocolorize" + "github.com/revel/cmd/logger" + "github.com/revel/cmd/model" + "github.com/revel/cmd/utils" + "bytes" +) + +const ( + // RevelCmdImportPath Revel framework cmd tool import path + RevelCmdImportPath = "github.com/revel/cmd" + + // RevelCmdImportPath Revel framework cmd tool import path + RevelSkeletonsImportPath = "github.com/revel/skeletons" + + // 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) error + 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 commands +var Commands = []*Command{ + nil, // Safety net, prevent missing index from running + + // 只改了这个 + nil, + nil, + cmdBuild, +} + +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 len(os.Args) < 2 { + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + + if err := ParseArgs(c, parser, os.Args[1:]); err != nil { + fmt.Fprint(os.Stderr, err.Error() + "\n") + os.Exit(1) + } + + // Switch based on the verbose flag + if len(c.Verbose) > 1 { + utils.InitLogger(wd, logger.LvlDebug) + } else if len(c.Verbose) > 0 { + utils.InitLogger(wd, logger.LvlInfo) + } else { + utils.InitLogger(wd, logger.LvlWarn) + } + + // Setup package resolver + c.InitPackageResolver() + + if err := c.UpdateImportPath(); err != nil { + utils.Logger.Error(err.Error()) + parser.WriteHelp(os.Stdout) + os.Exit(1) + } + + command := Commands[c.Index] + println("Revel executing:", command.Short) + + if err := command.RunWith(c); err != nil { + utils.Logger.Error("Unable to execute", "error", err) + os.Exit(1) + } +} + +// Parse the arguments passed into the model.CommandConfig +func ParseArgs(c *model.CommandConfig, parser *flags.Parser, args []string) (err error) { + var extraArgs []string + if ini := flag.String("ini", "none", ""); *ini != "none" { + if err = flags.NewIniParser(parser).ParseFile(*ini); err != nil { + return + } + } else { + if extraArgs, err = parser.ParseArgs(args); err != nil { + return + } else { + switch parser.Active.Name { + case "new": + c.Index = model.NEW + case "run": + c.Index = model.RUN + case "build": + c.Index = model.BUILD + case "package": + c.Index = model.PACKAGE + case "clean": + c.Index = model.CLEAN + case "test": + c.Index = model.TEST + case "version": + c.Index = model.VERSION + } + } + } + + if !Commands[c.Index].UpdateConfig(c, extraArgs) { + buffer := &bytes.Buffer{} + parser.WriteHelp(buffer) + err = fmt.Errorf("Invalid command line arguements %v\n%s", extraArgs, buffer.String()) + } + + return +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} diff --git a/go.mod b/go.mod index 41a713b..7095e83 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,16 @@ go 1.15 require ( github.com/PuerkitoBio/goquery v1.6.1 + github.com/agtorre/gocolorize v1.0.0 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/garyburd/redigo v1.6.2 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac // indirect + github.com/jessevdk/go-flags v1.4.0 github.com/mattn/go-colorable v0.1.8 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/revel/cmd v1.0.3 github.com/revel/config v1.0.0 // indirect github.com/revel/log15 v2.11.20+incompatible // indirect github.com/revel/modules v1.0.0 @@ -21,7 +24,8 @@ require ( github.com/xeonx/timeago v1.0.0-rc4 // indirect golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 // indirect - gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce + golang.org/x/tools v0.0.0-20200219054238-753a1d49df85 + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/stack.v0 v0.0.0-20141108040640-9b43fcefddd0 // indirect ) diff --git a/go.sum b/go.sum index 0498fec..8149488 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,20 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go github.com/Masterminds/squirrel v1.3.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk= github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/agtorre/gocolorize v1.0.0 h1:TvGQd+fAqWQlDjQxSKe//Y6RaxK+RHpEU9X/zPmHW50= +github.com/agtorre/gocolorize v1.0.0/go.mod h1:cH6imfTkHVBRJhSOeSeEZhB4zqEYSq0sXuIyehgZMIY= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/garyburd/redigo v1.6.2 h1:yE/pwKCrbLpLpQICzYTeZ7JsTA/C53wFTJHaEtRqniM= @@ -28,6 +32,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac h1:n1DqxAo4oWPMvH1+v+DLYlMCecgumhhgnxAPdqDIFHI= github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -56,14 +62,19 @@ github.com/newrelic/go-agent v3.4.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDk github.com/patrickmn/go-cache v1.0.0 h1:3gD5McaYs9CxjyK5AXGcq8gdeCARtd/9gJDUvVeaZ0Y= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/revel/cmd v1.0.3 h1:76YrotXjCcHI+oY9UtV7VP2SMsPfvDyet+AstcLYXoY= +github.com/revel/cmd v1.0.3/go.mod h1:44aFxD+KIr2w22yamS179ZrdyupHEXyko+2IsPK7+Wk= github.com/revel/config v0.21.0/go.mod h1:GT4a9px5kDGRqLizcw/md0QFErrhen76toz4qS3oIoI= github.com/revel/config v1.0.0 h1:UAzLPQ+x9nJeP6a+H93G+AKEosg3OO2oVLBXK9oSN2U= github.com/revel/config v1.0.0/go.mod h1:GT4a9px5kDGRqLizcw/md0QFErrhen76toz4qS3oIoI= github.com/revel/cron v0.21.0/go.mod h1:WrSp8p1H1IfOGumbbDGrGf8dZLjNSnGSnwxTj3nG80I= github.com/revel/log15 v2.11.20+incompatible h1:JkA4tbwIo/UGEMumY50zndKq816RQW3LQ0wIpRc+32U= github.com/revel/log15 v2.11.20+incompatible/go.mod h1:l0WmLRs+IM1hBl4noJiBc2tZQiOgZyXzS1mdmFt+5Gc= +github.com/revel/modules v0.21.0/go.mod h1:UBlNmO9VGZo4j6Ptn2uC/26Iclefuic+V40jYRPBxQE= github.com/revel/modules v1.0.0 h1:JrqUDCU7y8Qfxrk+XcvhTUq8fv0hfzTCHNFSWYpsFBg= github.com/revel/modules v1.0.0/go.mod h1:wJwbm8ccPzf+a5LBv49x/6a0TdIqh/XVad8u6w+RW8E= github.com/revel/pathtree v0.0.0-20140121041023-41257a1839e9 h1:/d6kfjzjyx19ieWqMOXHSTLFuRxLOH15ZubtcAXExKw= @@ -73,7 +84,9 @@ github.com/revel/revel v1.0.0 h1:BsPFnKuuzXEkPtrjdjZHiDcvDmbBiBQvh7Z5c6kLb/Y= github.com/revel/revel v1.0.0/go.mod h1:VZWJnHjpDEtuGUuZJ2NO42XryitrtwsdVaJxfDeo5yc= github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e h1:3/9k/etUfgykjM3Rx8X0echJzo7gNNeND/ubPkqYw1k= github.com/robfig/config v0.0.0-20141207224736-0f78529c8c7e/go.mod h1:Zerq1qYbCKtIIU9QgPydffGlpYfZ8KI/si49wuTLY/Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= github.com/tylerb/gls v0.0.0-20150407001822-e606233f194d/go.mod h1:0MwyId/pXK5wkYYEXe7NnVknX+aNBuF73fLV3U0reU8= @@ -87,17 +100,21 @@ github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8= golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -106,6 +123,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -114,11 +132,16 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200219054238-753a1d49df85 h1:XNHaQ2CZDl/SjEZlUXGh7+OQvfLuFgmk3oNWkCFfERE= +golang.org/x/tools v0.0.0-20200219054238-753a1d49df85/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/stack.v0 v0.0.0-20141108040640-9b43fcefddd0 h1:lMH45EKqD8Nf6LwoF+43YOKjOAEEHQRVgDyG8RCV4MU=