//go:build !plan9 && !windows // +build !plan9,!windows package run import ( "fmt" "log" "os" "os/exec" "os/signal" "path/filepath" "sort" "strings" "syscall" "time" "github.com/go-nunu/nunu/config" "github.com/AlecAivazis/survey/v2" "github.com/fsnotify/fsnotify" "github.com/go-nunu/nunu/internal/pkg/helper" "github.com/spf13/cobra" ) var quit = make(chan os.Signal, 1) type Run struct { } var excludeDir string var includeExt string var buildFlags string func init() { CmdRun.Flags().StringVarP(&excludeDir, "excludeDir", "", excludeDir, `eg: nunu run --excludeDir="tmp,vendor,.git,.idea"`) CmdRun.Flags().StringVarP(&includeExt, "includeExt", "", includeExt, `eg: nunu run --includeExt="go,tpl,tmpl,html,yaml,yml,toml,ini,json"`) CmdRun.Flags().StringVarP(&buildFlags, "buildFlags", "", buildFlags, `eg: nunu run --buildFlags="-tag cse"`) if excludeDir == "" { excludeDir = config.RunExcludeDir } if includeExt == "" { includeExt = config.RunIncludeExt } } var CmdRun = &cobra.Command{ Use: "run", Short: "nunu run [main.go path]", Long: "nunu run [main.go path]", Example: "nunu run cmd/server", Run: func(cmd *cobra.Command, args []string) { cmdArgs, programArgs := helper.SplitArgs(cmd, args) var dir string if len(cmdArgs) > 0 { dir = cmdArgs[0] } base, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) return } if dir == "" { cmdPath, err := helper.FindMain(base, excludeDir) if err != nil { fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) return } switch len(cmdPath) { case 0: fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", "The cmd directory cannot be found in the current directory") return case 1: for _, v := range cmdPath { dir = v } default: var cmdPaths []string for k := range cmdPath { cmdPaths = append(cmdPaths, k) } sort.Strings(cmdPaths) prompt := &survey.Select{ Message: "Which directory do you want to run?", Options: cmdPaths, PageSize: 10, } e := survey.AskOne(prompt, &dir) if e != nil || dir == "" { return } dir = cmdPath[dir] } } signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) fmt.Printf("\033[35mNunu run %s.\033[0m\n", dir) fmt.Printf("\033[35mWatch excludeDir %s\033[0m\n", excludeDir) fmt.Printf("\033[35mWatch includeExt %s\033[0m\n", includeExt) fmt.Printf("\033[35mWatch buildFlags %s\033[0m\n", buildFlags) watch(dir, programArgs) }, } func watch(dir string, programArgs []string) { // Listening file path watchPath := "./" // Create a new file watcher watcher, err := fsnotify.NewWatcher() if err != nil { fmt.Println("Error:", err) return } defer watcher.Close() excludeDirArr := strings.Split(excludeDir, ",") includeExtArr := strings.Split(includeExt, ",") buildFlagsArr := make([]string, 0) if strings.TrimSpace(buildFlags) != "" { buildFlagsArr = strings.Split(buildFlags, " ") } includeExtMap := make(map[string]struct{}) for _, s := range includeExtArr { includeExtMap[s] = struct{}{} } // Add files to watcher err = filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } for _, s := range excludeDirArr { if s == "" { continue } if strings.HasPrefix(path, s) { return nil } } if !info.IsDir() { ext := filepath.Ext(info.Name()) if _, ok := includeExtMap[strings.TrimPrefix(ext, ".")]; ok { err = watcher.Add(path) if err != nil { fmt.Println("Error:", err) } } } return nil }) if err != nil { fmt.Println("Error:", err) return } cmd := start(dir, buildFlagsArr, programArgs) // Loop listening file modification for { select { case <-quit: err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) if err != nil { fmt.Printf("\033[31mserver exiting...\033[0m\n") return } fmt.Printf("\033[31mserver exiting...\033[0m\n") os.Exit(0) case event := <-watcher.Events: // The file has been modified or created if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Remove == fsnotify.Remove { fmt.Printf("\033[36mfile modified: %s\033[0m\n", event.Name) syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) cmd = start(dir, buildFlagsArr, programArgs) } case err := <-watcher.Errors: fmt.Println("Error:", err) } } } func start(dir string, buildFlagsArgs []string, programArgs []string) *exec.Cmd { run := []string{"run"} run = append(run, buildFlagsArgs...) run = append(run, dir) cmd := exec.Command("go", append(run, programArgs...)...) // Set a new process group to kill all child processes when the program exits cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Start() if err != nil { log.Fatalf("\033[33;1mcmd run failed\u001B[0m") } time.Sleep(time.Second) fmt.Printf("\033[32;1mrunning...\033[0m\n") return cmd }