// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved // // This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. package cmd import ( "context" "fmt" "os" "path/filepath" "runtime" "strings" "time" "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gfsnotify" "github.com/gogf/gf/v2/os/gproc" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtimer" "github.com/gogf/gf/v2/util/gtag" "hotgo/internal/library/hggen/internal/utility/mlog" ) var Run = cRun{} type cRun struct { g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"` } type cRunApp struct { File string // Go run file name. Path string // Directory storing built binary. Options string // Extra "go run" options. Args string // Custom arguments. WatchPaths []string // Watch paths for live reload. } const ( cRunUsage = `gf run FILE [OPTION]` cRunBrief = `running go codes with hot-compiled-like feature` cRunEg = ` gf run main.go gf run main.go --args "server -p 8080" gf run main.go -mod=vendor gf run main.go -w "manifest/config/*.yaml" ` cRunDc = ` The "run" command is used for running go codes with hot-compiled-like feature, which compiles and runs the go codes asynchronously when codes change. ` cRunFileBrief = `building file path.` cRunPathBrief = `output directory path for built binary file. it's "./" in default` cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` cRunArgsBrief = `custom arguments for your process` cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml"` ) var process *gproc.Process func init() { gtag.Sets(g.MapStrStr{ `cRunUsage`: cRunUsage, `cRunBrief`: cRunBrief, `cRunEg`: cRunEg, `cRunDc`: cRunDc, `cRunFileBrief`: cRunFileBrief, `cRunPathBrief`: cRunPathBrief, `cRunExtraBrief`: cRunExtraBrief, `cRunArgsBrief`: cRunArgsBrief, `cRunWatchPathsBrief`: cRunWatchPathsBrief, }) } type ( cRunInput struct { g.Meta `name:"run" config:"gfcli.run"` File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` } cRunOutput struct{} ) func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err error) { if !gfile.Exists(in.File) { mlog.Fatalf(`given file "%s" not found`, in.File) } if !gfile.IsFile(in.File) { mlog.Fatalf(`given "%s" is not a file`, in.File) } // Necessary check. if gproc.SearchBinary("go") == "" { mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) } if len(in.WatchPaths) == 1 { in.WatchPaths = strings.Split(in.WatchPaths[0], ",") mlog.Printf("watchPaths: %v", in.WatchPaths) } app := &cRunApp{ File: in.File, Path: filepath.FromSlash(in.Path), Options: in.Extra, Args: in.Args, WatchPaths: in.WatchPaths, } dirty := gtype.NewBool() outputPath := app.genOutputPath() callbackFunc := func(event *gfsnotify.Event) { if !event.IsWrite() && !event.IsCreate() && !event.IsRemove() && !event.IsRename() { return } if gfile.ExtName(event.Path) != "go" { return } // Variable `dirty` is used for running the changes only one in one second. if !dirty.Cas(false, true) { return } // With some delay in case of multiple code changes in very short interval. gtimer.SetTimeout(ctx, 1500*gtime.MS, func(ctx context.Context) { defer dirty.Set(false) mlog.Printf(`watched file changes: %s`, event.String()) app.Run(ctx, outputPath) }) } if len(app.WatchPaths) > 0 { for _, path := range app.WatchPaths { _, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc) if err != nil { mlog.Fatal(err) } } } else { _, err = gfsnotify.Add(gfile.RealPath("."), callbackFunc) if err != nil { mlog.Fatal(err) } } go app.Run(ctx, outputPath) gproc.AddSigHandlerShutdown(func(sig os.Signal) { app.End(ctx, sig, outputPath) os.Exit(0) }) gproc.Listen() select {} } func (app *cRunApp) Run(ctx context.Context, outputPath string) { // Rebuild and run the codes. mlog.Printf("build: %s", app.File) // In case of `pipe: too many open files` error. // Build the app. buildCommand := fmt.Sprintf( `go build -o %s %s %s`, outputPath, app.Options, app.File, ) mlog.Print(buildCommand) result, err := gproc.ShellExec(ctx, buildCommand) if err != nil { mlog.Printf("build error: \n%s%s", result, err.Error()) return } // Kill the old process if build successfully. if process != nil { if err := process.Kill(); err != nil { mlog.Debugf("kill process error: %s", err.Error()) } } // Run the binary file. runCommand := fmt.Sprintf(`%s %s`, outputPath, app.Args) mlog.Print(runCommand) if runtime.GOOS == "windows" { // Special handling for windows platform. // DO NOT USE "cmd /c" command. process = gproc.NewProcess(outputPath, strings.Fields(app.Args)) } else { process = gproc.NewProcessCmd(runCommand, nil) } if pid, err := process.Start(ctx); err != nil { mlog.Printf("build running error: %s", err.Error()) } else { mlog.Printf("build running pid: %d", pid) } } func (app *cRunApp) End(ctx context.Context, sig os.Signal, outputPath string) { // Delete the binary file. // firstly, kill the process. if process != nil { if sig != nil && runtime.GOOS != "windows" { if err := process.Signal(sig); err != nil { mlog.Debugf("send signal to process error: %s", err.Error()) if err := process.Kill(); err != nil { mlog.Debugf("kill process error: %s", err.Error()) } } else { waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() done := make(chan error, 1) go func() { select { case <-waitCtx.Done(): done <- waitCtx.Err() case done <- process.Wait(): } }() err := <-done if err != nil { mlog.Debugf("process wait error: %s", err.Error()) if err := process.Kill(); err != nil { mlog.Debugf("kill process error: %s", err.Error()) } } else { mlog.Debug("process exited gracefully") } } } else { if err := process.Kill(); err != nil { mlog.Debugf("kill process error: %s", err.Error()) } } } if err := gfile.RemoveFile(outputPath); err != nil { mlog.Printf("delete binary file error: %s", err.Error()) } else { mlog.Printf("deleted binary file: %s", outputPath) } } func (app *cRunApp) genOutputPath() (outputPath string) { var renamePath string outputPath = gfile.Join(app.Path, gfile.Name(app.File)) if runtime.GOOS == "windows" { outputPath += ".exe" if gfile.Exists(outputPath) { renamePath = outputPath + "~" if err := gfile.Rename(outputPath, renamePath); err != nil { mlog.Print(err) } } } return filepath.FromSlash(outputPath) } func matchWatchPaths(watchPaths []string, eventPath string) bool { for _, path := range watchPaths { absPath, err := filepath.Abs(path) if err != nil { mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) continue } matched, err := filepath.Match(absPath, eventPath) if err != nil { mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) continue } if matched { return true } } return false }