//go:build !plan9 && !windows // +build !plan9,!windows package run import ( "fmt" "github.com/AlecAivazis/survey/v2" "github.com/fsnotify/fsnotify" "github.com/go-nunu/nunu/internal/pkg/helper" "github.com/spf13/cobra" "log" "os" "os/exec" "os/signal" "path/filepath" "runtime" "strings" "syscall" "time" ) var quit = make(chan os.Signal, 1) type Run struct { } var RunCmd = &cobra.Command{ Use: "run", Short: "nunu run [main.go path]", Long: "nunu wire [wire.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 == "" { // find the directory containing the cmd/* cmdPath, err := findCMD(base) 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) } 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) 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() // Add files to watcher err = filepath.Walk(watchPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { ext := filepath.Ext(info.Name()) if ext == ".go" || ext == ".yml" || ext == ".yaml" || ext == ".html" { err = watcher.Add(path) if err != nil { fmt.Println("Error:", err) } } } return nil }) if err != nil { fmt.Println("Error:", err) return } cmd := start(dir, programArgs) // Loop listening file modification for { select { case <-quit: err = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 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, programArgs) } case err := <-watcher.Errors: fmt.Println("Error:", err) } } } func start(dir string, programArgs []string) *exec.Cmd { cmd := exec.Command("go", append([]string{"run", dir}, programArgs...)...) // Set a new process group to kill all child processes when the program exits if runtime.GOOS == "windows" { cmd.SysProcAttr = &syscall.SysProcAttr{} } else { 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 } func findCMD(base string) (map[string]string, error) { wd, err := os.Getwd() if err != nil { return nil, err } if !strings.HasSuffix(wd, "/") { wd += "/" } var root bool next := func(dir string) (map[string]string, error) { cmdPath := make(map[string]string) err = filepath.Walk(dir, func(walkPath string, info os.FileInfo, err error) error { // multi level directory is not allowed under the cmdPath directory, so it is judged that the path ends with cmdPath. if strings.HasSuffix(walkPath, "cmd") { paths, err := os.ReadDir(walkPath) if err != nil { return err } for _, fileInfo := range paths { if fileInfo.IsDir() { abs := filepath.Join(walkPath, fileInfo.Name()) cmdPath[strings.TrimPrefix(abs, wd)] = abs } } return nil } if info.Name() == "go.mod" { root = true } return nil }) return cmdPath, err } for i := 0; i < 5; i++ { tmp := base cmd, err := next(tmp) if err != nil { return nil, err } if len(cmd) > 0 { return cmd, nil } if root { break } _ = filepath.Join(base, "..") } return map[string]string{"": base}, nil }