mirror of
https://github.com/go-nunu/nunu.git
synced 2025-10-16 13:10:41 +08:00
217 lines
4.7 KiB
Go
217 lines
4.7 KiB
Go
//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"
|
||
)
|
||
|
||
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)
|
||
|
||
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() && filepath.Ext(info.Name()) != ".log" && info.Name() != ".git" {
|
||
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 {
|
||
log.Println("server exiting...", err)
|
||
return
|
||
}
|
||
log.Println("server exiting...")
|
||
os.Exit(0)
|
||
|
||
case event := <-watcher.Events:
|
||
// The file has been modified or created
|
||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
||
fmt.Println("File modified:", event.Name)
|
||
err = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||
if err != nil {
|
||
fmt.Println("Stopping program failed:", err)
|
||
return
|
||
}
|
||
fmt.Println("Program restart in progress...")
|
||
|
||
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 {
|
||
fmt.Println("cmd run failed")
|
||
}
|
||
fmt.Println("Program started")
|
||
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
|
||
}
|